diff --git a/internal/domain/content/entity/content.go b/internal/domain/content/entity/content.go index 76ddd1b..0c0a300 100644 --- a/internal/domain/content/entity/content.go +++ b/internal/domain/content/entity/content.go @@ -47,6 +47,15 @@ func (c *Content) GetContent(contentType, id, status string) ([]byte, error) { return c.Repo.GetContent(GetNamespace(contentType, status), id) } +func (c *Content) GetContentByHash(contentType, hash, status string) ([]byte, error) { + idb, err := c.Repo.GetIdByHash(GetNamespace(contentType, status), hash) + if err != nil { + return nil, err + } + id := string(idb) + return c.GetContent(contentType, id, status) +} + func (c *Content) DeleteContent(contentType, id, status string) error { data, err := c.GetContent(contentType, id, status) if err != nil { @@ -62,10 +71,12 @@ func (c *Content) DeleteContent(contentType, id, status string) error { } ns := GetNamespace(contentType, status) - if err := c.Repo.DeleteContent( - ns, - id, - cti.(content.Sluggable).ItemSlug()); err != nil { + + hash := "" + if ctiHash, ok := cti.(content.Hashable); ok { + hash = ctiHash.ItemHash() + } + if err := c.Repo.DeleteContent(ns, id, cti.(content.Sluggable).ItemSlug(), hash); err != nil { return err } diff --git a/internal/domain/content/repository/repo.go b/internal/domain/content/repository/repo.go index 60b59ae..d96f2d5 100644 --- a/internal/domain/content/repository/repo.go +++ b/internal/domain/content/repository/repo.go @@ -7,10 +7,11 @@ type Repository interface { AllContent(namespace string) [][]byte ContentByPrefix(namespace, prefix string) ([][]byte, error) GetContent(namespace string, id string) ([]byte, error) - DeleteContent(namespace string, id string, slug string) error + DeleteContent(namespace string, id string, slug string, hash string) error NextContentId(ns string) (uint64, error) CheckSlugForDuplicate(namespace string, slug string) (string, error) + GetIdByHash(namespace string, hash string) ([]byte, error) PutSortedContent(namespace string, m map[string][]byte) error diff --git a/internal/domain/content/valueobject/post.go b/internal/domain/content/valueobject/post.go index ac0e058..5cabaed 100644 --- a/internal/domain/content/valueobject/post.go +++ b/internal/domain/content/valueobject/post.go @@ -68,6 +68,10 @@ func (s *Post) MarshalEditor() ([]byte, error) { // String defines the display name of a Song in the CMS list-view func (s *Post) String() string { return s.Title } +func (s *Post) SetHash() { + s.Hash = Hash([]string{s.Content}) +} + // Create implements api.Createable, and allows external POST requests from clients // to add content as long as the request contains the json tag names of the Song // struct fields, and is multipart encoded diff --git a/internal/domain/content/valueobject/resource.go b/internal/domain/content/valueobject/resource.go index f9fa8bf..9b93061 100644 --- a/internal/domain/content/valueobject/resource.go +++ b/internal/domain/content/valueobject/resource.go @@ -11,6 +11,7 @@ type Resource struct { Name string `json:"name"` Asset string `json:"asset"` + Size string `json:"size"` } // MarshalEditor writes a buffer of html to edit a Song within the CMS @@ -30,6 +31,12 @@ func (s *Resource) MarshalEditor() ([]byte, error) { "placeholder": "Upload the asset here", }), }, + editor.Field{ + View: editor.File("Size", s, map[string]string{ + "label": "Size", + "placeholder": "Upload the size here", + }), + }, ) if err != nil { @@ -42,6 +49,10 @@ func (s *Resource) MarshalEditor() ([]byte, error) { // String defines the display name of a Song in the CMS list-view func (s *Resource) String() string { return s.Name } +func (s *Resource) SetHash() { + s.Hash = Hash([]string{s.Name, s.Size}) +} + // Create implements api.Createable, and allows external POST requests from clients // to add content as long as the request contains the json tag names of the Song // struct fields, and is multipart encoded diff --git a/internal/interfaces/api/database/db.go b/internal/interfaces/api/database/db.go index 87b6117..00cae35 100644 --- a/internal/interfaces/api/database/db.go +++ b/internal/interfaces/api/database/db.go @@ -98,7 +98,7 @@ func (d *Database) GetContent(namespace string, id string) ([]byte, error) { }) } -func (d *Database) DeleteContent(namespace string, id string, slug string) error { +func (d *Database) DeleteContent(namespace string, id string, slug string, hash string) error { if err := d.getStore(namespace).Delete(&item{bucket: namespace, key: id}); err != nil { return err } @@ -111,6 +111,12 @@ func (d *Database) DeleteContent(namespace string, id string, slug string) error return err } + if hash != "" { + if err := d.getStore(namespace).RemoveIndex(fmt.Sprintf("%s:%s", namespace, hash)); err != nil { + return err + } + } + return nil } @@ -176,6 +182,12 @@ func (d *Database) NewContent(ci any, data []byte) error { if err := d.getStore(ns).SetIndex(newKeyValueItem(ciSlug.ItemSlug(), fmt.Sprintf("%s:%d", ns, id))); err != nil { return err } + ciHash, ok := ci.(content.Hashable) + if ok { + if err := d.getStore(ns).SetIndex(newKeyValueItem(fmt.Sprintf("%s:%s", ns, ciHash.ItemHash()), fmt.Sprintf("%d", id))); err != nil { + return err + } + } return nil } @@ -241,6 +253,10 @@ func (d *Database) CheckSlugForDuplicate(namespace string, slug string) (string, return d.getStore(namespace).CheckSlugForDuplicate(slug) } +func (d *Database) GetIdByHash(namespace string, hash string) ([]byte, error) { + return d.getStore(namespace).GetIndex(fmt.Sprintf("%s:%s", namespace, hash)) +} + func (d *Database) Query(namespace string, opts db.QueryOptions) (int, [][]byte) { return d.getStore(namespace).Query(namespace, opts) } diff --git a/internal/interfaces/api/handler/handlecontent.go b/internal/interfaces/api/handler/handlecontent.go index a006a28..166aeee 100644 --- a/internal/interfaces/api/handler/handlecontent.go +++ b/internal/interfaces/api/handler/handlecontent.go @@ -224,6 +224,35 @@ func (s *Handler) postContent(res http.ResponseWriter, req *http.Request) { post := p() + cid := req.PostForm.Get("id") + isUpdating := cid != "-1" + isCreating := !isUpdating + + if isUpdating { + ep := p() + data, err := s.contentApp.GetContent(t, cid, "") + if err != nil { + s.log.Errorf("Error getting content: %v with id %s", err, cid) + res.WriteHeader(http.StatusNotFound) + } + err = json.Unmarshal(data, ep) + if err != nil { + if err := s.res.err500(res); err != nil { + s.log.Errorf("Error response err 500: %s", err) + } + return + } + if sort, ok := ep.(content.Sortable); ok { + req.PostForm.Set("timestamp", timestamp.TimeToString(sort.Time())) + } + if identifier, ok := ep.(content.Identifiable); ok { + req.PostForm.Set("uuid", identifier.UniqueID().String()) + } + if slug, ok := ep.(content.Sluggable); ok { + req.PostForm.Set("slug", slug.ItemSlug()) + } + } + ext, ok := post.(content.Createable) if !ok { s.log.Printf("Attempt to create non-createable type: %s from %s", t, req.RemoteAddr) @@ -232,7 +261,9 @@ func (s *Handler) postContent(res http.ResponseWriter, req *http.Request) { } ts := timestamp.Now() - req.PostForm.Set("timestamp", ts) + if isCreating { + req.PostForm.Set("timestamp", ts) + } req.PostForm.Set("updated", ts) urlPaths, err := s.StoreFiles(req) @@ -310,15 +341,24 @@ func (s *Handler) postContent(res http.ResponseWriter, req *http.Request) { req.PostForm.Set("namespace", t) - id, err := s.contentApp.NewContent(t, req.PostForm) - if err != nil { - s.log.Errorf("Error calling SetContent: %v", err) - res.WriteHeader(http.StatusInternalServerError) - return + if isCreating { + id, err := s.contentApp.NewContent(t, req.PostForm) + if err != nil { + s.log.Errorf("Error calling SetContent: %v", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + cid = id + } else { + if err = s.contentApp.UpdateContent(t, req.PostForm); err != nil { + s.log.Errorf("Error updating content: %s", err) + res.WriteHeader(http.StatusInternalServerError) + return + } } // set the target in the context so user can get saved value from db in hook - ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%s", t, id)) + ctx := context.WithValue(req.Context(), "target", fmt.Sprintf("%s:%s", t, cid)) req = req.WithContext(ctx) err = hook.AfterSave(res, req) @@ -344,7 +384,7 @@ func (s *Handler) postContent(res http.ResponseWriter, req *http.Request) { } else { spec = "public" data = map[string]interface{}{ - "id": id, + "id": cid, "status": spec, "type": t, } @@ -370,3 +410,106 @@ func (s *Handler) postContent(res http.ResponseWriter, req *http.Request) { return } } + +func (s *Handler) DeleteContentHandler(res http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + res.WriteHeader(http.StatusMethodNotAllowed) + return + } + + id := req.PostForm.Get("id") + t := req.PostForm.Get("type") + status := req.PostForm.Get("status") + ct := t + + if id == "" || t == "" { + res.WriteHeader(http.StatusBadRequest) + return + } + + p, ok := s.contentApp.AllContentTypes()[ct] + if !ok { + s.log.Printf("Type %s not supported", t) + res.WriteHeader(http.StatusBadRequest) + return + } + + post := p() + hook, ok := post.(content.Hookable) + if !ok { + s.log.Printf("Type %s does not implement item.Hookable or embed item.Item.", t) + res.WriteHeader(http.StatusBadRequest) + return + } + + data, err := s.contentApp.GetContent(t, id, status) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + s.log.Printf("Error in db.Content %s:%s: %s", t, id, err) + return + } + if data == nil { + res.WriteHeader(http.StatusNotFound) + s.log.Printf("Content not found: %s %s %s", t, id, status) + return + } + + err = json.Unmarshal(data, post) + if err != nil { + log.Println("Error unmarshalling ", t, "=", id, err, " Hooks will be called on a zero-value.") + } + + reject := req.URL.Query().Get("reject") + if reject == "true" { + err = hook.BeforeReject(res, req) + if err != nil { + log.Println("Error running BeforeReject method in deleteHandler for:", t, err) + return + } + } + + err = hook.BeforeAdminDelete(res, req) + if err != nil { + log.Println("Error running BeforeAdminDelete method in deleteHandler for:", t, err) + return + } + + err = hook.BeforeDelete(res, req) + if err != nil { + log.Println("Error running BeforeDelete method in deleteHandler for:", t, err) + return + } + + err = s.contentApp.DeleteContent(t, id, status) + if err != nil { + s.log.Errorf("Error in db.Content %s:%s: %s", t, id, err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + if err := s.adminApp.InvalidateCache(); err != nil { + s.log.Errorf("Error invalidating cache: %s", err) + } + + err = hook.AfterDelete(res, req) + if err != nil { + s.log.Errorln("Error running AfterDelete method in deleteHandler for:", t, err) + return + } + + err = hook.AfterAdminDelete(res, req) + if err != nil { + s.log.Errorln("Error running AfterAdminDelete method in deleteHandler for:", t, err) + return + } + + if reject == "true" { + err = hook.AfterReject(res, req) + if err != nil { + s.log.Errorln("Error running AfterReject method in deleteHandler for:", t, err) + return + } + } + + res.WriteHeader(http.StatusOK) +} diff --git a/internal/interfaces/api/handler/handlehash.go b/internal/interfaces/api/handler/handlehash.go new file mode 100644 index 0000000..a1204f7 --- /dev/null +++ b/internal/interfaces/api/handler/handlehash.go @@ -0,0 +1,81 @@ +package handler + +import ( + "encoding/json" + "github.com/gohugonet/hugoverse/internal/domain/content" + "net/http" +) + +func (s *Handler) HashHandler(res http.ResponseWriter, req *http.Request) { + s.getContentByHash(res, req) +} + +func (s *Handler) getContentByHash(res http.ResponseWriter, req *http.Request) { + q := req.URL.Query() + t := q.Get("type") + status := q.Get("status") + hash := q.Get("hash") + + if t == "" || hash == "" { + res.WriteHeader(http.StatusBadRequest) + return + } + + pt, ok := s.contentApp.GetContentCreator(t) + if !ok { + res.WriteHeader(http.StatusNotFound) + return + } + p := pt() + + _, ok = p.(content.Hashable) + if !ok { + res.WriteHeader(http.StatusUnprocessableEntity) + return + } + + post, err := s.contentApp.GetContentByHash(t, hash, status) + if err != nil { + s.log.Errorf("Error getting content by hash %s: %v", hash, err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + if post == nil { + res.WriteHeader(http.StatusNotFound) + s.log.Debugf("Content not found: %s %s %s", t, hash, status) + return + } + + err = json.Unmarshal(post, p) + if err != nil { + s.log.Errorf("Error unmarshalling content: %v", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "id": p.(content.Identifier).ID(), + "type": t, + } + + resp := map[string]interface{}{ + "data": []map[string]interface{}{ + data, + }, + } + + j, err := json.Marshal(resp) + if err != nil { + s.log.Errorf("Error marshalling response to JSON: %v", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + res.Header().Set("Content-Type", "application/json") + _, err = res.Write(j) + if err != nil { + s.log.Errorf("Error writing response: %v", err) + return + } +} diff --git a/internal/interfaces/api/handlers.go b/internal/interfaces/api/handlers.go index 6627d9c..0434610 100644 --- a/internal/interfaces/api/handlers.go +++ b/internal/interfaces/api/handlers.go @@ -8,7 +8,12 @@ import ( func (s *Server) registerContentHandler() { s.mux.HandleFunc("/api/contents", s.wrapContentHandler(s.handler.ApiContentsHandler)) - s.mux.HandleFunc("/api/content", s.wrapContentHandler(s.content.Handle(s.handler.ContentHandler))) + s.mux.HandleFunc("/api/content", s.wrapContentHandler( + s.content.Handle(s.handler.ContentHandler))) + s.mux.HandleFunc("/api/content/delete", s.wrapContentHandler( + s.content.Handle(s.handler.DeleteContentHandler))) + + s.mux.HandleFunc("/api/hash", s.wrapContentHandler(s.handler.HashHandler)) s.mux.HandleFunc("/api/search", s.wrapContentHandler(s.handler.SearchContentHandler)) s.mux.HandleFunc("/api/search2", s.wrapContentHandler(s.handler.SearchContentHandler2)) diff --git a/internal/interfaces/cli/vercurr.go b/internal/interfaces/cli/vercurr.go index 34a893b..3395c3c 100644 --- a/internal/interfaces/cli/vercurr.go +++ b/internal/interfaces/cli/vercurr.go @@ -3,6 +3,6 @@ package cli var CurrentVersion = Version{ Major: 0, Minor: 1, - PatchLevel: 9, + PatchLevel: 10, Suffix: "", } diff --git a/manifest.json b/manifest.json index 3468b41..6e407d0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1.9", + "version": "0.1.10", "name": "Hugoverse", "description": "Headless CMS for Hugo", "author": "sunwei", diff --git a/pkg/db/index.go b/pkg/db/index.go index a2c2a8b..8c45856 100644 --- a/pkg/db/index.go +++ b/pkg/db/index.go @@ -56,6 +56,22 @@ func (s *Store) CheckSlugForDuplicate(slug string) (string, error) { return slug, nil } +func (s *Store) GetIndex(key string) ([]byte, error) { + var value []byte + err := s.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__contentIndex")) + if b == nil { + return bolt.ErrBucketNotFound + } + value = b.Get([]byte(key)) + return nil + }) + if err != nil { + return nil, err + } + return value, nil +} + func (s *Store) SetIndex(item KeyValue) error { var err error err = s.db.Update(func(tx *bolt.Tx) error { diff --git a/pkg/timestamp/now.go b/pkg/timestamp/now.go index 0b0ad93..83ff51e 100644 --- a/pkg/timestamp/now.go +++ b/pkg/timestamp/now.go @@ -7,7 +7,11 @@ import ( ) func Now() string { - return fmt.Sprintf("%d", CurrentTimeMillis()) + return TimeToString(CurrentTimeMillis()) +} + +func TimeToString(time int64) string { + return fmt.Sprintf("%d", time) } func CurrentTimeMillis() int64 {