257 lines
6.6 KiB
Go
257 lines
6.6 KiB
Go
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"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// 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 or folder metadata. Folders are resolved through the
|
|
// listing endpoint (their own id appears as the listing's Current); other ids
|
|
// fall back to the file metadata API.
|
|
func (s *restStore) Stat(ctx context.Context, id string) (Entry, error) {
|
|
return s.stat(ctx, id)
|
|
}
|
|
|
|
// stat resolves a single id to a folder or file Entry.
|
|
func (s *restStore) stat(ctx context.Context, id string) (Entry, error) {
|
|
var out Entry
|
|
err := retryStoreOp(ctx, func() error {
|
|
if l, err := s.c.ListDavFolder(ctx, id); err == nil {
|
|
if l != nil && l.Current.ID != "" && l.Current.ID == id {
|
|
out = DavFolderToEntry(l.Current, ProviderREST)
|
|
return nil
|
|
}
|
|
} else if Transient(err) {
|
|
return err
|
|
}
|
|
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 folders and/or files into parentID. Ids are classified through
|
|
// stat so folder moves use folderIds and file moves use fileIds on the shared
|
|
// fileops/move endpoint.
|
|
func (s *restStore) Move(ctx context.Context, ids []string, parentID string) error {
|
|
folders, files, err := s.split(ctx, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(folders) == 0 && len(files) == 0 {
|
|
return nil
|
|
}
|
|
return retryStoreOp(ctx, func() error {
|
|
return s.c.MoveDavItems(ctx, folders, files, parentID)
|
|
})
|
|
}
|
|
|
|
// Copy copies folders and/or files 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 {
|
|
folders, files, err := s.split(ctx, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(folders) == 0 && len(files) == 0 {
|
|
return nil
|
|
}
|
|
return retryStoreOp(ctx, func() error {
|
|
return s.c.CopyDavItems(ctx, folders, files, parentID)
|
|
})
|
|
}
|
|
|
|
// Rename sets a new title (including extension) for a file or folder.
|
|
func (s *restStore) Rename(ctx context.Context, id, title string) error {
|
|
e, err := s.stat(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.Kind == Folder {
|
|
return retryStoreOp(ctx, func() error {
|
|
return s.c.RenameDavFolder(ctx, id, title)
|
|
})
|
|
}
|
|
return retryStoreOp(ctx, func() error {
|
|
_, err := s.c.RenameFile(ctx, id, title)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Delete permanently deletes folders and/or files.
|
|
func (s *restStore) Delete(ctx context.Context, ids []string) error {
|
|
folders, files, err := s.split(ctx, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(folders) == 0 && len(files) == 0 {
|
|
return nil
|
|
}
|
|
return retryStoreOp(ctx, func() error {
|
|
return s.c.DeleteDavItems(ctx, folders, files)
|
|
})
|
|
}
|
|
|
|
// split classifies ids into folder and file id lists.
|
|
func (s *restStore) split(ctx context.Context, ids []string) (folders, files []string, err error) {
|
|
for _, id := range ids {
|
|
e, err := s.stat(ctx, id)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if e.Kind == Folder {
|
|
folders = append(folders, id)
|
|
} else {
|
|
files = append(files, id)
|
|
}
|
|
}
|
|
return folders, files, nil
|
|
}
|
|
|
|
// 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
|
|
}
|