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
200 lines
5.0 KiB
Go
200 lines
5.0 KiB
Go
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))
|
|
}
|