diff --git a/.gitignore b/.gitignore index be5fd0a..06686ee 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,5 @@ hugov /dddplayer/ /public/ - +tmp release-notes.md diff --git a/ADRs/notes/mvp.md b/ADRs/notes/mvp.md index 07733d1..de4d060 100644 --- a/ADRs/notes/mvp.md +++ b/ADRs/notes/mvp.md @@ -105,3 +105,6 @@ curl -X POST "http://127.0.0.1:1314/api/deploy?type=Site&id=2" \ curl -X GET "http://127.0.0.1:1314/api/search?type=SiteDeployment&q=slug:site2" \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOm51bGwsImV4cCI6IjIwMjQtMTItMDRUMDg6MTQ6NTIuNTk2MDI5KzA4OjAwIiwiaWF0IjpudWxsLCJpc3MiOm51bGwsImp0aSI6bnVsbCwibmJmIjpudWxsLCJzdWIiOm51bGwsInVzZXIiOiJtZUBzdW53ZWkueHl6In0.foManZwcdG0h52dCxeKY6jE6iTkdSZFcEbnGFanLZU0" + +curl -X GET "http://127.0.0.1:1314/api/search2?type=Language" \ +-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOm51bGwsImV4cCI6IjIwMjUtMDEtMDZUMDc6MTk6MzcuNDI2ODU4KzA4OjAwIiwiaWF0IjpudWxsLCJpc3MiOm51bGwsImp0aSI6bnVsbCwibmJmIjpudWxsLCJzdWIiOm51bGwsInVzZXIiOiJtZUBzdW53ZWkueHl6In0.wVsqhQc2G1n2berEIovbitzxftYWLPdq7x6rSwgCZ5c" diff --git a/internal/application/deploy.go b/internal/application/deploy.go index 58c5e93..00938f5 100644 --- a/internal/application/deploy.go +++ b/internal/application/deploy.go @@ -25,10 +25,10 @@ type netlifyConfig struct { LogFormat string `default:"text"` } -func DeployToNetlify(target string, deployment *valueobject.SiteDeployment, token string) error { +func DeployToNetlify(target string, deployment *valueobject.Deployment, domain *valueobject.Domain, token string) error { c := &netlifyConfig{ AuthToken: token, - SiteID: deployment.NetlifySiteID, + SiteID: deployment.SiteID, Directory: path.Join(target, "public"), Draft: false, DeployMessage: "Deployed by MDFriday", @@ -55,7 +55,7 @@ func DeployToNetlify(target string, deployment *valueobject.SiteDeployment, toke client := setupNetlifyClient() ctx := setupContext(c, logger) - fmt.Println("Deploying to Netlify...", deployment.Netlify, deployment.Domain) + fmt.Println("Deploying to Netlify...", deployment.SiteName, domain.FullDomain()) // 检查 SiteID 是否为空 if c.SiteID == "" { @@ -63,8 +63,8 @@ func DeployToNetlify(target string, deployment *valueobject.SiteDeployment, toke newSite, err := client.CreateSite(ctx, &models.SiteSetup{ Site: models.Site{ //AccountSlug: "admin-zbpioce", - Name: deployment.Netlify, - CustomDomain: fmt.Sprintf("%s.app.mdfriday.com", deployment.Domain), + Name: deployment.SiteName, + CustomDomain: domain.FullDomain(), Ssl: true, }, SiteSetupAllOf1: models.SiteSetupAllOf1{}, @@ -76,7 +76,8 @@ func DeployToNetlify(target string, deployment *valueobject.SiteDeployment, toke // 更新 SiteID c.SiteID = newSite.ID - deployment.NetlifySiteID = newSite.ID + deployment.SiteID = newSite.ID + deployment.Status = "deploying" logger.Println("Created new site with ID: " + c.SiteID) } diff --git a/internal/domain/content/entity/content.go b/internal/domain/content/entity/content.go index db4ef48..e55c2cc 100644 --- a/internal/domain/content/entity/content.go +++ b/internal/domain/content/entity/content.go @@ -125,6 +125,11 @@ func (c *Content) UpdateContentObject(ci any) error { } status := cis.ItemStatus() + cih, ok := ci.(content.Hashable) + if ok { + cih.SetHash() + } + cii, ok := ci.(content.Identifiable) if ok { go func() { @@ -226,5 +231,5 @@ func (c *Content) Unmarshal(data []byte, content any) error { } func (c *Content) NormalizeString(s string) (string, error) { - return stringToSlug(s) + return valueobject.StringToSlug(s) } diff --git a/internal/domain/content/entity/creator.go b/internal/domain/content/entity/creator.go index b00b2d1..3eef1c7 100644 --- a/internal/domain/content/entity/creator.go +++ b/internal/domain/content/entity/creator.go @@ -54,7 +54,7 @@ func (c *Content) newContent(contentType string, ci any) (string, error) { return "", errors.New("content type does not implement Identifiable") } - slug, err := Slug(cii) + slug, err := valueobject.Slug(cii) if err != nil { return "", err } @@ -80,6 +80,12 @@ func (c *Content) newContent(contentType string, ci any) (string, error) { return "", errors.New("content type does not implement Statusable") } + cih, ok := ci.(content.Hashable) + if ok { + cih.SetHash() + fmt.Println("--==--", cih.ItemHash()) + } + b, err := c.Marshal(ci) if err != nil { return "", err diff --git a/internal/domain/content/entity/deploy.go b/internal/domain/content/entity/deploy.go index 6db7955..1576b75 100644 --- a/internal/domain/content/entity/deploy.go +++ b/internal/domain/content/entity/deploy.go @@ -2,59 +2,48 @@ package entity import ( "encoding/json" - "errors" "fmt" "github.com/gohugonet/hugoverse/internal/domain/content/valueobject" "time" ) -func (c *Content) GetDeployment(siteId string, domain *valueobject.Domain) (*valueobject.SiteDeployment, error) { - content, err := c.getContent("Site", siteId) +func (c *Content) GetDeployment(domain *valueobject.Domain, hostName string) (*valueobject.Deployment, error) { + sd, err := c.searchDeployment(domain.QueryString(), hostName) if err != nil { return nil, err } - if site, ok := content.(*valueobject.Site); ok { - sd, err := c.searchDeployment(domain.Root, domain.Sub, domain.Owner) + if sd == nil { + item, err := valueobject.NewItemWithNamespace("Deployment") if err != nil { return nil, err } - if sd == nil { - item, err := valueobject.NewItemWithNamespace("SiteDeployment") - if err != nil { - return nil, err - } - - sd = &valueobject.SiteDeployment{ - Item: *item, - Site: site.QueryString(), - Netlify: fmt.Sprintf("mdf-%d-%s", time.Now().UnixMilli(), site.ItemSlug()), - Domain: site.ItemSlug(), - Status: "Not Started", - } - - _, err = c.newContent("SiteDeployment", sd) - if err != nil { - return nil, err - } + sd = &valueobject.Deployment{ + Item: *item, + Domain: domain.QueryString(), + SiteName: fmt.Sprintf("mdf-%d", time.Now().UnixMilli()), + HostName: hostName, + Status: "pending", } - return sd, nil + _, err = c.newContent("Deployment", sd) + if err != nil { + return nil, err + } } - return nil, errors.New("only site could be deployed") + return sd, nil } -func (c *Content) searchDeployment(root string, sub string, owner string) (*valueobject.SiteDeployment, error) { +func (c *Content) searchDeployment(domainQueryStr string, hostName string) (*valueobject.Deployment, error) { conditions := map[string]string{ - "root": root, - "domain": sub, - "owner": owner, + "domain": domainQueryStr, + "host_name": hostName, } // 查询域名信息 - deploys, err := c.termSearch("SiteDeployment", conditions) + deploys, err := c.termSearch("Deployment", conditions) if err != nil { return nil, err } @@ -66,7 +55,7 @@ func (c *Content) searchDeployment(root string, sub string, owner string) (*valu // 遍历查询结果并解析 for _, data := range deploys { - var deployment valueobject.SiteDeployment + var deployment valueobject.Deployment if err := json.Unmarshal(data, &deployment); err != nil { return nil, err } diff --git a/internal/domain/content/entity/domain.go b/internal/domain/content/entity/domain.go index e8c9440..1f2065a 100644 --- a/internal/domain/content/entity/domain.go +++ b/internal/domain/content/entity/domain.go @@ -3,30 +3,38 @@ package entity import ( "encoding/json" "errors" + "fmt" "github.com/gohugonet/hugoverse/internal/domain/content/valueobject" ) -func (c *Content) ApplyDomain(siteId string, domain string) (*valueobject.Domain, error) { +func (c *Content) ApplyDomain(siteId string, domain string) (*valueobject.Domain, bool, error) { site, err := c.getContent("Site", siteId) if err != nil { - return nil, err + return nil, false, err } if site, ok := site.(*valueobject.Site); ok { - slug, err := Slug(site) + slug, err := valueobject.Slug(site) + fmt.Println("-------- slug", slug, domain) if err != nil { - return nil, err + return nil, false, err } - sd, err := c.searchDomain(domain, slug, site.Owner) + sd, err := c.searchDomain(domain, slug) if err != nil { - return nil, err + return nil, false, err + } + + fmt.Println("-------- sd", sd) + + if sd != nil && sd.Owner != site.Owner { + return nil, true, errors.New(fmt.Sprintf("domain %s already exists", sd.String())) } if sd == nil { item, err := valueobject.NewItemWithNamespace("Domain") if err != nil { - return nil, err + return nil, false, err } sd = &valueobject.Domain{ @@ -38,22 +46,20 @@ func (c *Content) ApplyDomain(siteId string, domain string) (*valueobject.Domain _, err = c.newContent("Domain", sd) if err != nil { - return nil, err + return nil, false, err } } - return sd, nil + return sd, false, nil } - return nil, errors.New("only site could be deployed with domain") + return nil, false, errors.New("only site could be deployed with domain") } -func (c *Content) searchDomain(root string, sub string, owner string) (*valueobject.Domain, error) { +func (c *Content) searchDomain(root string, sub string) (*valueobject.Domain, error) { // 构建精确匹配的查询条件 conditions := map[string]string{ - "root": root, - "sub": sub, - "owner": owner, + "hash": valueobject.Hash([]string{sub, root}), } // 查询域名信息 @@ -67,6 +73,8 @@ func (c *Content) searchDomain(root string, sub string, owner string) (*valueobj return nil, nil } + fmt.Println("-------- domains", len(domains)) + // 遍历查询结果并解析 for _, data := range domains { var domain valueobject.Domain diff --git a/internal/domain/content/entity/search.go b/internal/domain/content/entity/search.go index d25f922..26b2db5 100644 --- a/internal/domain/content/entity/search.go +++ b/internal/domain/content/entity/search.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/blevesearch/bleve" + "github.com/blevesearch/bleve/search/query" "github.com/gohugonet/hugoverse/internal/domain/content" "github.com/gohugonet/hugoverse/internal/domain/content/repository" "github.com/gohugonet/hugoverse/internal/domain/content/valueobject" @@ -192,20 +193,23 @@ func (s *Search) TermQuery(typeName string, keyValues map[string]string, count, return nil, content.ErrNoIndex } - s.Log.Debugln("KeyValueQuery KeyValues: ", keyValues) + fmt.Println("KeyValueQuery KeyValues: ", keyValues) - // 创建每个 Key-Value 的查询 - var termQueries []bleve.Query + var termQueries []query.Query for key, value := range keyValues { tq := bleve.NewTermQuery(value) tq.SetField(key) + fmt.Printf("TermQuery 22 : %+v", tq) + termQueries = append(termQueries, tq) } // 将查询组合成一个 ConjunctionQuery finalQuery := bleve.NewConjunctionQuery(termQueries...) + s.Log.Debugf("TermQuery: %+v", finalQuery) + // 创建搜索请求 req := bleve.NewSearchRequestOptions(finalQuery, count, offset, false) @@ -246,6 +250,12 @@ func (s *Search) UpdateIndex(ns, id string, data []byte) error { // add data to search index i := valueobject.NewIndex(ns, id) + + l, ok := p.(*valueobject.Language) + if ok { + fmt.Println("indexing ppp...:", l, l.Name, l.Code) + } + err = idx.Index(i.String(), p) return err diff --git a/internal/domain/content/factory/content.go b/internal/domain/content/factory/content.go index e456257..3caae4f 100644 --- a/internal/domain/content/factory/content.go +++ b/internal/domain/content/factory/content.go @@ -52,7 +52,7 @@ func prepareUserTypes(c *entity.Content) { 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) } + c.UserTypes["Deployment"] = func() interface{} { return new(valueobject.Deployment) } } func prepareAdminTypes(c *entity.Content) { diff --git a/internal/domain/content/type.go b/internal/domain/content/type.go index 85d35b3..f9fbdc4 100644 --- a/internal/domain/content/type.go +++ b/internal/domain/content/type.go @@ -121,6 +121,11 @@ type Sluggable interface { ItemSlug() string } +type Hashable interface { + SetHash() + ItemHash() string +} + // Searchable ... type Searchable interface { SearchMapping() (*mapping.IndexMappingImpl, error) diff --git a/internal/domain/content/valueobject/sitedeployment.go b/internal/domain/content/valueobject/deployment.go similarity index 66% rename from internal/domain/content/valueobject/sitedeployment.go rename to internal/domain/content/valueobject/deployment.go index 1169ffb..1036cdb 100644 --- a/internal/domain/content/valueobject/sitedeployment.go +++ b/internal/domain/content/valueobject/deployment.go @@ -8,62 +8,60 @@ import ( "strings" ) -// TODO: deployment belongs to admin -// need netlify sub domain, and site id -// need domain root, sub, and owner info -// no need site info - -type SiteDeployment struct { +type Deployment struct { Item - Site string `json:"site"` - Netlify string `json:"netlify"` - NetlifySiteID string `json:"netlify_site_id"` - Domain string `json:"domain"` - Status string `json:"status"` + Domain string `json:"domain"` + + SiteID string `json:"site_id"` + SiteName string `json:"site_name"` + SitePath string `json:"site_path"` + HostName string `json:"host_name"` + + Status string `json:"status"` refSelData map[string][][]byte } // MarshalEditor writes a buffer of html to edit a Song within the CMS // and implements editor.Editable -func (s *SiteDeployment) MarshalEditor() ([]byte, error) { +func (s *Deployment) MarshalEditor() ([]byte, error) { view, err := editor.Form(s, editor.Field{ - View: editor.RefSelect("Site", s, map[string]string{ - "label": "Site", + View: editor.RefSelect("Domain", s, map[string]string{ + "label": "Domain", }, - "Site", - `{{ .title }} `, - s.refSelData["Site"], + "Domain", + `{{ .sub }}.{{ .root }}`, + s.refSelData["Domain"], ), }, editor.Field{ - View: editor.Input("Netlify", s, map[string]string{ - "label": "Netlify", + View: editor.Input("SiteID", s, map[string]string{ + "label": "SiteID", "type": "text", - "placeholder": "Enter the Netlify site url here", + "placeholder": "Enter the site id here", }), }, editor.Field{ - View: editor.Input("NetlifySiteID", s, map[string]string{ - "label": "NetlifySiteID", + View: editor.Input("SiteName", s, map[string]string{ + "label": "SiteName", "type": "text", - "placeholder": "Enter the Netlify site id here", + "placeholder": "Enter the site name here", }), }, editor.Field{ - View: editor.Input("Domain", s, map[string]string{ - "label": "Domain", + View: editor.Input("SitePath", s, map[string]string{ + "label": "SitePath", "type": "text", - "placeholder": "Enter the Customized sub domain here", + "placeholder": "Enter the site path here", }), }, editor.Field{ - View: editor.Input("Root", s, map[string]string{ - "label": "Root", + View: editor.Input("HostName", s, map[string]string{ + "label": "HostName", "type": "text", - "placeholder": "Enter the Customized root domain here", + "placeholder": "Enter the host name here", }), }, editor.Field{ @@ -82,30 +80,29 @@ func (s *SiteDeployment) MarshalEditor() ([]byte, error) { return view, nil } -func (s *SiteDeployment) SetSelectData(data map[string][][]byte) { +func (s *Deployment) SetSelectData(data map[string][][]byte) { s.refSelData = data } -func (s *SiteDeployment) SelectContentTypes() []string { - return []string{"Site", "Post"} +func (s *Deployment) SelectContentTypes() []string { + return []string{"Domain"} } // String defines the display name of a Song in the CMS list-view -func (s *SiteDeployment) String() string { - t, _ := extractTypeAndID(s.Site) +func (s *Deployment) String() string { + d, _ := extractTypeAndID(s.Domain) - return strings.Join([]string{t, s.Domain}, " - ") + return strings.Join([]string{d, s.HostName}, " - ") } // 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 *SiteDeployment) Create(res http.ResponseWriter, req *http.Request) error { +func (s *Deployment) Create(res http.ResponseWriter, req *http.Request) error { // do form data validation for required fields required := []string{ - "site", - "netlify", "domain", + "host_name", } for _, r := range required { @@ -121,7 +118,7 @@ func (s *SiteDeployment) Create(res http.ResponseWriter, req *http.Request) erro // 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 *SiteDeployment) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error { +func (s *Deployment) 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 @@ -140,9 +137,9 @@ func (s *SiteDeployment) BeforeAPICreate(res http.ResponseWriter, req *http.Requ // 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 *SiteDeployment) AfterAPICreate(res http.ResponseWriter, req *http.Request) error { +func (s *Deployment) AfterAPICreate(res http.ResponseWriter, req *http.Request) error { addr := req.RemoteAddr - log.Println("AfterAPICreate: SitePost sent by:", addr, "titled:", req.PostFormValue("title")) + log.Println("AfterAPICreate: Deployment sent by:", addr, "titled:", req.PostFormValue("title")) return nil } @@ -152,7 +149,7 @@ func (s *SiteDeployment) AfterAPICreate(res http.ResponseWriter, req *http.Reque // 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 *SiteDeployment) Approve(res http.ResponseWriter, req *http.Request) error { +func (s *Deployment) Approve(res http.ResponseWriter, req *http.Request) error { return nil } @@ -168,7 +165,7 @@ func (s *SiteDeployment) Approve(res http.ResponseWriter, req *http.Request) err // 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 *SiteDeployment) AutoApprove(res http.ResponseWriter, req *http.Request) error { +func (s *Deployment) 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. @@ -179,6 +176,6 @@ func (s *SiteDeployment) AutoApprove(res http.ResponseWriter, req *http.Request) return nil } -func (s *SiteDeployment) IndexContent() bool { +func (s *Deployment) IndexContent() bool { return true } diff --git a/internal/domain/content/valueobject/domain.go b/internal/domain/content/valueobject/domain.go index 4eacbf4..b4572ae 100644 --- a/internal/domain/content/valueobject/domain.go +++ b/internal/domain/content/valueobject/domain.go @@ -18,6 +18,10 @@ func (d *Domain) Name() string { return fmt.Sprintf("%s.%s - %s", d.Sub, d.Root, d.Owner) } +func (d *Domain) FullDomain() string { + return fmt.Sprintf("%s.%s", d.Sub, d.Root) +} + // MarshalEditor writes a buffer of html to edit a Song within the CMS // and implements editor.Editable func (d *Domain) MarshalEditor() ([]byte, error) { @@ -55,6 +59,10 @@ func (d *Domain) MarshalEditor() ([]byte, error) { // String defines the display name of a Song in the CMS list-view func (d *Domain) String() string { return d.Name() } +func (d *Domain) SetHash() { + d.Hash = Hash([]string{d.Sub, d.Root}) +} + // 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/hash.go b/internal/domain/content/valueobject/hash.go new file mode 100644 index 0000000..2136ec3 --- /dev/null +++ b/internal/domain/content/valueobject/hash.go @@ -0,0 +1,17 @@ +package valueobject + +import ( + "crypto/sha256" + "encoding/hex" +) + +func Hash(fields []string) string { + data := "" + for _, field := range fields { + data += field + } + + hash := sha256.Sum256([]byte(data)) + + return hex.EncodeToString(hash[:]) +} diff --git a/internal/domain/content/valueobject/item.go b/internal/domain/content/valueobject/item.go index 5a6a071..a005a08 100644 --- a/internal/domain/content/valueobject/item.go +++ b/internal/domain/content/valueobject/item.go @@ -20,6 +20,7 @@ type Item struct { Namespace string `json:"namespace"` ID int `json:"id"` Slug string `json:"slug"` + Hash string `json:"hash"` Timestamp int64 `json:"timestamp"` Updated int64 `json:"updated"` } @@ -36,6 +37,7 @@ func NewItem() (*Item, error) { UUID: uid, ID: -1, Slug: "", + Hash: "", Timestamp: nowMillis, Updated: nowMillis, }, nil @@ -70,6 +72,11 @@ func (i *Item) ItemSlug() string { return i.Slug } +// ItemHash gets the item's hash +func (i *Item) ItemHash() string { + return i.Hash +} + // ItemID gets the *Item's ID field // partially implements the Identifiable interface func (i *Item) ItemID() int { diff --git a/internal/domain/content/valueobject/language.go b/internal/domain/content/valueobject/language.go index 1457b94..e561a4f 100644 --- a/internal/domain/content/valueobject/language.go +++ b/internal/domain/content/valueobject/language.go @@ -49,6 +49,10 @@ func (a *Language) String() string { return a.Code } +func (a *Language) SetHash() { + a.Hash = Hash([]string{a.Name, a.Code}) +} + func (a *Language) Create(res http.ResponseWriter, req *http.Request) error { // do form data validation for required fields required := []string{ diff --git a/internal/domain/content/entity/slug.go b/internal/domain/content/valueobject/slug.go similarity index 88% rename from internal/domain/content/entity/slug.go rename to internal/domain/content/valueobject/slug.go index 84d5eab..bed7286 100644 --- a/internal/domain/content/entity/slug.go +++ b/internal/domain/content/valueobject/slug.go @@ -1,4 +1,4 @@ -package entity +package valueobject import ( "github.com/gohugonet/hugoverse/internal/domain/content" @@ -11,7 +11,7 @@ func Slug(i content.Identifiable) (string, error) { name := strings.TrimSpace(i.String()) // filter out non-alphanumeric character or non-whitespace - slug, err := stringToSlug(name) + slug, err := StringToSlug(name) if err != nil { return "", err } diff --git a/internal/domain/content/entity/string.go b/internal/domain/content/valueobject/string.go similarity index 86% rename from internal/domain/content/entity/string.go rename to internal/domain/content/valueobject/string.go index 068142b..deec876 100644 --- a/internal/domain/content/entity/string.go +++ b/internal/domain/content/valueobject/string.go @@ -1,4 +1,4 @@ -package entity +package valueobject import ( "golang.org/x/text/transform" @@ -11,7 +11,7 @@ import ( var rxList map[*regexp.Regexp][]byte func init() { - // Compile regex once to use in stringToSlug(). + // Compile regex once to use in StringToSlug(). // We store the compiled regex as the key // and assign the replacement as the map's value. rxList = map[*regexp.Regexp][]byte{ @@ -26,8 +26,7 @@ func init() { } } -// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example -func stringToSlug(s string) (string, error) { +func StringToSlug(s string) (string, error) { src := []byte(strings.ToLower(s)) // Range over compiled regex and replacements from init(). diff --git a/internal/interfaces/api/handler/handledeploy.go b/internal/interfaces/api/handler/handledeploy.go index d5d3386..5452f98 100644 --- a/internal/interfaces/api/handler/handledeploy.go +++ b/internal/interfaces/api/handler/handledeploy.go @@ -2,7 +2,6 @@ package handler import ( "encoding/json" - "fmt" "github.com/gohugonet/hugoverse/internal/application" "github.com/gohugonet/hugoverse/internal/domain/content" "log" @@ -27,18 +26,24 @@ func (s *Handler) DeployContentHandler(res http.ResponseWriter, req *http.Reques return } - netlify := req.PostForm.Get("netlify") + hostName := req.PostForm.Get("host_name") + hostToken := req.PostForm.Get("host_token") root := req.PostForm.Get("domain") - if netlify == "" || root == "" { - netlify = s.adminApp.Netlify.Token() + if hostToken == "" || root == "" { + hostName = "Netlify" + hostToken = s.adminApp.Netlify.Token() root = "app.mdfriday.com" } - d, err := s.contentApp.ApplyDomain(id, root) - if err != nil { + d, isTaken, err := s.contentApp.ApplyDomain(id, root) + if !isTaken && err != nil { s.log.Errorf("Error applying domain: %v", err) res.WriteHeader(http.StatusInternalServerError) return + } else if isTaken { + s.log.Errorf("Domain already taken: %s", err.Error()) + res.WriteHeader(http.StatusConflict) + return } pt, ok := s.contentApp.GetContentCreator(t) @@ -69,14 +74,20 @@ func (s *Handler) DeployContentHandler(res http.ResponseWriter, req *http.Reques return } - sd, err := s.contentApp.GetDeployment(id, d) + sd, err := s.contentApp.GetDeployment(d, hostName) if err != nil { s.log.Errorf("Error getting deployment: %v", err) res.WriteHeader(http.StatusInternalServerError) return } - err = application.DeployToNetlify(t, sd, netlify) + if hostName != "Netlify" { + s.log.Errorf("Error: Netlify only supported for now") + res.WriteHeader(http.StatusInternalServerError) + return + } + s.log.Infof("Deploying site %s to Netlify with token %s", id, hostToken) + err = application.DeployToNetlify(t, sd, d, hostToken) if err != nil { s.log.Errorf("Error building: %v", err) res.WriteHeader(http.StatusInternalServerError) @@ -89,7 +100,7 @@ func (s *Handler) DeployContentHandler(res http.ResponseWriter, req *http.Reques return } - jsonBytes, err := json.Marshal(fmt.Sprintf("https://%s.app.mdfriday.com", sd.Domain)) + jsonBytes, err := json.Marshal(d.FullDomain()) if err != nil { s.log.Errorf("Error marshalling token: %v", err) return diff --git a/internal/interfaces/api/handler/handlesearch.go b/internal/interfaces/api/handler/handlesearch.go index 5fb0272..e032f10 100644 --- a/internal/interfaces/api/handler/handlesearch.go +++ b/internal/interfaces/api/handler/handlesearch.go @@ -2,16 +2,173 @@ package handler import ( "bytes" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" + "fmt" + "github.com/blevesearch/bleve" + "github.com/blevesearch/bleve/mapping" + "github.com/blevesearch/bleve/search/query" "github.com/gohugonet/hugoverse/internal/domain/content" + "github.com/gohugonet/hugoverse/internal/domain/content/valueobject" "github.com/gohugonet/hugoverse/pkg/editor" "net/http" "net/url" + "os" "strconv" "strings" ) +var indexMapping *mapping.IndexMappingImpl +var exampleIndex bleve.Index +var err error + +func ExampleNew() { + indexMapping = bleve.NewIndexMapping() + indexMapping.StoreDynamic = false + + //docMapping := bleve.NewDocumentMapping() + // + //keywordFieldMapping := bleve.NewTextFieldMapping() + //keywordFieldMapping.Analyzer = keyword.Name + // + //// 为不同字段设置映射 + //docMapping.AddFieldMappingsAt("Name", keywordFieldMapping) + //docMapping.AddFieldMappingsAt("Code", keywordFieldMapping) + // + //// 将文档映射添加到索引映射 + //indexMapping.AddDocumentMapping("document", docMapping) + + exampleIndex, err = bleve.New("/Users/sunwei/github/gohugonet/hugoverse/tmp", indexMapping) + if err != nil { + panic(err) + } + count, err := exampleIndex.DocCount() + if err != nil { + panic(err) + } + + fmt.Println(count) + // Output: + // 0 +} + +type dodo struct { + Name string + Code string + Hash string +} + +func (d *dodo) hash() { + // 拼接 Name 和 Code + data := d.Name + d.Code + // 使用 SHA-256 哈希函数 + hash := sha256.Sum256([]byte(data)) + // 返回哈希值 + d.Hash = hex.EncodeToString(hash[:]) +} + +func newDodo(name, code string) *dodo { + d := &dodo{Name: name, Code: code} + d.hash() + return d +} + +var data = newDodo("app.mdfriday.com", "Untitled Friday Site 299") +var data2 = newDodo("app.mdfriday.com", "Untitled Friday Site 199") + +func ExampleIndex_indexing() { + // index some data + err = exampleIndex.Index("document id 1", &data) + if err != nil { + panic(err) + } + err = exampleIndex.Index("document id 2", &data2) + if err != nil { + panic(err) + } + + // 2 documents have been indexed + count, err := exampleIndex.DocCount() + if err != nil { + panic(err) + } + + fmt.Println(count) + // Output: + // 2 +} + +func (s *Handler) SearchContentHandler2(res http.ResponseWriter, req *http.Request) { + err = os.RemoveAll("/Users/sunwei/github/gohugonet/hugoverse/tmp") + if err != nil { + panic(err) + } + + ExampleNew() + ExampleIndex_indexing() + + fmt.Println("hash: ", data2.Hash) + var keyValues = map[string]string{"Hash": data2.Hash} + + var termQueries []query.Query + for key, value := range keyValues { + tq := bleve.NewTermQuery(value) + tq.SetField(key) + + termQueries = append(termQueries, tq) + } + + // 将查询组合成一个 ConjunctionQuery + finalQuery := bleve.NewConjunctionQuery(termQueries...) + q := bleve.NewSearchRequestOptions(finalQuery, 10, 0, false) + + indices, err := exampleIndex.Search(q) + if err != nil { + panic(err) + } + fmt.Println("Search Results for conjunction:", len(indices.Hits)) + for _, index := range indices.Hits { + fmt.Println(index) + } + + //// Perform search for "en99" + //query := bleve.NewTermQuery("en99") + //query.SetField("Code") + //searchRequest := bleve.NewSearchRequest(query) + //searchResults, err := exampleIndex.Search(searchRequest) + //if err != nil { + // panic(err) + //} + //fmt.Println("Search Results for en99:", len(searchResults.Hits)) // Expected output: 1 + // + //// Perform search for "en-100" + //query2 := bleve.NewMatchQuery("Untitled Friday Site 199") + //query2.SetField("Code") + //searchRequest2 := bleve.NewSearchRequest(query2) + //searchResults2, err := exampleIndex.Search(searchRequest2) + //if err != nil { + // panic(err) + //} + //fmt.Println("Search Results for en-100:", len(searchResults2.Hits)) + + qr, err := s.contentApp.Search.TermQuery("Language", + map[string]string{"hash": valueobject.Hash([]string{"English888", "en888"})}, 10, 0) + if errors.Is(err, content.ErrNoIndex) { + res.WriteHeader(http.StatusNotFound) + return + } + + for _, index := range qr { + fmt.Println(index.ID(), index.ContentType()) + } + + fmt.Println("???---???", len(qr)) + + res.WriteHeader(http.StatusOK) +} + func (s *Handler) SearchContentHandler(res http.ResponseWriter, req *http.Request) { qs := req.URL.Query() t := qs.Get("type") diff --git a/internal/interfaces/api/handlers.go b/internal/interfaces/api/handlers.go index ed498fc..6627d9c 100644 --- a/internal/interfaces/api/handlers.go +++ b/internal/interfaces/api/handlers.go @@ -11,6 +11,7 @@ func (s *Server) registerContentHandler() { s.mux.HandleFunc("/api/content", s.wrapContentHandler(s.content.Handle(s.handler.ContentHandler))) s.mux.HandleFunc("/api/search", s.wrapContentHandler(s.handler.SearchContentHandler)) + s.mux.HandleFunc("/api/search2", s.wrapContentHandler(s.handler.SearchContentHandler2)) s.mux.HandleFunc("/api/preview", s.wrapContentHandler(s.handler.PreviewContentHandler)) s.mux.HandleFunc("/api/build", s.wrapContentHandler(s.handler.BuildContentHandler))