package route import ( "encoding/json" "fmt" "log" "net/http" "strconv" jsonpatch "gopkg.in/evanphx/json-patch.v4" "forge.cadoles.com/wpetit/guesstimate/internal/config" "forge.cadoles.com/wpetit/guesstimate/internal/model" "forge.cadoles.com/wpetit/guesstimate/internal/storm" "github.com/SebastiaanKlippert/go-wkhtmltopdf" "github.com/go-chi/chi" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/middleware/container" ) type projectResponse struct { Version uint64 `json:"version"` Project *model.Project `json:"project"` } func handleGetProject(w http.ResponseWriter, r *http.Request) { ctn := container.Must(r.Context()) db := storm.Must(ctn) projectID := getProjectID(r) var ( version uint64 err error ) rawVersion := r.URL.Query().Get("version") if rawVersion != "" { version, err = strconv.ParseUint(rawVersion, 10, 64) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } } tx, err := db.Begin(false) if err != nil { panic(errors.Wrap(err, "could not start transaction")) } defer func() { if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction { panic(errors.Wrap(err, "could not rollback transaction")) } }() entry := &model.ProjectEntry{} if err := tx.One("ID", projectID, entry); err != nil { if err == storm.ErrNotFound { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } panic(errors.Wrapf(err, "could not find project '%s'", projectID)) } if rawVersion != "" && entry.Version == version { http.Error(w, http.StatusText(http.StatusNotModified), http.StatusNotModified) return } if err := writeJSON(w, http.StatusOK, &projectResponse{entry.Version, entry.Project}); err != nil { panic(errors.Wrap(err, "could not write json")) } } func handleExportProject(w http.ResponseWriter, r *http.Request) { ctn := container.Must(r.Context()) cfg := config.Must(ctn) projectID := getProjectID(r) var ( err error url string ) url = string(cfg.Client.PublicBaseURL) + "/pdf/" + string(projectID) // Create new PDF generator pdfg, err := wkhtmltopdf.NewPDFGenerator() if err != nil { log.Fatal(err) } // Set global options pdfg.Dpi.Set(300) pdfg.Orientation.Set(wkhtmltopdf.OrientationPortrait) pdfg.Grayscale.Set(true) // Create a new input page from an URL page := wkhtmltopdf.NewPage(url) // Set options for this page page.FooterRight.Set("[page]") page.FooterFontSize.Set(10) page.Zoom.Set(0.95) // Add to document pdfg.AddPage(page) // Create PDF document in internal buffer err = pdfg.Create() if err != nil { log.Fatal(err) } rsp, err := writePDF(w, http.StatusOK, pdfg.Bytes()) if err != nil { panic(errors.Wrap(err, "could not write pdf response")) } fmt.Println(rsp) } type createRequest struct { Project *model.Project `json:"project"` } func handleCreateProject(w http.ResponseWriter, r *http.Request) { ctn := container.Must(r.Context()) db := storm.Must(ctn) projectID := getProjectID(r) log.Printf("handling create request for project %s", projectID) createReq := &createRequest{} if err := parseJSONBody(r, createReq); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) panic(errors.Wrap(err, "could not parse create request")) } tx, err := db.Begin(true) if err != nil { panic(errors.Wrap(err, "could not start transaction")) } defer func() { if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction { panic(errors.Wrap(err, "could not rollback transaction")) } }() entry := &model.ProjectEntry{} err = tx.One("ID", projectID, entry) if err == nil { http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict) return } if err != storm.ErrNotFound { panic(errors.Wrapf(err, "could not check project '%s'", projectID)) } entry.ID = projectID entry.Project = createReq.Project entry.Version = 0 if err := tx.Save(entry); err != nil { panic(errors.Wrap(err, "could not save project")) } if err := tx.Commit(); err != nil { panic(errors.Wrap(err, "could not commit transaction")) } if err := writeJSON(w, http.StatusCreated, &projectResponse{entry.Version, entry.Project}); err != nil { panic(errors.Wrap(err, "could not write json response")) } } type patchRequest struct { Version uint64 `json:"version"` Patch json.RawMessage `json:"patch"` } func handlePatchProject(w http.ResponseWriter, r *http.Request) { ctn := container.Must(r.Context()) db := storm.Must(ctn) projectID := getProjectID(r) log.Printf("handling patch request for project %s", projectID) patchReq := &patchRequest{} if err := parseJSONBody(r, patchReq); err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) panic(errors.Wrap(err, "could not parse patch request")) } tx, err := db.Begin(true) if err != nil { panic(errors.Wrap(err, "could not start transaction")) } defer func() { if err := tx.Rollback(); err != nil && err != storm.ErrNotInTransaction { panic(errors.Wrap(err, "could not rollback transaction")) } }() entry := &model.ProjectEntry{} if err := tx.One("ID", projectID, entry); err != nil { if err == storm.ErrNotFound { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } panic(errors.Wrapf(err, "could not find project '%s'", projectID)) } if entry.Version != patchReq.Version { if err := writeJSON(w, http.StatusConflict, &projectResponse{entry.Version, entry.Project}); err != nil { panic(errors.Wrap(err, "could not write json response")) } return } projectData, err := json.Marshal(entry.Project) if err != nil { panic(errors.Wrap(err, "could not marshal project")) } projectData, err = jsonpatch.MergePatch(projectData, patchReq.Patch) if err != nil { panic(errors.Wrap(err, "could not merge project patch")) } newProject := &model.Project{} if err := json.Unmarshal(projectData, newProject); err != nil { panic(errors.Wrap(err, "could not merge project patch")) } entry.Version++ entry.Project = newProject if err := tx.Save(entry); err != nil { panic(errors.Wrap(err, "could not save project")) } if err := tx.Commit(); err != nil { panic(errors.Wrap(err, "could not commit transaction")) } if err := writeJSON(w, http.StatusOK, &projectResponse{entry.Version, entry.Project}); err != nil { panic(errors.Wrap(err, "could not write json response")) } } func handleDeleteProject(w http.ResponseWriter, r *http.Request) { } func getProjectID(r *http.Request) model.ProjectID { return model.ProjectID(chi.URLParam(r, "projectID")) } func parseJSONBody(r *http.Request, payload interface{}) (err error) { decoder := json.NewDecoder(r.Body) defer func() { if err = r.Body.Close(); err != nil { err = errors.Wrap(err, "could not close request body") } }() if err := decoder.Decode(payload); err != nil { return errors.Wrap(err, "could not decode request body") } return nil } func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) error { encoder := json.NewEncoder(w) encoder.SetIndent("", " ") w.WriteHeader(statusCode) w.Header().Set("Content-Type", "application/json") return encoder.Encode(data) } func writePDF(w http.ResponseWriter, statusCode int, data []byte) (int, error) { w.Header().Set("Content-Disposition", "attachment; filename=estimation.pdf") w.Header().Set("Content-Type", "application/pdf") w.WriteHeader(statusCode) return w.Write(data) }