Skip to content

Commit

Permalink
add site resource
Browse files Browse the repository at this point in the history
  • Loading branch information
sunwei committed Nov 24, 2024
1 parent 6f26cee commit 87ef888
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 2 deletions.
53 changes: 53 additions & 0 deletions internal/domain/content/entity/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ func (c *Content) BuildTarget(contentType, id, status string) (string, error) {
return "", err
}

if err := c.writeSiteResource(site.ID, dir); err != nil {
c.Log.Errorf("failed to write site resources: %v", err)
writer.close()
return "", err
}

writer.close()

err = <-writer.errs
Expand Down Expand Up @@ -107,6 +113,34 @@ func (c *Content) writeSitePosts(siteId int, dir string, writerFiles chan *value
return nil
}

func (c *Content) writeSiteResource(siteId int, dir string) error {
q := fmt.Sprintf(`site%d`, siteId)
encodedQ := url.QueryEscape(q)

siteResources, err := c.search("SiteResource", fmt.Sprintf("slug:%s", encodedQ))
if err != nil {
return err
}

for _, data := range siteResources {
var sr valueobject.SiteResource
if err := json.Unmarshal(data, &sr); err != nil {
return err
}

res, err := c.getResource(sr.Resource)
if err != nil {
return err
}

if res.Asset != "" {
go c.copyFiles(dir, getParentPath(sr.Path), []string{res.Asset})
}
}

return nil
}

