refactor(files): единый файловый фасад + миграция CLI/TUI (#38) #43
@@ -17,6 +17,18 @@ const MailListPageSize = 25
|
||||
// Loader fetches list items for a menu subject using the OnlyOffice client.
|
||||
type Loader struct {
|
||||
Client *onlyoffice.Client
|
||||
|
||||
// Files is the backend-agnostic file store used for file download, preview
|
||||
// and delete. When nil it falls back to Client.FileStore(ProviderREST).
|
||||
Files onlyoffice.FileStore
|
||||
}
|
||||
|
||||
// fileStore returns the configured file store, defaulting to REST.
|
||||
func (l *Loader) fileStore() onlyoffice.FileStore {
|
||||
if l.Files != nil {
|
||||
return l.Files
|
||||
}
|
||||
return l.Client.FileStore(onlyoffice.ProviderREST)
|
||||
}
|
||||
|
||||
// List returns items for the given list spec (nav leaf).
|
||||
@@ -171,11 +183,7 @@ func (l *Loader) executeDelete(ctx context.Context, item model.Item) (string, er
|
||||
}
|
||||
return fmt.Sprintf("Deleted message %s", item.Title), nil
|
||||
case model.KindFile:
|
||||
id, err := strconv.Atoi(item.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := l.Client.DeleteFiles(ctx, []int{id}); err != nil {
|
||||
if err := l.fileStore().Delete(ctx, []string{item.ID}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Deleted file %s", item.Title), nil
|
||||
@@ -199,7 +207,7 @@ func (l *Loader) executeDownload(ctx context.Context, item model.Item, destPath
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := l.Client.DownloadFile(ctx, item.ID, f); err != nil {
|
||||
if _, err := l.fileStore().Download(ctx, item.ID, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Downloaded to %s", destPath), nil
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/preview"
|
||||
)
|
||||
@@ -30,13 +29,11 @@ func (l *Loader) filePreviewMarkdown(ctx context.Context, item model.Item) (stri
|
||||
return "", fmt.Errorf("file id missing")
|
||||
}
|
||||
name := item.Title
|
||||
if meta, err := l.Client.GetFile(ctx, item.ID); err == nil && meta != nil {
|
||||
if t := onlyoffice.FileEntryTitle(meta); t != "" {
|
||||
name = t
|
||||
}
|
||||
if e, err := l.fileStore().Stat(ctx, item.ID); err == nil && e.Title != "" {
|
||||
name = e.Title
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := l.Client.DownloadFile(ctx, item.ID, &buf); err != nil {
|
||||
if _, err := l.fileStore().Download(ctx, item.ID, &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return preview.FileBytesToMarkdown(name, buf.Bytes())
|
||||
|
||||
+55
-51
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -12,8 +13,8 @@ func init() {
|
||||
rootCmd.AddCommand(davCmd())
|
||||
}
|
||||
|
||||
// davCmd exposes the Documents module through the same Dav calls that back
|
||||
// oo-webdav (ListDavFolder / MoveDavItems / CopyDavItems / DownloadDavFile).
|
||||
// davCmd exposes the Documents module through the backend-agnostic FileStore
|
||||
// (DAV backend). The underlying Dav calls are the oo-webdav proven path:
|
||||
// MoveDavItems sends resolveType=Skip + holdResult=true, which the legacy
|
||||
// fileops/move call without those params silently ignores (200 without move).
|
||||
func davCmd() *cobra.Command {
|
||||
@@ -61,59 +62,46 @@ func davLsCmd() *cobra.Command {
|
||||
printTable([]string{"id", "title", "filesCount", "foldersCount"}, rows)
|
||||
return nil
|
||||
}
|
||||
l, err := c.ListDavFolder(ctx, args[0])
|
||||
entries, err := c.FileStore(onlyoffice.ProviderDAV).List(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outputFormat == "json" {
|
||||
folders := make([]map[string]any, 0, len(l.Folders))
|
||||
for _, f := range l.Folders {
|
||||
folders = append(folders, map[string]any{
|
||||
"id": f.ID,
|
||||
"title": f.Title,
|
||||
"filesCount": f.FilesCount,
|
||||
"foldersCount": f.FoldersCount,
|
||||
})
|
||||
folders := make([]onlyoffice.Entry, 0, len(entries))
|
||||
files := make([]onlyoffice.Entry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.Kind == onlyoffice.Folder {
|
||||
folders = append(folders, e)
|
||||
} else {
|
||||
files = append(files, e)
|
||||
}
|
||||
files := make([]map[string]any, 0, len(l.Files))
|
||||
for _, f := range l.Files {
|
||||
files = append(files, map[string]any{
|
||||
"id": f.ID,
|
||||
"title": f.Title,
|
||||
"size": f.Size,
|
||||
"updated": f.Updated,
|
||||
})
|
||||
}
|
||||
printObject(map[string]any{"folders": folders, "files": files})
|
||||
return nil
|
||||
}
|
||||
if len(l.Folders) > 0 {
|
||||
frows := make([]map[string]any, 0, len(l.Folders))
|
||||
for _, f := range l.Folders {
|
||||
frows = append(frows, map[string]any{
|
||||
"id": f.ID,
|
||||
"title": f.Title,
|
||||
"filesCount": f.FilesCount,
|
||||
"foldersCount": f.FoldersCount,
|
||||
})
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Println("folders:")
|
||||
}
|
||||
printTable([]string{"id", "title", "filesCount", "foldersCount"}, frows)
|
||||
frows := make([]map[string]any, 0, len(folders))
|
||||
for _, f := range folders {
|
||||
frows = append(frows, map[string]any{
|
||||
"id": f.ID,
|
||||
"title": f.Title,
|
||||
"filesCount": f.FilesCount,
|
||||
"foldersCount": f.FoldersCount,
|
||||
})
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(l.Files))
|
||||
for _, f := range l.Files {
|
||||
rows := make([]map[string]any, 0, len(files))
|
||||
for _, f := range files {
|
||||
rows = append(rows, map[string]any{
|
||||
"id": f.ID,
|
||||
"title": f.Title,
|
||||
"size": f.Size,
|
||||
"updated": f.Updated,
|
||||
"updated": entryUpdated(f),
|
||||
})
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Println("files:")
|
||||
if outputFormat == "json" {
|
||||
printObject(map[string]any{"folders": frows, "files": rows})
|
||||
return nil
|
||||
}
|
||||
if len(frows) > 0 {
|
||||
fmt.Println("folders:")
|
||||
printTable([]string{"id", "title", "filesCount", "foldersCount"}, frows)
|
||||
}
|
||||
fmt.Println("files:")
|
||||
printTable([]string{"id", "title", "size", "updated"}, rows)
|
||||
return nil
|
||||
},
|
||||
@@ -121,6 +109,18 @@ func davLsCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// entryUpdated prefers the backend-native timestamp string so table/JSON output
|
||||
// round-trips what the API returned.
|
||||
func entryUpdated(e onlyoffice.Entry) string {
|
||||
if e.Updated != "" {
|
||||
return e.Updated
|
||||
}
|
||||
if e.Modified.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return e.Modified.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func davMoveCmd() *cobra.Command {
|
||||
var folderIDs []string
|
||||
cmd := &cobra.Command{
|
||||
@@ -132,7 +132,8 @@ func davMoveCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.MoveDavItems(cmd.Context(), folderIDs, args[1:], args[0]); err != nil {
|
||||
ids := append(append([]string{}, folderIDs...), args[1:]...)
|
||||
if err := c.FileStore(onlyoffice.ProviderDAV).Move(cmd.Context(), ids, args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"moved_files": args[1:], "moved_folders": folderIDs, "dest": args[0]})
|
||||
@@ -154,7 +155,8 @@ func davCopyCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.CopyDavItems(cmd.Context(), folderIDs, args[1:], args[0]); err != nil {
|
||||
ids := append(append([]string{}, folderIDs...), args[1:]...)
|
||||
if err := c.FileStore(onlyoffice.ProviderDAV).Copy(cmd.Context(), ids, args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"copied_files": args[1:], "copied_folders": folderIDs, "dest": args[0]})
|
||||
@@ -175,7 +177,7 @@ func davMkdirCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := c.CreateDavFolder(cmd.Context(), args[0], args[1])
|
||||
f, err := c.FileStore(onlyoffice.ProviderDAV).CreateFolder(cmd.Context(), args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -200,7 +202,8 @@ func davRemoveCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.DeleteDavItems(cmd.Context(), folderIDs, args); err != nil {
|
||||
ids := append(append([]string{}, args...), folderIDs...)
|
||||
if err := c.FileStore(onlyoffice.ProviderDAV).Delete(cmd.Context(), ids); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"deleted_files": args, "deleted_folders": folderIDs})
|
||||
@@ -221,7 +224,7 @@ func davRenameFileCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RenameDavFile(cmd.Context(), args[0], args[1]); err != nil {
|
||||
if err := c.FileStore(onlyoffice.ProviderDAV).Rename(cmd.Context(), args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": args[0], "title": args[1]})
|
||||
@@ -240,7 +243,7 @@ func davRenameFolderCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RenameDavFolder(cmd.Context(), args[0], args[1]); err != nil {
|
||||
if err := c.FileStore(onlyoffice.ProviderDAV).Rename(cmd.Context(), args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": args[0], "title": args[1]})
|
||||
@@ -261,20 +264,21 @@ func davDownloadCmd() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
ctx := cmd.Context()
|
||||
f, err := c.GetFile(ctx, args[0])
|
||||
store := c.FileStore(onlyoffice.ProviderDAV)
|
||||
e, err := store.Stat(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := to
|
||||
if path == "" {
|
||||
path = onlyoffice.SafeLocalFileName(onlyoffice.FileEntryTitle(f))
|
||||
path = onlyoffice.SafeLocalFileName(e.Title)
|
||||
}
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
n, err := c.DownloadDavFile(ctx, args[0], out)
|
||||
n, err := store.Download(ctx, args[0], out)
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -159,20 +160,22 @@ func prjFilesDownloadCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := c.GetFile(cmd.Context(), args[0])
|
||||
ctx := cmd.Context()
|
||||
store := c.Files()
|
||||
e, err := store.Stat(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := to
|
||||
if path == "" {
|
||||
path = onlyoffice.SafeLocalFileName(onlyoffice.FileEntryTitle(f))
|
||||
path = onlyoffice.SafeLocalFileName(e.Title)
|
||||
}
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
n, err := c.DownloadFile(cmd.Context(), args[0], out)
|
||||
n, err := store.Download(ctx, args[0], out)
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
@@ -199,11 +202,16 @@ func prjFilesRenameCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := c.RenameFile(cmd.Context(), args[0], args[1])
|
||||
store := c.Files()
|
||||
if err := store.Rename(cmd.Context(), args[0], args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
entry, err := store.Stat(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(fileEntryToMap(entry))
|
||||
entry.Title = args[1]
|
||||
printObject(entryToMap(entry))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -228,7 +236,7 @@ func prjFilesDeleteCmd() *cobra.Command {
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := c.DeleteFiles(cmd.Context(), ids); err != nil {
|
||||
if err := c.Files().Delete(cmd.Context(), args); err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"deleted": ids})
|
||||
@@ -317,6 +325,29 @@ func fileEntryToMap(f *onlyoffice.FileEntry) map[string]any {
|
||||
return m
|
||||
}
|
||||
|
||||
// entryToMap renders a canonical Entry with the same keys as fileEntryToMap.
|
||||
func entryToMap(e onlyoffice.Entry) map[string]any {
|
||||
m := map[string]any{
|
||||
"id": e.ID,
|
||||
"title": e.Title,
|
||||
"fileExst": filepath.Ext(e.Title),
|
||||
"contentLength": contentLengthString(e.Size),
|
||||
}
|
||||
if e.Updated != "" {
|
||||
m["updated"] = e.Updated
|
||||
} else if !e.Modified.IsZero() {
|
||||
m["updated"] = e.Modified.Format(time.RFC3339)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func contentLengthString(n int64) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(n, 10)
|
||||
}
|
||||
|
||||
func fileIDStr(f *onlyoffice.FileEntry) string {
|
||||
if f == nil || f.ID == nil {
|
||||
return ""
|
||||
|
||||
+8
-5
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/eslider/go-onlyoffice/cmd/internal/bootstrap"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -9,9 +10,9 @@ func init() {
|
||||
rootCmd.AddCommand(searchCmd())
|
||||
}
|
||||
|
||||
// searchCmd queries the OnlyOffice Elasticsearch index directly. The REST
|
||||
// /api/2.0/files/@search endpoint only searches file names in the database;
|
||||
// content search needs ES (see docs/elasticsearch.md).
|
||||
// searchCmd queries the OnlyOffice document index through the file facade. The
|
||||
// REST /api/2.0/files/@search endpoint only searches file names in the database;
|
||||
// content search needs Elasticsearch (see docs/elasticsearch.md).
|
||||
func searchCmd() *cobra.Command {
|
||||
var (
|
||||
content bool
|
||||
@@ -33,11 +34,13 @@ func searchCmd() *cobra.Command {
|
||||
if asJSON {
|
||||
outputFormat = "json"
|
||||
}
|
||||
es, err := onlyoffice.NewESSearcher(onlyoffice.ESConfigFromEnv())
|
||||
bootstrap.LoadEnv()
|
||||
c := onlyoffice.NewClient(onlyoffice.GetEnvironmentCredentials())
|
||||
searcher, err := c.Files().Search()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hits, err := es.Search(cmd.Context(), onlyoffice.SearchQuery{
|
||||
hits, err := searcher.Search(cmd.Context(), onlyoffice.SearchQuery{
|
||||
Text: args[0],
|
||||
InContent: content,
|
||||
FolderID: folder,
|
||||
|
||||
+25
-10
@@ -52,8 +52,17 @@ type Entry struct {
|
||||
MIME string
|
||||
Created time.Time
|
||||
Modified time.Time
|
||||
// Updated is the backend-native timestamp string, when the backend exposes
|
||||
// one. It lets list output round-trip the API value; Modified is the
|
||||
// parsed form for logic.
|
||||
Updated string
|
||||
Version int
|
||||
Provider string
|
||||
|
||||
// Folder-only counters. Zero for files and for backends that do not
|
||||
// report them.
|
||||
FilesCount int
|
||||
FoldersCount int
|
||||
}
|
||||
|
||||
// FileStore is the operation surface every file backend implements.
|
||||
@@ -97,8 +106,8 @@ type Searcher interface {
|
||||
}
|
||||
|
||||
// FileStore returns the adapter for a backend name: ProviderREST (default) or
|
||||
// ProviderDAV. Unknown or empty names select the REST backend. The full facade
|
||||
// (backend composition) is deliberately left to a later change.
|
||||
// ProviderDAV. Unknown or empty names select the REST backend. The composed
|
||||
// facade (backend selection/fallback) lives on FileClient in file_facade.go.
|
||||
func (c *Client) FileStore(backend string) FileStore {
|
||||
switch strings.ToLower(strings.TrimSpace(backend)) {
|
||||
case ProviderDAV, "webdav":
|
||||
@@ -108,8 +117,9 @@ func (c *Client) FileStore(backend string) FileStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Files returns the default (REST) file store.
|
||||
func (c *Client) Files() FileStore { return c.FileStore(ProviderREST) }
|
||||
// Files returns the composed file facade. The returned *FileClient implements
|
||||
// FileStore, so callers that used Files() as the plain REST store keep working.
|
||||
func (c *Client) Files() *FileClient { return c.newFileClient() }
|
||||
|
||||
// retryStoreOp runs one store operation under the shared deterministic
|
||||
// transient-error policy (429/502/503/504).
|
||||
@@ -140,6 +150,7 @@ func FileEntryToEntry(f *FileEntry, provider string) Entry {
|
||||
e.MIME = mimeForTitle(e.Title, exst)
|
||||
if f.Updated != nil {
|
||||
e.Modified = *f.Updated
|
||||
e.Updated = f.Updated.Format(time.RFC3339)
|
||||
}
|
||||
return e
|
||||
}
|
||||
@@ -153,6 +164,7 @@ func DavFileToEntry(f DavFile, provider string) Entry {
|
||||
Size: f.Size,
|
||||
MIME: mimeForTitle(f.Title, ""),
|
||||
Modified: f.ModTime(),
|
||||
Updated: f.Updated,
|
||||
Provider: provider,
|
||||
}
|
||||
}
|
||||
@@ -160,12 +172,15 @@ func DavFileToEntry(f DavFile, provider string) Entry {
|
||||
// DavFolderToEntry converts a WebDAV folder row to the canonical model.
|
||||
func DavFolderToEntry(f DavFolder, provider string) Entry {
|
||||
return Entry{
|
||||
ID: f.ID,
|
||||
ParentID: f.ParentID,
|
||||
Title: f.Title,
|
||||
Kind: Folder,
|
||||
Modified: f.ModTime(),
|
||||
Provider: provider,
|
||||
ID: f.ID,
|
||||
ParentID: f.ParentID,
|
||||
Title: f.Title,
|
||||
Kind: Folder,
|
||||
Modified: f.ModTime(),
|
||||
Updated: f.Updated,
|
||||
Provider: provider,
|
||||
FilesCount: f.FilesCount,
|
||||
FoldersCount: f.FoldersCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package onlyoffice
|
||||
|
||||
// Single file client (epic #34, F4 #38). FileClient composes the registered
|
||||
// FileStore and Searcher backends and picks one per operation: REST/DAV for
|
||||
// writes, PostgreSQL (when registered) for fast reads, Elasticsearch for name
|
||||
// and content search. Client.Files returns the facade; it also implements
|
||||
// FileStore, so existing callers keep compiling.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Provider names for the composed backends. ProviderPG is reserved for the
|
||||
// read-only PostgreSQL store (F2 #36); it is not registered until it exists.
|
||||
const (
|
||||
ProviderPG = "postgres"
|
||||
ProviderES = "elasticsearch"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoReadBackend = errors.New("onlyoffice: no file backend registered for reads")
|
||||
errNoWriteBackend = errors.New("onlyoffice: no file backend registered for writes")
|
||||
errNoSearcher = errors.New("onlyoffice: no search backend registered (set ONLYOFFICE_ES_URL)")
|
||||
)
|
||||
|
||||
// FileClient is the single entry point for file operations. It holds the
|
||||
// registered backends and the order in which each operation tries them.
|
||||
type FileClient struct {
|
||||
stores map[string]FileStore
|
||||
searchers map[string]Searcher
|
||||
|
||||
readOrder []string
|
||||
writeOrder []string
|
||||
searchOrder []string
|
||||
}
|
||||
|
||||
// newFileClient builds the facade over the built-in REST and DAV stores. The
|
||||
// Elasticsearch searcher is registered when ONLYOFFICE_ES_URL is set; the
|
||||
// missing-credential case is left to Search so read-only commands still work.
|
||||
func (c *Client) newFileClient() *FileClient {
|
||||
f := &FileClient{
|
||||
stores: map[string]FileStore{
|
||||
ProviderREST: &restStore{c: c},
|
||||
ProviderDAV: &davStore{c: c},
|
||||
},
|
||||
searchers: map[string]Searcher{},
|
||||
readOrder: []string{ProviderPG, ProviderREST, ProviderDAV},
|
||||
writeOrder: []string{ProviderREST, ProviderDAV},
|
||||
searchOrder: []string{ProviderES},
|
||||
}
|
||||
if cfg := ESConfigFromEnv(); cfg.URL != "" {
|
||||
if es, err := NewESSearcher(cfg); err == nil {
|
||||
f.searchers[ProviderES] = es
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// RegisterStore adds or replaces a named backend (for example the PostgreSQL
|
||||
// read store). The name is matched case-insensitively.
|
||||
func (f *FileClient) RegisterStore(name string, s FileStore) {
|
||||
if f == nil || s == nil {
|
||||
return
|
||||
}
|
||||
name = normalizeProvider(name)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
if f.stores == nil {
|
||||
f.stores = map[string]FileStore{}
|
||||
}
|
||||
f.stores[name] = s
|
||||
}
|
||||
|
||||
// RegisterSearcher adds or replaces a named search backend.
|
||||
func (f *FileClient) RegisterSearcher(name string, s Searcher) {
|
||||
if f == nil || s == nil {
|
||||
return
|
||||
}
|
||||
name = normalizeProvider(name)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
if f.searchers == nil {
|
||||
f.searchers = map[string]Searcher{}
|
||||
}
|
||||
f.searchers[name] = s
|
||||
}
|
||||
|
||||
// Read returns the preferred backend for reads: PostgreSQL when registered,
|
||||
// then REST, then WebDAV.
|
||||
func (f *FileClient) Read() FileStore { return f.firstStore(f.readOrder) }
|
||||
|
||||
// Write returns the preferred backend for writes: REST, then WebDAV.
|
||||
func (f *FileClient) Write() FileStore { return f.firstStore(f.writeOrder) }
|
||||
|
||||
// Search returns the preferred name/content searcher (Elasticsearch), or an
|
||||
// error when no search backend is configured.
|
||||
func (f *FileClient) Search() (Searcher, error) {
|
||||
if f == nil {
|
||||
return nil, errNoSearcher
|
||||
}
|
||||
for _, name := range f.searchOrder {
|
||||
if s := f.searchers[normalizeProvider(name)]; s != nil {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, errNoSearcher
|
||||
}
|
||||
|
||||
// firstStore returns the first registered store in the order.
|
||||
func (f *FileClient) firstStore(order []string) FileStore {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
for _, name := range order {
|
||||
if s := f.stores[normalizeProvider(name)]; s != nil {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// orderedStores returns the registered stores in the order.
|
||||
func (f *FileClient) orderedStores(order []string) []FileStore {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]FileStore, 0, len(order))
|
||||
for _, name := range order {
|
||||
if s := f.stores[normalizeProvider(name)]; s != nil {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeProvider(name string) string {
|
||||
return strings.ToLower(strings.TrimSpace(name))
|
||||
}
|
||||
|
||||
// Name implements FileStore and reports the preferred read backend.
|
||||
func (f *FileClient) Name() string {
|
||||
if s := f.Read(); s != nil {
|
||||
return s.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// List reads from the preferred backend, falling back to the next read backend
|
||||
// only on a transient error (429/502/503/504).
|
||||
func (f *FileClient) List(ctx context.Context, parentID string) ([]Entry, error) {
|
||||
return fallbackRead(ctx, f.orderedStores(f.readOrder), func(s FileStore) ([]Entry, error) {
|
||||
return s.List(ctx, parentID)
|
||||
})
|
||||
}
|
||||
|
||||
// Stat reads from the preferred backend, with the same transient fallback.
|
||||
func (f *FileClient) Stat(ctx context.Context, id string) (Entry, error) {
|
||||
return fallbackRead(ctx, f.orderedStores(f.readOrder), func(s FileStore) (Entry, error) {
|
||||
return s.Stat(ctx, id)
|
||||
})
|
||||
}
|
||||
|
||||
// Download streams file bytes. It does not fall back: a failed attempt may have
|
||||
// already written partial bytes into w, so a second backend would append.
|
||||
func (f *FileClient) Download(ctx context.Context, id string, w io.Writer) (int64, error) {
|
||||
s := f.Read()
|
||||
if s == nil {
|
||||
return 0, errNoReadBackend
|
||||
}
|
||||
return s.Download(ctx, id, w)
|
||||
}
|
||||
|
||||
// CreateFolder writes to the preferred write backend.
|
||||
func (f *FileClient) CreateFolder(ctx context.Context, parentID, title string) (Entry, error) {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return Entry{}, errNoWriteBackend
|
||||
}
|
||||
return s.CreateFolder(ctx, parentID, title)
|
||||
}
|
||||
|
||||
// Upload writes to the preferred write backend.
|
||||
func (f *FileClient) Upload(ctx context.Context, parentID, title string, r io.Reader) (Entry, error) {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return Entry{}, errNoWriteBackend
|
||||
}
|
||||
return s.Upload(ctx, parentID, title, r)
|
||||
}
|
||||
|
||||
// Move writes to the preferred write backend.
|
||||
func (f *FileClient) Move(ctx context.Context, ids []string, parentID string) error {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return errNoWriteBackend
|
||||
}
|
||||
return s.Move(ctx, ids, parentID)
|
||||
}
|
||||
|
||||
// Copy writes to the preferred write backend.
|
||||
func (f *FileClient) Copy(ctx context.Context, ids []string, parentID string) error {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return errNoWriteBackend
|
||||
}
|
||||
return s.Copy(ctx, ids, parentID)
|
||||
}
|
||||
|
||||
// Rename writes to the preferred write backend.
|
||||
func (f *FileClient) Rename(ctx context.Context, id, title string) error {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return errNoWriteBackend
|
||||
}
|
||||
return s.Rename(ctx, id, title)
|
||||
}
|
||||
|
||||
// Delete writes to the preferred write backend.
|
||||
func (f *FileClient) Delete(ctx context.Context, ids []string) error {
|
||||
s := f.Write()
|
||||
if s == nil {
|
||||
return errNoWriteBackend
|
||||
}
|
||||
return s.Delete(ctx, ids)
|
||||
}
|
||||
|
||||
// fallbackRead runs op against each store in order, moving on only when the
|
||||
// error is transient. Non-transient errors (not found, forbidden) are final.
|
||||
func fallbackRead[T any](ctx context.Context, stores []FileStore, op func(FileStore) (T, error)) (T, error) {
|
||||
var zero T
|
||||
if len(stores) == 0 {
|
||||
return zero, errNoReadBackend
|
||||
}
|
||||
var err error
|
||||
for i, s := range stores {
|
||||
var v T
|
||||
v, err = op(s)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if i == len(stores)-1 || !Transient(err) {
|
||||
return zero, err
|
||||
}
|
||||
}
|
||||
return zero, err
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package onlyoffice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeStore is a FileStore test double; it records which backend served a call
|
||||
// and returns a canned result or error.
|
||||
type fakeStore struct {
|
||||
name string
|
||||
entries []Entry
|
||||
err error
|
||||
calls *[]string
|
||||
}
|
||||
|
||||
func (f *fakeStore) record(op string) {
|
||||
if f.calls != nil {
|
||||
*f.calls = append(*f.calls, op+":"+f.name)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeStore) Name() string { return f.name }
|
||||
|
||||
func (f *fakeStore) List(_ context.Context, _ string) ([]Entry, error) {
|
||||
f.record("list")
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.entries, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) Stat(_ context.Context, id string) (Entry, error) {
|
||||
f.record("stat")
|
||||
if f.err != nil {
|
||||
return Entry{}, f.err
|
||||
}
|
||||
return Entry{ID: id, Title: "t-" + f.name, Provider: f.name}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateFolder(_ context.Context, _, title string) (Entry, error) {
|
||||
f.record("mkdir")
|
||||
if f.err != nil {
|
||||
return Entry{}, f.err
|
||||
}
|
||||
return Entry{ID: "new", Title: title, Provider: f.name}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) Upload(_ context.Context, _, title string, _ io.Reader) (Entry, error) {
|
||||
f.record("upload")
|
||||
return Entry{ID: "up", Title: title, Provider: f.name}, f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Download(_ context.Context, _ string, _ io.Writer) (int64, error) {
|
||||
f.record("download")
|
||||
return 0, f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Move(_ context.Context, _ []string, _ string) error {
|
||||
f.record("move")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Copy(_ context.Context, _ []string, _ string) error {
|
||||
f.record("copy")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Rename(_ context.Context, _, _ string) error {
|
||||
f.record("rename")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Delete(_ context.Context, _ []string) error {
|
||||
f.record("delete")
|
||||
return f.err
|
||||
}
|
||||
|
||||
type fakeSearcher struct{ name string }
|
||||
|
||||
func (s *fakeSearcher) Name() string { return s.name }
|
||||
|
||||
func (s *fakeSearcher) Search(_ context.Context, _ SearchQuery) ([]SearchHit, error) {
|
||||
return []SearchHit{{Entry: Entry{Title: s.name}}}, nil
|
||||
}
|
||||
|
||||
func newFacadeTestClient(stores map[string]FileStore, read, write []string) *FileClient {
|
||||
return &FileClient{
|
||||
stores: stores,
|
||||
searchers: map[string]Searcher{},
|
||||
readOrder: read,
|
||||
writeOrder: write,
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileClientIsFileStore guarantees the facade can stand in for the
|
||||
// interface anywhere a plain FileStore is expected.
|
||||
func TestFileClientIsFileStore(t *testing.T) {
|
||||
var _ FileStore = (*FileClient)(nil)
|
||||
}
|
||||
|
||||
func TestClientFilesPrefersRESTForReadsAndWrites(t *testing.T) {
|
||||
c := NewClient(Credentials{})
|
||||
f := c.Files()
|
||||
if got := f.Read().Name(); got != ProviderREST {
|
||||
t.Errorf("Read().Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
if got := f.Write().Name(); got != ProviderREST {
|
||||
t.Errorf("Write().Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
if got := f.Name(); got != ProviderREST {
|
||||
t.Errorf("Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientPostgresTakesReadPriority(t *testing.T) {
|
||||
pg := &fakeStore{name: ProviderPG}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{ProviderREST: &fakeStore{name: ProviderREST}, ProviderPG: pg},
|
||||
[]string{ProviderPG, ProviderREST},
|
||||
[]string{ProviderREST},
|
||||
)
|
||||
if got := f.Read().Name(); got != ProviderPG {
|
||||
t.Errorf("Read().Name() = %q, want %q", got, ProviderPG)
|
||||
}
|
||||
if got := f.Write().Name(); got != ProviderREST {
|
||||
t.Errorf("Write().Name() = %q, want %q (PG is read-only)", got, ProviderREST)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientRegisterStoreNormalizesName(t *testing.T) {
|
||||
pg := &fakeStore{name: "pg"}
|
||||
f := newFacadeTestClient(map[string]FileStore{}, []string{ProviderPG}, nil)
|
||||
f.RegisterStore(" POSTGRES ", pg)
|
||||
if got := f.Read(); got != pg {
|
||||
t.Fatalf("Read() = %v, want registered postgres store", got)
|
||||
}
|
||||
f.RegisterStore("", pg)
|
||||
f.RegisterStore("pg", nil)
|
||||
}
|
||||
|
||||
func TestFileClientReadFallsBackOnlyOnTransient(t *testing.T) {
|
||||
var calls []string
|
||||
primary := &fakeStore{name: "primary", err: fmt.Errorf("onlyoffice: list: 503 unavailable"), calls: &calls}
|
||||
secondary := &fakeStore{name: "secondary", entries: []Entry{{ID: "1"}}, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{"primary": primary, "secondary": secondary},
|
||||
[]string{"primary", "secondary"},
|
||||
nil,
|
||||
)
|
||||
got, err := f.List(context.Background(), "root")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].ID != "1" {
|
||||
t.Fatalf("List() = %+v, want secondary entry", got)
|
||||
}
|
||||
want := []string{"list:primary", "list:secondary"}
|
||||
if fmt.Sprint(calls) != fmt.Sprint(want) {
|
||||
t.Fatalf("call order = %v, want %v", calls, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientReadStopsOnPermanentError(t *testing.T) {
|
||||
var calls []string
|
||||
primary := &fakeStore{name: "primary", err: errors.New("onlyoffice: not found"), calls: &calls}
|
||||
secondary := &fakeStore{name: "secondary", entries: []Entry{{ID: "1"}}, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{"primary": primary, "secondary": secondary},
|
||||
[]string{"primary", "secondary"},
|
||||
nil,
|
||||
)
|
||||
if _, err := f.List(context.Background(), "root"); err == nil {
|
||||
t.Fatal("expected permanent error to be returned")
|
||||
}
|
||||
if len(calls) != 1 || calls[0] != "list:primary" {
|
||||
t.Fatalf("secondary backend must not run on a permanent error: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientWriteUsesWriteBackend(t *testing.T) {
|
||||
var calls []string
|
||||
rest := &fakeStore{name: ProviderREST, calls: &calls}
|
||||
dav := &fakeStore{name: ProviderDAV, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{ProviderREST: rest, ProviderDAV: dav},
|
||||
[]string{ProviderREST},
|
||||
[]string{ProviderREST, ProviderDAV},
|
||||
)
|
||||
if _, err := f.CreateFolder(context.Background(), "p", "t"); err != nil {
|
||||
t.Fatalf("CreateFolder: %v", err)
|
||||
}
|
||||
if _, err := f.Upload(context.Background(), "p", "t", nil); err != nil {
|
||||
t.Fatalf("Upload: %v", err)
|
||||
}
|
||||
if len(calls) != 2 || calls[0] != "mkdir:rest" || calls[1] != "upload:rest" {
|
||||
t.Fatalf("write calls = %v, want REST", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientWriteWithoutBackend(t *testing.T) {
|
||||
f := newFacadeTestClient(map[string]FileStore{}, nil, nil)
|
||||
if err := f.Delete(context.Background(), []string{"1"}); !errors.Is(err, errNoWriteBackend) {
|
||||
t.Fatalf("Delete err = %v, want errNoWriteBackend", err)
|
||||
}
|
||||
if _, err := f.List(context.Background(), "root"); !errors.Is(err, errNoReadBackend) {
|
||||
t.Fatalf("List err = %v, want errNoReadBackend", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientSearchSelection(t *testing.T) {
|
||||
f := &FileClient{searchers: map[string]Searcher{}, searchOrder: []string{ProviderES}}
|
||||
_, err := f.Search()
|
||||
if err == nil || !strings.Contains(err.Error(), "ONLYOFFICE_ES_URL") {
|
||||
t.Fatalf("Search without backend = %v, want ONLYOFFICE_ES_URL hint", err)
|
||||
}
|
||||
es := &fakeSearcher{name: "fake-es"}
|
||||
f.RegisterSearcher(ProviderES, es)
|
||||
got, err := f.Search()
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if got.Name() != "fake-es" {
|
||||
t.Fatalf("searcher = %q, want fake-es", got.Name())
|
||||
}
|
||||
}
|
||||
@@ -247,6 +247,8 @@ func (c *Client) UploadProjectFileReplacing(ctx context.Context, projectID, loca
|
||||
}
|
||||
|
||||
// GetFile returns file metadata including viewUrl for download.
|
||||
//
|
||||
// Deprecated: use FileStore.Stat via Client.Files()/Client.FileStore.
|
||||
func (c *Client) GetFile(ctx context.Context, fileID string) (*FileEntry, error) {
|
||||
if fileID == "" {
|
||||
return nil, fmt.Errorf("file id is required")
|
||||
@@ -260,6 +262,8 @@ func (c *Client) GetFile(ctx context.Context, fileID string) (*FileEntry, error)
|
||||
}
|
||||
|
||||
// RenameFile sets a new title (including extension) for the file.
|
||||
//
|
||||
// Deprecated: use FileStore.Rename via Client.Files()/Client.FileStore.
|
||||
func (c *Client) RenameFile(ctx context.Context, fileID, newTitle string) (*FileEntry, error) {
|
||||
if fileID == "" || newTitle == "" {
|
||||
return nil, fmt.Errorf("file id and new title are required")
|
||||
@@ -275,6 +279,8 @@ func (c *Client) RenameFile(ctx context.Context, fileID, newTitle string) (*File
|
||||
// DeleteFiles permanently deletes files by numeric id (Documents module).
|
||||
// Uses per-file DELETE (DeleteDavItems); fileops/delete returns 200 on some
|
||||
// portals (e.g. produktor.io) without removing the file.
|
||||
//
|
||||
// Deprecated: use FileStore.Delete via Client.Files()/Client.FileStore.
|
||||
func (c *Client) DeleteFiles(ctx context.Context, fileIDs []int) error {
|
||||
if len(fileIDs) == 0 {
|
||||
return fmt.Errorf("no file ids to delete")
|
||||
@@ -288,6 +294,8 @@ func (c *Client) DeleteFiles(ctx context.Context, fileIDs []int) error {
|
||||
|
||||
// ListFolder returns the Documents module listing for a folder id
|
||||
// (GET /api/2.0/files/{folderId}).
|
||||
//
|
||||
// Deprecated: use FileStore.List via Client.Files()/Client.FileStore.
|
||||
func (c *Client) ListFolder(ctx context.Context, folderID string) (map[string]any, error) {
|
||||
if folderID == "" {
|
||||
return nil, fmt.Errorf("folder id is required")
|
||||
@@ -313,6 +321,8 @@ func (c *Client) CreateFolder(ctx context.Context, parentFolderID, title string)
|
||||
}
|
||||
|
||||
// MoveFiles moves file ids into destFolderID (Documents fileops/move).
|
||||
//
|
||||
// Deprecated: use FileStore.Move via Client.Files()/Client.FileStore.
|
||||
func (c *Client) MoveFiles(ctx context.Context, destFolderID int, fileIDs []int) (map[string]any, error) {
|
||||
if destFolderID == 0 || len(fileIDs) == 0 {
|
||||
return nil, fmt.Errorf("dest folder and file ids are required")
|
||||
@@ -343,6 +353,8 @@ func (c *Client) MoveFiles(ctx context.Context, destFolderID int, fileIDs []int)
|
||||
}
|
||||
|
||||
// UploadToFolder uploads a local file into an arbitrary Documents folder id.
|
||||
//
|
||||
// Deprecated: use FileStore.Upload via Client.Files()/Client.FileStore.
|
||||
func (c *Client) UploadToFolder(ctx context.Context, folderID, localPath string) (*FileEntry, error) {
|
||||
if folderID == "" || localPath == "" {
|
||||
return nil, fmt.Errorf("folder id and local path are required")
|
||||
@@ -400,6 +412,8 @@ func FileFolderID(f *FileEntry) string {
|
||||
// as API calls. Writes into dst. When the portal serves the file from its stale
|
||||
// AWS S3 consumer, the bytes are fetched from the local MinIO store instead
|
||||
// (see storage_fallback.go).
|
||||
//
|
||||
// Deprecated: use FileStore.Download via Client.Files()/Client.FileStore.
|
||||
func (c *Client) DownloadFile(ctx context.Context, fileID string, dst io.Writer) (int64, error) {
|
||||
f, err := c.GetFile(ctx, fileID)
|
||||
if err != nil {
|
||||
|
||||
@@ -52,6 +52,8 @@ type DavListing struct {
|
||||
|
||||
// ListDavFolder returns the contents of a folder by id, which may be a
|
||||
// symbolic root such as "@my". For "@root" use ListDavSections.
|
||||
//
|
||||
// Deprecated: use FileStore.List via Client.Files()/Client.FileStore.
|
||||
func (c *Client) ListDavFolder(ctx context.Context, id string) (*DavListing, error) {
|
||||
raw, err := c.getJSON(ctx, "/api/2.0/files/"+url.PathEscape(id))
|
||||
if err != nil {
|
||||
@@ -111,6 +113,8 @@ func (c *Client) ListDavSections(ctx context.Context) ([]DavFolder, error) {
|
||||
}
|
||||
|
||||
// CreateDavFolder creates a folder titled title inside parentID.
|
||||
//
|
||||
// Deprecated: use FileStore.CreateFolder via Client.Files()/Client.FileStore.
|
||||
func (c *Client) CreateDavFolder(ctx context.Context, parentID, title string) (*DavFolder, error) {
|
||||
raw, err := c.postJSON(ctx, "/api/2.0/files/folder/"+url.PathEscape(parentID),
|
||||
map[string]string{"title": title})
|
||||
@@ -130,6 +134,8 @@ func (c *Client) CreateDavFolder(ctx context.Context, parentID, title string) (*
|
||||
}
|
||||
|
||||
// RenameDavFolder renames a folder.
|
||||
//
|
||||
// Deprecated: use FileStore.Rename via Client.Files()/Client.FileStore.
|
||||
func (c *Client) RenameDavFolder(ctx context.Context, id, title string) error {
|
||||
_, err := c.putJSON(ctx, "/api/2.0/files/folder/"+url.PathEscape(id),
|
||||
map[string]string{"title": title})
|
||||
@@ -137,6 +143,8 @@ func (c *Client) RenameDavFolder(ctx context.Context, id, title string) error {
|
||||
}
|
||||
|
||||
// RenameDavFile renames a file (title includes the extension).
|
||||
//
|
||||
// Deprecated: use FileStore.Rename via Client.Files()/Client.FileStore.
|
||||
func (c *Client) RenameDavFile(ctx context.Context, id, title string) error {
|
||||
_, err := c.putJSON(ctx, "/api/2.0/files/file/"+url.PathEscape(id),
|
||||
map[string]string{"title": title})
|
||||
@@ -147,6 +155,8 @@ func (c *Client) RenameDavFile(ctx context.Context, id, title string) error {
|
||||
// The fileops API answers 200 with per-operation error strings even when
|
||||
// nothing moves (e.g. missing permission), so the response is parsed and the
|
||||
// first operation error is returned instead of a silent nil.
|
||||
//
|
||||
// Deprecated: use FileStore.Move via Client.Files()/Client.FileStore.
|
||||
func (c *Client) MoveDavItems(ctx context.Context, folderIDs, fileIDs []string, destFolderID string) error {
|
||||
raw, err := c.putJSON(ctx, "/api/2.0/files/fileops/move", map[string]any{
|
||||
"folderIds": nums(folderIDs),
|
||||
@@ -163,6 +173,8 @@ func (c *Client) MoveDavItems(ctx context.Context, folderIDs, fileIDs []string,
|
||||
|
||||
// CopyDavItems copies the given folders and/or files into destFolderID.
|
||||
// Per-operation errors are surfaced like in MoveDavItems.
|
||||
//
|
||||
// Deprecated: use FileStore.Copy via Client.Files()/Client.FileStore.
|
||||
func (c *Client) CopyDavItems(ctx context.Context, folderIDs, fileIDs []string, destFolderID string) error {
|
||||
raw, err := c.putJSON(ctx, "/api/2.0/files/fileops/copy", map[string]any{
|
||||
"folderIds": nums(folderIDs),
|
||||
@@ -226,6 +238,8 @@ func fileopsError(raw json.RawMessage) error {
|
||||
}
|
||||
|
||||
// DeleteDavItems deletes the given folders and/or files.
|
||||
//
|
||||
// Deprecated: use FileStore.Delete via Client.Files()/Client.FileStore.
|
||||
func (c *Client) DeleteDavItems(ctx context.Context, folderIDs, fileIDs []string) error {
|
||||
body := map[string]any{"DeleteAfter": true, "Immediately": true}
|
||||
for _, id := range folderIDs {
|
||||
@@ -242,6 +256,8 @@ func (c *Client) DeleteDavItems(ctx context.Context, folderIDs, fileIDs []string
|
||||
}
|
||||
|
||||
// UploadDavFile uploads src (fileName) into folderID, streaming from src.
|
||||
//
|
||||
// Deprecated: use FileStore.Upload via Client.Files()/Client.FileStore.
|
||||
func (c *Client) UploadDavFile(ctx context.Context, folderID, fileName string, src io.Reader) (*DavFile, error) {
|
||||
raw, err := c.uploadReader(ctx, "/api/2.0/files/"+url.PathEscape(folderID)+"/upload", "file", fileName, src)
|
||||
if err != nil {
|
||||
@@ -261,6 +277,8 @@ func (c *Client) UploadDavFile(ctx context.Context, folderID, fileName string, s
|
||||
|
||||
// DownloadDavFile streams the file identified by id to w, returning bytes
|
||||
// copied. It shares the MinIO stale-S3 fallback with DownloadFile.
|
||||
//
|
||||
// Deprecated: use FileStore.Download via Client.Files()/Client.FileStore.
|
||||
func (c *Client) DownloadDavFile(ctx context.Context, id string, w io.Writer) (int64, error) {
|
||||
return c.DownloadFile(ctx, id, w)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user