diff --git a/router/editor.go b/router/editor.go new file mode 100644 index 0000000..8b7c8f7 --- /dev/null +++ b/router/editor.go @@ -0,0 +1,107 @@ +package router + +import ( + "errors" + "fmt" + "io" + "net/http" + + "forge.capytal.company/capytalcode/project-comicverse/service" + "forge.capytal.company/loreddev/x/smalltrip/exception" +) + +func (router *router) pages(w http.ResponseWriter, r *http.Request) { + router.assert.NotNil(w) + router.assert.NotNil(r) + + id := r.PathValue("ID") + if id == "" { + exception. + BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)). + ServeHTTP(w, r) + return + } + + switch getMethod(r) { + case http.MethodGet, http.MethodHead: + imgID := r.PathValue("PageID") + if imgID == "" { + exception. + BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)). + ServeHTTP(w, r) + return + } + + router.getPage(w, r) + + case http.MethodPost: + router.addPage(w, r) + + default: + exception. + MethodNotAllowed([]string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + }). + ServeHTTP(w, r) + } +} + +func (router *router) addPage(w http.ResponseWriter, r *http.Request) { + router.assert.NotNil(w) + router.assert.NotNil(r) + router.assert.NotNil(router.service) + + id := r.PathValue("ID") + router.assert.NotZero(id, "This method should be used after the path values are checked") + + img, _, err := r.FormFile("image") + if err != nil { + // TODO: Handle if the file is bigger than allowed by ParseForm (10mb) + exception.InternalServerError(err).ServeHTTP(w, r) + return + } + + err = router.service.AddPage(id, img) + if err != nil { + exception.InternalServerError(err).ServeHTTP(w, r) + return + } + + http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther) +} + +func (router *router) getPage(w http.ResponseWriter, r *http.Request) { + router.assert.NotNil(w) + router.assert.NotNil(r) + router.assert.NotNil(router.service) + + id := r.PathValue("ID") + router.assert.NotZero(id, "This method should be used after the path values are checked") + + imgID := r.PathValue("PageID") + router.assert.NotZero(imgID, "This method should be used after the path values are checked") + + img, err := router.service.GetPage(id, imgID) + if errors.Is(err, service.ErrPageNotExists) { + exception.NotFound(exception.WithError(err)).ServeHTTP(w, r) + return + } + if err != nil { + exception.InternalServerError(err).ServeHTTP(w, r) + return + } + + if i, ok := img.(io.WriterTo); ok { + _, err = i.WriteTo(w) + } else { + _, err = io.Copy(w, img) + } + + if err != nil { + exception.InternalServerError(err).ServeHTTP(w, r) + return + } +} + diff --git a/router/router.go b/router/router.go index 866640a..73ab3bf 100644 --- a/router/router.go +++ b/router/router.go @@ -97,6 +97,8 @@ func (router *router) setup() http.Handler { r.HandleFunc("/projects/{$}", router.projects) r.HandleFunc("/projects/{ID}/", router.projects) + r.HandleFunc("/projects/{ID}/pages/{$}", router.pages) + r.HandleFunc("/projects/{ID}/pages/{PageID}", router.pages) return r } diff --git a/service/editor.go b/service/editor.go new file mode 100644 index 0000000..fc67511 --- /dev/null +++ b/service/editor.go @@ -0,0 +1,82 @@ +package service + +import ( + "errors" + "fmt" + "io" + "net/http" + + "forge.capytal.company/capytalcode/project-comicverse/internals/randstr" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +const pageIDLength = 6 + +var ErrPageNotExists = errors.New("page does not exists in storage") + +type Project struct { + ID string `json:"id"` + Title string `json:"title"` + Pages map[string]ProjectPage `json:"pages"` +} + +type ProjectPage struct{} + +func (s *Service) AddPage(projectID string, img io.Reader) error { + s.assert.NotNil(s.ctx) + s.assert.NotNil(s.s3) + s.assert.NotNil(s.bucket) + s.assert.NotZero(projectID) + s.assert.NotNil(img) + + id, err := randstr.NewHex(pageIDLength) + if err != nil { + return err + } + + p, err := s.GetProject(projectID) + if err != nil { + return errors.Join(errors.New("unable to get project"), err) + } + + p.Pages[id] = ProjectPage{} + + k := fmt.Sprintf("%s/%s", projectID, id) + _, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{ + Key: &k, + Body: img, + Bucket: &s.bucket, + }) + if err != nil { + return err + } + + err = s.UpdateProject(projectID, p) + return err +} + +func (s *Service) GetPage(projectID string, imgID string) (io.Reader, error) { + s.assert.NotNil(s.ctx) + s.assert.NotNil(s.s3) + s.assert.NotNil(s.bucket) + s.assert.NotZero(projectID) + s.assert.NotNil(imgID) + + k := fmt.Sprintf("%s/%s", projectID, imgID) + res, err := s.s3.GetObject(s.ctx, &s3.GetObjectInput{ + Key: &k, + Bucket: &s.bucket, + }) + if err != nil { + var resErr *awshttp.ResponseError + if errors.As(err, &resErr) && resErr.ResponseError.HTTPStatusCode() == http.StatusNotFound { + return nil, errors.Join(ErrPageNotExists, resErr) + } + return nil, err + } + + s.assert.NotNil(res.Body) + return res.Body, nil +} + diff --git a/service/projects.go b/service/projects.go index 60e4bbf..8752d37 100644 --- a/service/projects.go +++ b/service/projects.go @@ -2,7 +2,7 @@ package service import ( "bytes" - "encoding/xml" + "encoding/json" "errors" "fmt" "io" @@ -17,13 +17,6 @@ const projectIDLength = 6 var ErrProjectNotExists = errors.New("project does not exists in database") -type Project struct { - XMLName xml.Name `xml:"body"` - ID string `xml:"id,attr"` - Title string `xml:"h1"` - Contents string `xml:"-"` -} - func (s *Service) CreateProject() (Project, error) { s.assert.NotNil(s.db) s.assert.NotNil(s.s3) @@ -51,16 +44,17 @@ func (s *Service) CreateProject() (Project, error) { p := Project{ ID: id, Title: title, + Pages: map[string]ProjectPage{}, } - c, err := xml.Marshal(p) + c, err := json.Marshal(p) if err != nil { return Project{}, err } s.log.Debug("Creating project on storage", slog.String("id", id)) - f := fmt.Sprintf("%s.comic.xml", id) + f := fmt.Sprintf("%s.comic.json", id) _, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{ Bucket: &s.bucket, Key: &f, @@ -70,8 +64,6 @@ func (s *Service) CreateProject() (Project, error) { return Project{}, err } - p.Contents = string(c) - return p, nil } @@ -90,28 +82,27 @@ func (s *Service) GetProject(id string) (Project, error) { return Project{}, err } - p := Project{ - ID: res.ID, - Title: res.Title, - } - - f := fmt.Sprintf("%s.comic.xml", p.ID) + f := fmt.Sprintf("%s.comic.json", id) file, err := s.s3.GetObject(s.ctx, &s3.GetObjectInput{ Bucket: &s.bucket, Key: &f, }) if err != nil { - return p, err + return Project{}, err } c, err := io.ReadAll(file.Body) if err != nil { - return p, err + return Project{}, err } - p.Contents = string(c) + var p Project + err = json.Unmarshal(c, &p) - return p, nil + s.assert.Equal(res.ID, p.ID, "The project ID should always be equal in the Database and Storage") + s.assert.Equal(res.Title, p.Title) + + return p, err } func (s *Service) ListProjects() ([]Project, error) { @@ -123,16 +114,46 @@ func (s *Service) ListProjects() ([]Project, error) { } p := make([]Project, len(ps)) - for i := range p { - p[i] = Project{ - ID: ps[i].ID, - Title: ps[i].Title, + for i, dp := range ps { + // TODO: this is temporally for debugging, getting every project + // from s3 can be expensive + v, err := s.GetProject(dp.ID) + if err != nil { + return []Project{}, err } + p[i] = v } return p, nil } +func (s *Service) UpdateProject(id string, project Project) error { + s.assert.NotNil(s.db) + s.assert.NotNil(s.s3) + s.assert.NotZero(s.bucket) + s.assert.NotNil(s.ctx) + s.assert.NotZero(id) + + c, err := json.Marshal(project) + if err != nil { + return err + } + + s.log.Debug("Updating project on storage", slog.String("id", id)) + + f := fmt.Sprintf("%s.comic.json", id) + _, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{ + Bucket: &s.bucket, + Body: bytes.NewReader(c), + Key: &f, + }) + if err != nil { + return err + } + + return nil +} + func (s *Service) DeleteProject(id string) error { s.assert.NotNil(s.db) s.assert.NotNil(s.s3) @@ -145,7 +166,7 @@ func (s *Service) DeleteProject(id string) error { return err } - f := fmt.Sprintf("%s.comic.xml", id) + f := fmt.Sprintf("%s.comic.json", id) _, err = s.s3.DeleteObject(s.ctx, &s3.DeleteObjectInput{ Bucket: &s.bucket, Key: &f,