func getParentPath(fullPath string) string {
// 获取父目录路径
dirPath := filepath.Dir(fullPath)
Expand Down Expand Up @@ -205,6 +239,25 @@ func (c *Content) getPost(rawURL string) (*valueobject.Post, error) {
return post, nil
}

func (c *Content) getResource(rawURL string) (*valueobject.Resource, error) {
id, err := c.getIDByURL(rawURL)
if err != nil {
return nil, err
}

p, err := c.getContent("Resource", id)
if err != nil {
return nil, err
}

post, ok := p.(*valueobject.Resource)
if !ok {
return nil, errors.New("invalid post")
}

return post, nil
}

func (c *Content) getIDByURL(rawURL string) (string, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions internal/domain/content/entity/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ func (c *Content) PreviewTarget(contentType, id, status string) (string, string,
return "", "", err
}

if err := c.writeSiteResource(site.ID, dir); err != nil {
c.Log.Errorf("failed to write site resources: %v", err)
writer.close()
return "", "", err
}

writer.close()

err = <-writer.errs
Expand Down
2 changes: 2 additions & 0 deletions internal/domain/content/factory/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ func prepareUserTypes(c *entity.Content) {
c.UserTypes["Language"] = func() interface{} { return new(valueobject.Language) }
c.UserTypes["Theme"] = func() interface{} { return new(valueobject.Theme) }
c.UserTypes["Post"] = func() interface{} { return new(valueobject.Post) }
c.UserTypes["Resource"] = func() interface{} { return new(valueobject.Resource) }
c.UserTypes["Site"] = func() interface{} { return new(valueobject.Site) }
c.UserTypes["SiteLanguage"] = func() interface{} { return new(valueobject.SiteLanguage) }
c.UserTypes["SitePost"] = func() interface{} { return new(valueobject.SitePost) }
c.UserTypes["SiteResource"] = func() interface{} { return new(valueobject.SiteResource) }
c.UserTypes["SiteDeployment"] = func() interface{} { return new(valueobject.SiteDeployment) }
}

Expand Down
125 changes: 125 additions & 0 deletions internal/domain/content/valueobject/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package valueobject

import (
"fmt"
"github.com/gohugonet/hugoverse/pkg/editor"
"net/http"
)

type Resource struct {
Item

Name string `json:"name"`
Asset string `json:"asset"`
}

// MarshalEditor writes a buffer of html to edit a Song within the CMS
// and implements editor.Editable
func (s *Resource) MarshalEditor() ([]byte, error) {
view, err := editor.Form(s,
editor.Field{
View: editor.Input("Name", s, map[string]string{
"label": "Name",
"type": "text",
"placeholder": "Enter the name here",
}),
},
editor.Field{
View: editor.File("Asset", s, map[string]string{
"label": "Asset",
"placeholder": "Upload the asset here",
}),
},
)

if err != nil {
return nil, fmt.Errorf("failed to render Resource editor view: %s", err.Error())
}

return view, nil
}

// String defines the display name of a Song in the CMS list-view
func (s *Resource) String() string { return s.Name }

// 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
func (s *Resource) Create(res http.ResponseWriter, req *http.Request) error {
// do form data validation for required fields
required := []string{
"name",
"asset",
}

for _, r := range required {
if req.PostFormValue(r) == "" {
err := fmt.Errorf("request missing required field: %s", r)
return err
}
}

return nil
}

// BeforeAPICreate is only called if the Song type implements api.Createable
// It is called before Create, and returning an error will cancel the request
// causing the system to reject the data sent in the POST
func (s *Resource) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error {
// do initial user authentication here on the request, checking for a
// token or cookie, or that certain form fields are set and valid

// for example, this will check if the request was made by a CMS admin user:
//if !user.IsValid(req) {
// return api.ErrNoAuth
//}

// you could then to data validation on the request post form, or do it in
// the Create method, which is called after BeforeAPICreate

return nil
}

// AfterAPICreate is called after Create, and is useful for logging or triggering
// notifications, etc. after the data is saved to the database, etc.
// The request has a context containing the databse 'target' affected by the
// request. Ex. Song__pending:3 or Song:8 depending if Song implements api.Trustable
func (s *Resource) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
return nil
}

// Approve implements editor.Mergeable, which enables content supplied by external
// clients to be approved and thus added to the public content API. Before content
// is approved, it is waiting in the Pending bucket, and can only be approved in
// the CMS if the Mergeable interface is satisfied. If not, you will not see this
// content show up in the CMS.
func (s *Resource) Approve(res http.ResponseWriter, req *http.Request) error {
return nil
}

/*
NOTICE: if AutoApprove (seen below) is implemented, the Approve method above will have no
effect, except to add the Public / Pending toggle in the CMS UI. Though, no
Song content would be in Pending, since all externally submitting Song data
is immediately approved.
*/

// AutoApprove implements api.Trustable, and will automatically approve content
// that has been submitted by an external client via api.Createable. Be careful
// when using AutoApprove, because content will immediately be available through
// your public content API. If the Trustable interface is satisfied, the AfterApprove
// method is bypassed. The
func (s *Resource) AutoApprove(res http.ResponseWriter, req *http.Request) error {
// Use AutoApprove to check for trust-specific headers or whitelisted IPs,
// etc. Remember, you will not be able to Approve or Reject content that
// is auto-approved. You could add a field to Song, i.e.
// AutoApproved bool `json:auto_approved`
// and set that data here, as it is called before the content is saved, but
// after the BeforeSave hook.

return nil
}

func (s *Resource) IndexContent() bool {
return true
}
155 changes: 155 additions & 0 deletions internal/domain/content/valueobject/siteresource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package valueobject

import (
"fmt"
"github.com/gohugonet/hugoverse/pkg/editor"
"net/http"
"strings"
)

type SiteResource struct {
Item

Site string `json:"site"`
Resource string `json:"resource"`
Path string `json:"path"`

refSelData map[string][][]byte
}

// MarshalEditor writes a buffer of html to edit a Song within the CMS
// and implements editor.Editable
func (s *SiteResource) MarshalEditor() ([]byte, error) {
view, err := editor.Form(s,
editor.Field{
View: editor.RefSelect("Site", s, map[string]string{
"label": "Site",
},
"Site",
`{{ .title }} `,
s.refSelData["Site"],
),
},
editor.Field{
View: editor.RefSelect("Resource", s, map[string]string{
"label": "Post",
},
"Resource",
`{{ .name }} `,
s.refSelData["Resource"],
),
},
editor.Field{
View: editor.Input("Path", s, map[string]string{
"label": "Path",
"type": "text",
"placeholder": "Enter the relative path in content here",
}),
},
)

if err != nil {
return nil, fmt.Errorf("failed to render SiteResource editor view: %s", err.Error())
}

return view, nil
}

func (s *SiteResource) SetSelectData(data map[string][][]byte) {
s.refSelData = data
}

func (s *SiteResource) SelectContentTypes() []string {
return []string{"Site", "Resource"}
}

// String defines the display name of a Song in the CMS list-view
func (s *SiteResource) String() string {
t, _ := extractTypeAndID(s.Site)
l, _ := extractTypeAndID(s.Resource)

return strings.Join([]string{t, l}, " - ")
}

// 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
func (s *SiteResource) Create(res http.ResponseWriter, req *http.Request) error {
// do form data validation for required fields
required := []string{
"site",
"resource",
"path",
}

for _, r := range required {
if req.PostFormValue(r) == "" {
err := fmt.Errorf("request missing required field: %s", r)
return err
}
}

return nil
}

// BeforeAPICreate is only called if the Song type implements api.Createable
// It is called before Create, and returning an error will cancel the request
// causing the system to reject the data sent in the POST
func (s *SiteResource) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error {
// do initial user authentication here on the request, checking for a
// token or cookie, or that certain form fields are set and valid

// for example, this will check if the request was made by a CMS admin user:
//if !user.IsValid(req) {
// return api.ErrNoAuth
//}

// you could then to data validation on the request post form, or do it in
// the Create method, which is called after BeforeAPICreate

return nil
}

// AfterAPICreate is called after Create, and is useful for logging or triggering
// notifications, etc. after the data is saved to the database, etc.
// The request has a context containing the databse 'target' affected by the
// request. Ex. Song__pending:3 or Song:8 depending if Song implements api.Trustable
func (s *SiteResource) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
return nil
}

