mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (plg_backend_perkeep): support for perkeep
This commit is contained in:
parent
a7177b8c82
commit
e7cd78b164
3 changed files with 370 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ import (
|
|||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_mysql"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_nfs"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_nop"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_perkeep"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_psql"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_s3"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_samba"
|
||||
|
|
|
|||
333
server/plugin/plg_backend_perkeep/index.go
Normal file
333
server/plugin/plg_backend_perkeep/index.go
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
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
|
||||
}
|
||||
36
server/plugin/plg_backend_perkeep/types.go
Normal file
36
server/plugin/plg_backend_perkeep/types.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package plg_backend_perkeep
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SearchResponse struct {
|
||||
Blobs []struct {
|
||||
Blob string `json:"blob"`
|
||||
} `json:"blobs"`
|
||||
Description struct {
|
||||
Meta map[string]struct {
|
||||
Permanode struct {
|
||||
Attr map[string][]string `json:"attr"`
|
||||
ModTime time.Time
|
||||
} `json:"permanode"`
|
||||
File *struct {
|
||||
FileName string `json:"fileName"`
|
||||
Size int64 `json:"size"`
|
||||
Time time.Time `json:"time"`
|
||||
WholeRef string `json:"wholeRef"`
|
||||
} `json:"file"`
|
||||
CamliType string `json:"camliType"`
|
||||
} `json:"meta"`
|
||||
} `json:"description"`
|
||||
}
|
||||
|
||||
type DescribeResponse struct {
|
||||
Meta map[string]struct {
|
||||
Permanode struct {
|
||||
Attr map[string][]string `json:"attr"`
|
||||
ModTime time.Time
|
||||
} `json:"permanode"`
|
||||
CamliType string `json:"camliType"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
Loading…
Reference in a new issue