chore(router,service): remove editor and projects endpoint and services
They will be reimplemented later
This commit is contained in:
283
router/editor.go
283
router/editor.go
@@ -1,283 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"forge.capytal.company/capytalcode/project-comicverse/internals/randstr"
|
||||
"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)
|
||||
|
||||
// TODO: Check if project exists
|
||||
id := r.PathValue("ID")
|
||||
if id == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
pageID := r.PathValue("PageID")
|
||||
|
||||
switch getMethod(r) {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
if pageID == "" {
|
||||
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)
|
||||
|
||||
case http.MethodDelete:
|
||||
if pageID == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
router.deletePage(w, r)
|
||||
|
||||
default:
|
||||
exception.
|
||||
MethodNotAllowed([]string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodDelete,
|
||||
}).
|
||||
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")
|
||||
|
||||
pageID := r.PathValue("PageID")
|
||||
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
|
||||
|
||||
page, err := router.service.GetPage(id, pageID)
|
||||
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 := page.Image.(io.WriterTo); ok {
|
||||
_, err = i.WriteTo(w)
|
||||
} else {
|
||||
_, err = io.Copy(w, page.Image)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (router *router) deletePage(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")
|
||||
|
||||
pageID := r.PathValue("PageID")
|
||||
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
|
||||
|
||||
err := router.service.DeletePage(id, pageID)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (router *router) interactions(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
|
||||
// TODO: Check if the project exists
|
||||
id := r.PathValue("ID")
|
||||
if id == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Check if page exists
|
||||
pageID := r.PathValue("PageID")
|
||||
if pageID == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "PageID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
interactionID := r.PathValue("InteractionID")
|
||||
|
||||
switch getMethod(r) {
|
||||
case http.MethodPost:
|
||||
router.addInteraction(w, r)
|
||||
|
||||
case http.MethodDelete:
|
||||
if interactionID == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "InteractionID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
router.deleteInteraction(w, r)
|
||||
|
||||
default:
|
||||
exception.
|
||||
MethodNotAllowed([]string{
|
||||
http.MethodPost,
|
||||
http.MethodDelete,
|
||||
}).
|
||||
ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (router *router) addInteraction(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")
|
||||
|
||||
pageID := r.PathValue("PageID")
|
||||
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
|
||||
|
||||
// TODO: Methods to manipulate interactions, instead of router need to do this logic
|
||||
page, err := router.service.GetPage(id, pageID)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
page.Image = nil // HACK: Prevent image update on S3
|
||||
|
||||
x, err := strconv.ParseUint(r.FormValue("x"), 10, 0)
|
||||
if err != nil {
|
||||
exception.
|
||||
BadRequest(errors.Join(errors.New(`value "x" should be a valid non-negative integer`), err)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
y, err := strconv.ParseUint(r.FormValue("y"), 10, 0)
|
||||
if err != nil {
|
||||
exception.
|
||||
BadRequest(errors.Join(errors.New(`value "y" should be a valid non-negative integer`), err)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
link := r.FormValue("link")
|
||||
if link == "" {
|
||||
exception.BadRequest(errors.New(`missing parameter "link" in request`)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
intID, err := randstr.NewHex(6)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
page.Interactions[intID] = service.PageInteraction{
|
||||
X: uint16(x),
|
||||
Y: uint16(y),
|
||||
URL: link,
|
||||
}
|
||||
|
||||
err = router.service.UpdatePage(id, page)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (router *router) deleteInteraction(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")
|
||||
|
||||
pageID := r.PathValue("PageID")
|
||||
router.assert.NotZero(pageID, "This method should be used after the path values are checked")
|
||||
|
||||
interactionID := r.PathValue("InteractionID")
|
||||
router.assert.NotZero(interactionID, "This method should be used after the path values are checked")
|
||||
|
||||
// TODO: Methods to manipulate interactions, instead of router need to do this logic
|
||||
page, err := router.service.GetPage(id, pageID)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
page.Image = nil // HACK: Prevent image update on S3
|
||||
|
||||
delete(page.Interactions, interactionID)
|
||||
|
||||
err = router.service.UpdatePage(id, page)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/projects/%s/", id), http.StatusSeeOther)
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"forge.capytal.company/capytalcode/project-comicverse/service"
|
||||
"forge.capytal.company/loreddev/x/smalltrip/exception"
|
||||
)
|
||||
|
||||
func (router *router) projects(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
|
||||
switch getMethod(r) {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
if id := r.PathValue("ID"); id != "" {
|
||||
router.getProject(w, r)
|
||||
} else {
|
||||
router.listProjects(w, r)
|
||||
}
|
||||
|
||||
case http.MethodPost:
|
||||
router.createProject(w, r)
|
||||
|
||||
case http.MethodDelete:
|
||||
if id := r.PathValue("ID"); id != "" {
|
||||
router.deleteProject(w, r)
|
||||
} else {
|
||||
exception.
|
||||
BadRequest(errors.New(`missing "ID" path value`)).
|
||||
ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
default:
|
||||
exception.MethodNotAllowed([]string{
|
||||
http.MethodHead,
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodDelete,
|
||||
}).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (router *router) createProject(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
router.assert.NotNil(router.service)
|
||||
|
||||
if getMethod(r) != http.MethodPost {
|
||||
exception.
|
||||
MethodNotAllowed([]string{http.MethodPost}).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := router.service.CreateProject()
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
router.assert.NotZero(p.ID)
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/", path.Join(r.URL.Path, p.ID)), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (router *router) getProject(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
router.assert.NotNil(router.service)
|
||||
router.assert.NotNil(router.templates)
|
||||
|
||||
if getMethod(r) != http.MethodGet && getMethod(r) != http.MethodHead {
|
||||
exception.
|
||||
MethodNotAllowed([]string{http.MethodGet, http.MethodHead}).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("ID")
|
||||
if id == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := router.service.GetProject(id)
|
||||
switch {
|
||||
case errors.Is(err, service.ErrProjectNotExists):
|
||||
exception.NotFound().ServeHTTP(w, r)
|
||||
return
|
||||
|
||||
case err != nil:
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err = router.templates.ExecuteTemplate(w, "project", p)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (router *router) listProjects(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
router.assert.NotNil(router.service)
|
||||
router.assert.NotNil(router.templates)
|
||||
|
||||
if getMethod(r) != http.MethodGet && getMethod(r) != http.MethodHead {
|
||||
exception.
|
||||
MethodNotAllowed([]string{http.MethodGet, http.MethodHead}).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ps, err := router.service.ListProjects()
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(ps)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (router *router) deleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
router.assert.NotNil(router.service)
|
||||
router.assert.NotNil(router.templates)
|
||||
|
||||
if getMethod(r) != http.MethodDelete {
|
||||
exception.
|
||||
MethodNotAllowed([]string{http.MethodDelete}).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("ID")
|
||||
if id == "" {
|
||||
exception.
|
||||
BadRequest(fmt.Errorf(`a valid path value of "ID" must be provided`)).
|
||||
ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err := router.service.DeleteProject(id)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err = router.templates.ExecuteTemplate(w, "partials-status", map[string]any{
|
||||
"StatusCode": http.StatusOK,
|
||||
"Message": fmt.Sprintf("Project %q successfully deleted", id),
|
||||
"Redirect": "/dashboard/",
|
||||
"RedirectMessage": "Go back to dashboard",
|
||||
})
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.capytal.company/capytalcode/project-comicverse/service"
|
||||
"forge.capytal.company/capytalcode/project-comicverse/templates"
|
||||
@@ -95,44 +94,6 @@ func (router *router) setup() http.Handler {
|
||||
|
||||
r.HandleFunc("/dashboard/", router.dashboard)
|
||||
|
||||
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)
|
||||
r.HandleFunc("/projects/{ID}/pages/{PageID}/interactions/{$}", router.interactions)
|
||||
r.HandleFunc("/projects/{ID}/pages/{PageID}/interactions/{InteractionID}", router.interactions)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (router *router) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
router.assert.NotNil(router.templates)
|
||||
router.assert.NotNil(router.service)
|
||||
router.assert.NotNil(w)
|
||||
router.assert.NotNil(r)
|
||||
|
||||
p, err := router.service.ListProjects()
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = router.templates.ExecuteTemplate(w, "dashboard", p)
|
||||
if err != nil {
|
||||
exception.InternalServerError(err).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func getMethod(r *http.Request) string {
|
||||
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
||||
return r.Method
|
||||
}
|
||||
|
||||
m := r.FormValue("x-method")
|
||||
if m == "" {
|
||||
return r.Method
|
||||
}
|
||||
|
||||
return strings.ToUpper(m)
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"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 []ProjectPage `json:"pages"`
|
||||
}
|
||||
|
||||
type ProjectPage struct {
|
||||
ID string `json:"id"`
|
||||
Interactions map[string]PageInteraction `json:"interactions"`
|
||||
Image io.ReadCloser `json:"-"`
|
||||
}
|
||||
|
||||
type PageInteraction struct {
|
||||
URL string `json:"url"`
|
||||
X uint16 `json:"x"`
|
||||
Y uint16 `json:"y"`
|
||||
}
|
||||
|
||||
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 = append(p.Pages, ProjectPage{ID: id, Interactions: map[string]PageInteraction{}})
|
||||
|
||||
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, pageID string) (ProjectPage, error) {
|
||||
s.assert.NotNil(s.ctx)
|
||||
s.assert.NotNil(s.s3)
|
||||
s.assert.NotNil(s.bucket)
|
||||
s.assert.NotZero(projectID)
|
||||
s.assert.NotNil(pageID)
|
||||
|
||||
p, err := s.GetProject(projectID)
|
||||
if err != nil {
|
||||
return ProjectPage{}, errors.Join(errors.New("unable to get project"), err)
|
||||
}
|
||||
|
||||
pageIndex := slices.IndexFunc(p.Pages, func(p ProjectPage) bool { return p.ID == pageID })
|
||||
if pageIndex == -1 {
|
||||
return ProjectPage{}, ErrPageNotExists
|
||||
}
|
||||
page := p.Pages[pageIndex]
|
||||
|
||||
k := fmt.Sprintf("%s/%s", projectID, pageID)
|
||||
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 {
|
||||
// TODO: This would probably be better in some background "maintenance" worker
|
||||
p.Pages = slices.Delete(p.Pages, pageIndex, pageIndex)
|
||||
_ = s.UpdateProject(projectID, p)
|
||||
|
||||
return ProjectPage{}, errors.Join(ErrPageNotExists, resErr)
|
||||
}
|
||||
return ProjectPage{}, err
|
||||
}
|
||||
|
||||
s.assert.NotNil(res.Body)
|
||||
s.assert.NotNil(page.Interactions)
|
||||
|
||||
page.Image = res.Body
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdatePage(projectID string, page ProjectPage) error {
|
||||
s.assert.NotNil(s.ctx)
|
||||
s.assert.NotNil(s.s3)
|
||||
s.assert.NotNil(s.bucket)
|
||||
s.assert.NotZero(projectID)
|
||||
s.assert.NotZero(page.ID)
|
||||
s.assert.NotNil(page.Interactions)
|
||||
|
||||
p, err := s.GetProject(projectID)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("unable to get project"), err)
|
||||
}
|
||||
|
||||
pageIndex := slices.IndexFunc(p.Pages, func(p ProjectPage) bool { return p.ID == page.ID })
|
||||
if pageIndex == -1 {
|
||||
return ErrPageNotExists
|
||||
}
|
||||
p.Pages[pageIndex] = page
|
||||
|
||||
// TODO: Probably a "lastUpdated" timestamp in the ProjectPage data would be better
|
||||
// so we don't update equal images. Changing the image in ProjectPage would be better
|
||||
// using a method, or could be completely decoupled from the struct.
|
||||
if page.Image != nil {
|
||||
k := fmt.Sprintf("%s/%s", projectID, page.ID)
|
||||
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
|
||||
Key: &k,
|
||||
Body: page.Image,
|
||||
Bucket: &s.bucket,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("error while trying to update image"), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.UpdateProject(projectID, p)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("error while trying to update project"), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) DeletePage(projectID string, id string) error {
|
||||
s.assert.NotNil(s.ctx)
|
||||
s.assert.NotNil(s.s3)
|
||||
s.assert.NotNil(s.bucket)
|
||||
s.assert.NotZero(projectID)
|
||||
s.assert.NotNil(id)
|
||||
|
||||
p, err := s.GetProject(projectID)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("unable to get project"), err)
|
||||
}
|
||||
|
||||
k := fmt.Sprintf("%s/%s", projectID, id)
|
||||
_, err = s.s3.DeleteObject(s.ctx, &s3.DeleteObjectInput{
|
||||
Key: &k,
|
||||
Bucket: &s.bucket,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Pages = slices.DeleteFunc(p.Pages, func(p ProjectPage) bool { return p.ID == id })
|
||||
|
||||
err = s.UpdateProject(projectID, p)
|
||||
return err
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"forge.capytal.company/capytalcode/project-comicverse/internals/randstr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
const projectIDLength = 6
|
||||
|
||||
var ErrProjectNotExists = errors.New("project does not exists in database")
|
||||
|
||||
func (s *Service) CreateProject() (Project, error) {
|
||||
s.assert.NotNil(s.db)
|
||||
s.assert.NotNil(s.s3)
|
||||
s.assert.NotNil(s.ctx)
|
||||
s.assert.NotZero(s.bucket)
|
||||
|
||||
s.log.Debug("Creating new project")
|
||||
|
||||
id, err := randstr.NewHex(projectIDLength)
|
||||
if err != nil {
|
||||
return Project{}, errors.Join(errors.New("creating hexadecimal ID returned error"), err)
|
||||
}
|
||||
|
||||
title := "New Project"
|
||||
|
||||
s.assert.NotZero(id, "ID should never be empty")
|
||||
|
||||
s.log.Debug("Creating project on database", slog.String("id", id))
|
||||
|
||||
_, err = s.db.CreateProject(id, title)
|
||||
if err != nil {
|
||||
return Project{}, err
|
||||
}
|
||||
|
||||
p := Project{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Pages: []ProjectPage{},
|
||||
}
|
||||
|
||||
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.json", id)
|
||||
_, err = s.s3.PutObject(s.ctx, &s3.PutObjectInput{
|
||||
Bucket: &s.bucket,
|
||||
Key: &f,
|
||||
Body: bytes.NewReader(c),
|
||||
})
|
||||
if err != nil {
|
||||
return Project{}, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetProject(id string) (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)
|
||||
|
||||
res, err := s.db.GetProject(id)
|
||||
// if errors.Is(err, database.ErrNoRows) {
|
||||
// return Project{}, errors.Join(ErrProjectNotExists, err)
|
||||
// }
|
||||
if err != nil {
|
||||
return Project{}, err
|
||||
}
|
||||
|
||||
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 Project{}, err
|
||||
}
|
||||
|
||||
c, err := io.ReadAll(file.Body)
|
||||
if err != nil {
|
||||
return Project{}, err
|
||||
}
|
||||
|
||||
var p Project
|
||||
err = json.Unmarshal(c, &p)
|
||||
|
||||
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) {
|
||||
s.assert.NotNil(s.db)
|
||||
|
||||
ps, err := s.db.ListProjects()
|
||||
if err != nil {
|
||||
return []Project{}, err
|
||||
}
|
||||
|
||||
p := make([]Project, len(ps))
|
||||
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)
|
||||
s.assert.NotZero(s.bucket)
|
||||
s.assert.NotNil(s.ctx)
|
||||
s.assert.NotZero(id)
|
||||
|
||||
p, err := s.GetProject(id)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("unable to get information of project"), err)
|
||||
}
|
||||
|
||||
err = s.db.DeleteProject(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.log.Debug("Deleting project on storage", slog.String("id", id))
|
||||
|
||||
files := []types.ObjectIdentifier{}
|
||||
|
||||
f := fmt.Sprintf("%s.comic.json", id)
|
||||
files = append(files, types.ObjectIdentifier{Key: &f})
|
||||
|
||||
for k := range p.Pages {
|
||||
f := fmt.Sprintf("%s/%s", id, k)
|
||||
files = append(files, types.ObjectIdentifier{Key: &f})
|
||||
}
|
||||
|
||||
_, err = s.s3.DeleteObjects(s.ctx, &s3.DeleteObjectsInput{
|
||||
Delete: &types.Delete{Objects: files},
|
||||
Bucket: &s.bucket,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user