// Approve implements editor.Mergeable, which enables content supplied by external
// clients to be approved and thus added to the public content API. Before content
// is approved, it is waiting in the Pending bucket, and can only be approved in
// the CMS if the Mergeable interface is satisfied. If not, you will not see this
// content show up in the CMS.
func (s *SiteResource) Approve(res http.ResponseWriter, req *http.Request) error {
return nil
}

/*
NOTICE: if AutoApprove (seen below) is implemented, the Approve method above will have no
effect, except to add the Public / Pending toggle in the CMS UI. Though, no
Song content would be in Pending, since all externally submitting Song data
is immediately approved.
*/

// AutoApprove implements api.Trustable, and will automatically approve content
// that has been submitted by an external client via api.Createable. Be careful
// when using AutoApprove, because content will immediately be available through
// your public content API. If the Trustable interface is satisfied, the AfterApprove
// method is bypassed. The
func (s *SiteResource) AutoApprove(res http.ResponseWriter, req *http.Request) error {
// Use AutoApprove to check for trust-specific headers or whitelisted IPs,
// etc. Remember, you will not be able to Approve or Reject content that
// is auto-approved. You could add a field to Song, i.e.
// AutoApproved bool `json:auto_approved`
// and set that data here, as it is called before the content is saved, but
// after the BeforeSave hook.

return nil
}

func (s *SiteResource) IndexContent() bool {
return true
}
2 changes: 1 addition & 1 deletion internal/interfaces/cli/vercurr.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package cli
var CurrentVersion = Version{
Major: 0,
Minor: 0,
PatchLevel: 12,
PatchLevel: 13,
Suffix: "",
}
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.0.12",
"version": "0.0.13",
"name": "Hugoverse",
"description": "Headless CMS for Hugo",
"author": "sunwei",
Expand Down

0 comments on commit 87ef888

Please sign in to comment.