package plg_backend_perkeep import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "strings" . "github.com/mickael-kerjean/filestash/server/common" ) func init() { Backend.Register("perkeep", &Perkeep{}) } type Perkeep struct { serverURL string } func (this Perkeep) Init(params map[string]string, app *App) (IBackend, error) { url := params["url"] if url == "" { url = "http://localhost:3179/" } if !strings.HasSuffix(url, "/") { url += "/" } return &Perkeep{ serverURL: url, }, nil } func (this Perkeep) Meta(path string) Metadata { return Metadata{ CanCreateFile: NewBool(false), // TODO CanCreateDirectory: NewBool(false), // TODO CanRename: NewBool(false), // TODO CanMove: NewBool(false), // TODO CanUpload: NewBool(false), // TODO: see http://localhost:3179/bs-and-maybe-also-index/camli/upload CanDelete: NewBool(false), // TODO } } func (this Perkeep) LoginForm() Form { return Form{ Elmnts: []FormElement{ { Name: "type", Type: "hidden", Value: "perkeep", }, { Name: "url", Type: "text", Placeholder: "eg: http://localhost:3179", }, }, } } func (this Perkeep) Ls(path string) ([]os.FileInfo, error) { var files []os.FileInfo if path == "/" { response, err := this.query(map[string]interface{}{ // curl 'http://localhost:3179/my-search/camli/search/query' -d '{"sort":"-created","constraint":{"permanode":{"attr": "camliRoot","valueMatches": {}}},"describe":{},"limit":-1}' "sort": "-created", "constraint": map[string]interface{}{ "permanode": map[string]interface{}{ "attr": "camliRoot", "valueMatches": map[string]interface{}{}, }, }, "describe": map[string]interface{}{}, "limit": 1, }) if err != nil { return nil, err } for _, blob := range response.Blobs { if meta, ok := response.Description.Meta[blob.Blob]; ok { if rootNames, ok := meta.Permanode.Attr["camliRoot"]; ok && len(rootNames) > 0 { files = append(files, File{ FName: rootNames[0], FType: "directory", FTime: meta.Permanode.ModTime.Unix(), }) } } } return files, nil } ref, err := this.getRef(path) if err != nil { return nil, err } response, err := this.query(map[string]interface{}{ // curl 'http://localhost:3179/my-search/camli/search/query' -d '{"sort":"-created","constraint":{"permanode":{"relation":{"relation":"parent","any":{"blobRefPrefix":"sha224-ff8f64ab406dc5aec7a35bf182dee79ea20d41bfffc2311fcb4acd9f"}}}},"describe":{"rules":[{"attrs": ["camliContent"]}]},"limit":50}' "sort": "-created", "constraint": map[string]interface{}{ "permanode": map[string]interface{}{ "relation": map[string]interface{}{ "relation": "parent", "any": map[string]interface{}{ "blobRefPrefix": ref, }, }, }, }, "describe": map[string]interface{}{ "rules": []map[string]interface{}{ { "attrs": []string{"camliContent", "title", "camliNodeType"}, }, }, }, "limit": 50, }) if err != nil { return nil, err } for _, blob := range response.Blobs { if meta, ok := response.Description.Meta[blob.Blob]; ok { var ( fileName string fileType string fileSize int64 fileTime int64 = -1 ) if nodeType, ok := meta.Permanode.Attr["camliNodeType"]; ok && len(nodeType) > 0 && nodeType[0] == "directory" { if titles, ok := meta.Permanode.Attr["title"]; ok && len(titles) > 0 { fileType = "directory" fileName = titles[0] fileTime = meta.Permanode.ModTime.Unix() } } else if contentRefs, hasContent := meta.Permanode.Attr["camliContent"]; hasContent && len(contentRefs) > 0 { contentRef := contentRefs[0] if contentMeta, ok := response.Description.Meta[contentRef]; ok && contentMeta.File != nil { fileType = "file" fileName = contentMeta.File.FileName fileTime = contentMeta.File.Time.Unix() fileSize = contentMeta.File.Size } } if fileName != "" && fileType != "" { files = append(files, File{ FName: fileName, FType: fileType, FSize: fileSize, FTime: fileTime, }) } } } return files, nil } func (this Perkeep) Cat(path string) (io.ReadCloser, error) { ref, err := this.getRef(path) if err != nil { return nil, err } response, err := this.describe(ref) if err != nil { return nil, err } contentRefs, hasContent := response.Meta[ref].Permanode.Attr["camliContent"] if !hasContent || len(contentRefs) == 0 { return nil, NewError("No content", 400) } resp, err := http.Get(this.serverURL + "ui/download/" + contentRefs[0]) if err != nil { return nil, NewError("Failed to fetch file: "+err.Error(), 500) } if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, NewError("Failed to fetch file", resp.StatusCode) } return resp.Body, nil } func (this Perkeep) Mkdir(path string) error { return ErrNotImplemented } func (this Perkeep) Rm(path string) error { return ErrNotImplemented } func (this Perkeep) Mv(from, to string) error { return ErrNotImplemented } func (this Perkeep) Save(path string, content io.Reader) error { return ErrNotImplemented } func (this Perkeep) Touch(path string) error { return ErrNotImplemented } func (this *Perkeep) query(searchRequest any) (*SearchResponse, error) { queryJSON, err := json.Marshal(searchRequest) if err != nil { return nil, NewError("Failed to marshal search query: "+err.Error(), 500) } req, err := http.NewRequest( "POST", this.serverURL+"my-search/camli/search/query", bytes.NewBuffer(queryJSON), ) if err != nil { return nil, NewError("Failed to create request: "+err.Error(), 500) } resp, err := HTTPClient.Do(req) if err != nil { return nil, NewError("Failed to query perkeep: "+err.Error(), 500) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, NewError(fmt.Sprintf("Perkeep API error (%d): %s", resp.StatusCode, string(body)), 500) } var result SearchResponse err = json.NewDecoder(resp.Body).Decode(&result) return &result, err } func (this *Perkeep) describe(blobRef string) (*DescribeResponse, error) { describeJSON, err := json.Marshal(map[string]interface{}{ "blobRef": blobRef, }) if err != nil { return nil, NewError("Failed to marshal describe request: "+err.Error(), 500) } req, err := http.NewRequest( "POST", this.serverURL+"my-search/camli/search/describe", bytes.NewBuffer(describeJSON), ) if err != nil { return nil, NewError("Failed to create request: "+err.Error(), 500) } resp, err := HTTPClient.Do(req) if err != nil { return nil, NewError("Failed to describe perkeep: "+err.Error(), 500) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, NewError(fmt.Sprintf("Perkeep API error (%d): %s", resp.StatusCode, string(body)), 500) } var result DescribeResponse err = json.NewDecoder(resp.Body).Decode(&result) return &result, err } func (this *Perkeep) getRef(path string) (string, error) { path = strings.Trim(path, "/") if path == "" { return "", NewError("Empty path", 400) } pathChunks := strings.Split(path, "/") response, err := this.query(map[string]interface{}{ "constraint": map[string]interface{}{ "permanode": map[string]interface{}{ "attr": "camliRoot", "value": pathChunks[0], }, }, "describe": map[string]interface{}{}, "limit": 1, }) if err != nil { return "", err } else if len(response.Blobs) == 0 { return "", NewError("Root folder not found: "+pathChunks[0], 404) } currentRef := response.Blobs[0].Blob for i := 1; i < len(pathChunks); i++ { childResponse, err := this.query(map[string]interface{}{ "constraint": map[string]interface{}{ "permanode": map[string]interface{}{ "relation": map[string]interface{}{ "relation": "parent", "any": map[string]interface{}{ "blobRefPrefix": currentRef, }, }, }, }, "describe": map[string]interface{}{ "rules": []map[string]interface{}{ { "attrs": []string{"camliContent"}, }, }, }, "limit": -1, }) if err != nil { return "", err } found := false for _, blob := range childResponse.Blobs { if meta, ok := childResponse.Description.Meta[blob.Blob]; ok { if titles, ok := meta.Permanode.Attr["title"]; ok && len(titles) > 0 { if titles[0] == pathChunks[i] { currentRef = blob.Blob found = true break } } else if contentRefs, hasContent := meta.Permanode.Attr["camliContent"]; hasContent && len(contentRefs) > 0 { if contentMeta, ok := childResponse.Description.Meta[contentRefs[0]]; ok && contentMeta.File != nil { if contentMeta.File.FileName == pathChunks[i] { currentRef = blob.Blob found = true break } } } } } if !found { return "", NewError("Path element not found: "+pathChunks[i], 404) } } return currentRef, nil }