feat(files): canonical Entry + FileStore REST/DAV adapters (#35)
Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 18s
Tests / Test (Go stable) (pull_request) Successful in 18s
Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 18s
Tests / Test (Go stable) (pull_request) Successful in 18s
This commit is contained in:
+230
@@ -0,0 +1,230 @@
|
||||
package onlyoffice
|
||||
|
||||
// restStore implements FileStore on top of the REST Documents methods in
|
||||
// files.go. It is a thin adapter: no endpoint logic lives here, and every call
|
||||
// is wrapped in DoRetry.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// restStore is a FileStore over the REST Documents API.
|
||||
type restStore struct{ c *Client }
|
||||
|
||||
// Name reports the backend name.
|
||||
func (s *restStore) Name() string { return ProviderREST }
|
||||
|
||||
// List returns the files and folders directly below parentID.
|
||||
func (s *restStore) List(ctx context.Context, parentID string) ([]Entry, error) {
|
||||
var out []Entry
|
||||
err := retryStoreOp(ctx, func() error {
|
||||
raw, err := s.c.ListFolder(ctx, parentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := entriesFromFolderMap(raw, ProviderREST)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = entries
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Stat returns file metadata. The REST adapter resolves files only; folders
|
||||
// are listed by their parent (use List).
|
||||
func (s *restStore) Stat(ctx context.Context, id string) (Entry, error) {
|
||||
var out Entry
|
||||
err := retryStoreOp(ctx, func() error {
|
||||
f, err := s.c.GetFile(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = FileEntryToEntry(f, ProviderREST)
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// CreateFolder creates a subfolder under parentID.
|
||||
func (s *restStore) CreateFolder(ctx context.Context, parentID, title string) (Entry, error) {
|
||||
var out Entry
|
||||
err := retryStoreOp(ctx, func() error {
|
||||
m, err := s.c.CreateFolder(ctx, parentID, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e, err := folderEntryFromMap(m, parentID, ProviderREST)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ParentID == "" {
|
||||
e.ParentID = parentID
|
||||
}
|
||||
if e.Title == "" {
|
||||
e.Title = title
|
||||
}
|
||||
out = e
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Upload streams r into parentID as title. UploadToFolder is path based, so
|
||||
// the reader is spooled to a temporary file first (ponytail: OnlyOffice
|
||||
// multipart upload buffers the whole body anyway).
|
||||
func (s *restStore) Upload(ctx context.Context, parentID, title string, r io.Reader) (Entry, error) {
|
||||
dir, err := os.MkdirTemp("", "oo-rest-upload-")
|
||||
if err != nil {
|
||||
return Entry{}, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
local := filepath.Join(dir, SafeLocalFileName(title))
|
||||
f, err := os.Create(local)
|
||||
if err != nil {
|
||||
return Entry{}, err
|
||||
}
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
f.Close()
|
||||
return Entry{}, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return Entry{}, err
|
||||
}
|
||||
|
||||
var out Entry
|
||||
err = retryStoreOp(ctx, func() error {
|
||||
fe, err := s.c.UploadToFolder(ctx, parentID, local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = FileEntryToEntry(fe, ProviderREST)
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Download streams the file bytes into w.
|
||||
func (s *restStore) Download(ctx context.Context, id string, w io.Writer) (int64, error) {
|
||||
var n int64
|
||||
err := retryStoreOp(ctx, func() error {
|
||||
var e error
|
||||
n, e = s.c.DownloadFile(ctx, id, w)
|
||||
return e
|
||||
})
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Move moves file ids into parentID. The REST MoveFiles endpoint handles files
|
||||
// only; folder moves are not exposed by this adapter.
|
||||
func (s *restStore) Move(ctx context.Context, ids []string, parentID string) error {
|
||||
dest, err := strconv.Atoi(strings.TrimSpace(parentID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("onlyoffice: rest store: move: non-numeric destination folder id %q", parentID)
|
||||
}
|
||||
fileIDs, err := numericIDs(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return retryStoreOp(ctx, func() error {
|
||||
_, err := s.c.MoveFiles(ctx, dest, fileIDs)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Copy copies file ids into parentID. files.go has no copy method, so the
|
||||
// shared REST fileops copy endpoint (CopyDavItems) is used.
|
||||
func (s *restStore) Copy(ctx context.Context, ids []string, parentID string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return retryStoreOp(ctx, func() error {
|
||||
return s.c.CopyDavItems(ctx, nil, ids, parentID)
|
||||
})
|
||||
}
|
||||
|
||||
// Rename sets a new title (including extension) for a file.
|
||||
func (s *restStore) Rename(ctx context.Context, id, title string) error {
|
||||
return retryStoreOp(ctx, func() error {
|
||||
_, err := s.c.RenameFile(ctx, id, title)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Delete permanently deletes file ids.
|
||||
func (s *restStore) Delete(ctx context.Context, ids []string) error {
|
||||
fileIDs, err := numericIDs(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fileIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return retryStoreOp(ctx, func() error {
|
||||
return s.c.DeleteFiles(ctx, fileIDs)
|
||||
})
|
||||
}
|
||||
|
||||
// entriesFromFolderMap converts a ListFolder response map into canonical
|
||||
// entries, reusing the DavFile/DavFolder decoders for robust size handling.
|
||||
func entriesFromFolderMap(m map[string]any, provider string) ([]Entry, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var listing DavListing
|
||||
if err := json.Unmarshal(b, &listing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Entry, 0, len(listing.Folders)+len(listing.Files))
|
||||
for _, f := range listing.Folders {
|
||||
out = append(out, DavFolderToEntry(f, provider))
|
||||
}
|
||||
for _, f := range listing.Files {
|
||||
out = append(out, DavFileToEntry(f, provider))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// folderEntryFromMap converts a CreateFolder response map into a folder Entry.
|
||||
func folderEntryFromMap(m map[string]any, parentID, provider string) (Entry, error) {
|
||||
e := Entry{Kind: Folder, Provider: provider, ParentID: parentID}
|
||||
if m == nil {
|
||||
return e, nil
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
var f DavFolder
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return e, err
|
||||
}
|
||||
e = DavFolderToEntry(f, provider)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// numericIDs parses Documents numeric ids from strings.
|
||||
func numericIDs(ids []string) ([]int, error) {
|
||||
out := make([]int, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(id))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("onlyoffice: rest store: non-numeric id %q", id)
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user