package onlyoffice // Canonical file model and the backend-agnostic store interface. REST // (files.go), WebDAV (files_webdav.go) and future backends (PostgreSQL, // Elasticsearch) implement FileStore/Searcher so callers stop depending on a // concrete transport. This file holds only types and pure conversions — no IO. import ( "context" "io" "mime" "path/filepath" "strconv" "strings" "time" ) // Kind distinguishes files from folders in the canonical model. type Kind int const ( File Kind = iota Folder ) // String renders the kind for logs and table output. func (k Kind) String() string { switch k { case File: return "file" case Folder: return "folder" default: return "unknown" } } // Provider names for the FileStore adapters. const ( ProviderREST = "rest" ProviderDAV = "dav" ) // Entry is the backend-independent representation of a document or folder. // Fields that a backend cannot supply stay at their zero value. type Entry struct { ID string ParentID string Title string Kind Kind Size int64 MIME string Created time.Time Modified time.Time Version int Provider string } // FileStore is the operation surface every file backend implements. type FileStore interface { Name() string List(ctx context.Context, parentID string) ([]Entry, error) Stat(ctx context.Context, id string) (Entry, error) CreateFolder(ctx context.Context, parentID, title string) (Entry, error) Upload(ctx context.Context, parentID, title string, r io.Reader) (Entry, error) Download(ctx context.Context, id string, w io.Writer) (int64, error) Move(ctx context.Context, ids []string, parentID string) error Copy(ctx context.Context, ids []string, parentID string) error Rename(ctx context.Context, id, title string) error Delete(ctx context.Context, ids []string) error } // SearchQuery narrows a Searcher request. InContent asks the backend to match // document bodies, not just titles. type SearchQuery struct { Text string InContent bool FolderID string Extensions []string Limit int } // SearchHit is one Searcher result: the matching entry plus backend-specific // ranking metadata. type SearchHit struct { Entry Score float64 Highlight string Path []string } // Searcher is the optional content/name search surface. Only some backends // (for example Elasticsearch) provide it. type Searcher interface { Search(ctx context.Context, q SearchQuery) ([]SearchHit, error) Name() string } // 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. func (c *Client) FileStore(backend string) FileStore { switch strings.ToLower(strings.TrimSpace(backend)) { case ProviderDAV, "webdav": return &davStore{c: c} default: return &restStore{c: c} } } // Files returns the default (REST) file store. func (c *Client) Files() FileStore { return c.FileStore(ProviderREST) } // retryStoreOp runs one store operation under the shared deterministic // transient-error policy (429/502/503/504). func retryStoreOp(ctx context.Context, fn func() error) error { return DoRetry(ctx, DefaultRetryPolicy(), fn) } // FileEntryToEntry converts a Files-module file row to the canonical model. func FileEntryToEntry(f *FileEntry, provider string) Entry { e := Entry{Kind: File, Provider: provider} if f == nil { return e } if f.ID != nil { e.ID = f.ID.String() } e.ParentID = FileFolderID(f) if f.Title != nil { e.Title = *f.Title } if f.ContentLength != nil { e.Size = parseContentLength(*f.ContentLength) } exst := "" if f.FileExst != nil { exst = *f.FileExst } e.MIME = mimeForTitle(e.Title, exst) if f.Updated != nil { e.Modified = *f.Updated } return e } // DavFileToEntry converts a WebDAV file row to the canonical model. func DavFileToEntry(f DavFile, provider string) Entry { return Entry{ ID: f.ID, Title: f.Title, Kind: File, Size: f.Size, MIME: mimeForTitle(f.Title, ""), Modified: f.ModTime(), Provider: provider, } } // 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, } } // parseContentLength reads the leading integer of an OnlyOffice contentLength // string (the API sometimes appends a unit, e.g. "12345 b"). func parseContentLength(s string) int64 { fields := strings.Fields(s) if len(fields) == 0 { return 0 } n, err := strconv.ParseInt(fields[0], 10, 64) if err != nil { return 0 } return n } // mimeForTitle derives a MIME type from an explicit extension or the title. func mimeForTitle(title, exst string) string { ext := strings.TrimSpace(exst) if ext == "" { ext = filepath.Ext(title) } if ext == "" { return "" } if !strings.HasPrefix(ext, ".") { ext = "." + ext } return mime.TypeByExtension(strings.ToLower(ext)) }