docs(files): add unified file client contract (#39)
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 17s
Tests / Test (Go stable) (pull_request) Successful in 1m20s
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 17s
Tests / Test (Go stable) (pull_request) Successful in 1m20s
This commit is contained in:
@@ -10,11 +10,12 @@ Canonical Go client for OnlyOffice Workspace (Projects + Calendar + CRM) and the
|
||||
- `auth.go` — `Authenticate`, `AuthenticateContext`, `InvalidateToken`, `Auth`, token lifecycle.
|
||||
- `http.go` — transport + DRY response decoders (`ResponseArray`/`ResponseObject`/`postFormObject`/`putFormObject`/`deleteObject`).
|
||||
- `projects.go`, `tasks.go`, `users.go`, `calendar.go`, `crm.go`, `files.go`, `files_webdav.go`, `files_stem.go`, `retry.go`, `mails.go`, `invoices.go` — typed / untyped domain methods. **`files.go`** — CRM opportunity upload plus **project/task Documents** (`UpdateFile`, `UploadToFolderReplacing`). **`files_webdav.go`** — Documents module by id (`ListDavFolder`, `MoveDavItems`/`CopyDavItems` with per-operation error surfacing, `ListFileOps`). **`retry.go`** — `DoRetry`: deterministic linear backoff (no jitter) on 429/502/503/504; every bulk tool routes API calls through it. **`mails.go`** — OnlyOffice Workspace Mail. **`invoices.go`** — CRM invoices, PDF regen/cleanup, status. Association rules: [`docs/crm-associations.md`](docs/crm-associations.md).
|
||||
- **Unified file client (epic #34) — `file_core.go`, `file_rest.go`, `file_dav.go`, `file_pg.go`, `file_es.go`, `file_es_text.go`, `file_text_index.go`, `file_facade.go`.** `file_core.go` — model (`Entry`, `Kind`) + `FileStore`/`Searcher`; `file_rest.go`/`file_dav.go` — REST/WebDAV adapters; `file_pg.go` — **read-only** SQL store (PostgreSQL/MySQL, `ErrReadOnly` on writes); `file_es.go` — OnlyOffice Elasticsearch searcher; `file_es_text.go`/`file_text_index.go` — own PDF/scan index (`oo_docs_text`, PDF attachments via pdfdetach); `file_facade.go` — `FileClient` with read/write/search order and transient fallback. Use `c.Files()` (facade) or `c.FileStore("rest"|"dav")`; contract and how to add a backend: [`docs/unified-file-client.md`](docs/unified-file-client.md).
|
||||
- Pure stdlib + `google/go-querystring`; no UI, no dotenv.
|
||||
- **CLI — `cmd/oo/` as `package main`.** Cobra wrapper that loads `.env` via `godotenv` at startup. **Subject-based command tree** mirroring [`tea`](https://gitea.com/gitea/tea):
|
||||
- `main.go` — entry point (docstring lists the command tree).
|
||||
- `common.go` — `rootCmd`, `newOO`, `printTable`/`printObject`, `--output table|json` flag.
|
||||
- `calendar.go`, `projects.go`, `projects_files.go`, `tasks.go`, `tasks_files.go`, `users.go`, `contacts.go`, `opportunities.go`, `cases.go`, `crm.go`, `crm_tasks.go`, `catalog.go`, `docs.go`, `dav.go`, `mails.go`, `invoices.go` — one file per subject (or per subject facet), each registers in `init()`. `dav.go` exposes the Documents module by id (`oo dav ls|move|copy|mkdir|rename-file|rename-folder|download|fileops`).
|
||||
- `calendar.go`, `projects.go`, `projects_files.go`, `tasks.go`, `tasks_files.go`, `users.go`, `contacts.go`, `opportunities.go`, `cases.go`, `crm.go`, `crm_tasks.go`, `catalog.go`, `docs.go`, `dav.go`, `search.go`, `index.go`, `mails.go`, `invoices.go` — one file per subject (or per subject facet), each registers in `init()`. `dav.go` exposes the Documents module by id (`oo dav ls|move|copy|mkdir|rename-file|rename-folder|download|fileops`); `search.go` runs `oo search QUERY` (name/content, `--backend oo|own`); `index.go` fills the own full-text index (`oo index folder|files`, see [`docs/unified-file-client.md`](docs/unified-file-client.md)).
|
||||
- CLI-only deps (`spf13/cobra`, `joho/godotenv`) stay out of the library.
|
||||
- **TUI — `cmd/office/` as `package main`.** Bubble Tea three-pane browser (module tree, selectable list, markdown preview). Reuses `cmd/internal/bootstrap` for env/auth and the root `onlyoffice` library for all API calls. UI logic in `cmd/office/ui/`; preview/formatting in `cmd/office/preview/`; list loaders in `cmd/office/fetch/`.
|
||||
- **List table (`DataTable`)** — `cmd/office/ui/table*.go`. Column layout policies live in `cmd/office/model/table_layout.go` (`TableFlexLayoutFor`); cell rendering uses the bubbles/table inline pattern in `table_render.go` (`renderTableCell`, `padANSIWidth`). See `.cursor/skills/office-tui-table/SKILL.md` before changing center-pane tables.
|
||||
|
||||
@@ -678,7 +678,7 @@ oo dav download 22881 --to ./copy.pdf # default path: ./<server title>
|
||||
oo dav fileops # active move/copy operations (status polling)
|
||||
```
|
||||
|
||||
### Search (`oo search`)
|
||||
### Search and index (`oo search`, `oo index`)
|
||||
|
||||
Full-text search over the Documents index. The REST endpoint
|
||||
`/api/2.0/files/@search/{query}` only searches file names in the database, so
|
||||
@@ -718,6 +718,16 @@ oo search "Rechnung" --backend own --folder 634 --json
|
||||
See [`docs/elasticsearch.md`](docs/elasticsearch.md) for the decision and
|
||||
trade-offs.
|
||||
|
||||
### Unified file client
|
||||
|
||||
All file backends (REST, WebDAV, read-only SQL, Elasticsearch) sit behind one
|
||||
facade: `c.Files()` returns a `*FileClient` that also implements `FileStore`,
|
||||
so old call sites keep working. Pick a transport per call with
|
||||
`c.FileStore("rest"|"dav")`, or register a backend on the facade
|
||||
(`RegisterStore`/`RegisterSearcher`). Contract, model (`Entry`/`Kind`),
|
||||
fallback rules, env names and how to add a backend:
|
||||
[`docs/unified-file-client.md`](docs/unified-file-client.md).
|
||||
|
||||
### Bulk tools (`cmd/`)
|
||||
|
||||
Small single-purpose binaries for bulk Documents work. All of them pace
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
type: reference
|
||||
status: current
|
||||
related:
|
||||
- README.md
|
||||
- file_core.go
|
||||
- file_facade.go
|
||||
- docs/elasticsearch.md
|
||||
- docs/community-server-db.md
|
||||
---
|
||||
|
||||
# Unified file client — контракт файловых бэкендов
|
||||
|
||||
## Что это
|
||||
|
||||
Один файловый клиент на все бэкенды (эпик #34). Модель и интерфейсы —
|
||||
`file_core.go`. Фасад `FileClient` — `file_facade.go`. Бэкенды:
|
||||
REST, WebDAV, SQL (PostgreSQL/MySQL), Elasticsearch. Правило одно:
|
||||
код зовёт `c.Files()` и не знает про транспорт.
|
||||
|
||||
## Модель
|
||||
|
||||
- `Kind` — `File` (0) или `Folder` (1).
|
||||
- `Entry` — бэкенд-независимая строка: `ID`, `ParentID`, `Title`, `Kind`,
|
||||
`Size`, `MIME`, `Created`, `Modified`, `Updated` (сырая строка API),
|
||||
`Version`, `Provider`, `FilesCount`/`FoldersCount` (папки).
|
||||
Чего бэкенд не даёт — остаётся в нуле.
|
||||
- `SearchQuery` — `Text`, `InContent`, `FolderID`, `Extensions`, `Limit`.
|
||||
- `SearchHit` — `Entry` + `Score`, `Highlight`, `Path`.
|
||||
|
||||
## Интерфейсы
|
||||
|
||||
`FileStore` — операции с файлами:
|
||||
|
||||
```go
|
||||
type FileStore interface {
|
||||
Name() string
|
||||
List(ctx, parentID) ([]Entry, error)
|
||||
Stat(ctx, id) (Entry, error)
|
||||
CreateFolder(ctx, parentID, title) (Entry, error)
|
||||
Upload(ctx, parentID, title, r) (Entry, error)
|
||||
Download(ctx, id, w) (int64, error)
|
||||
Move(ctx, ids, parentID) error
|
||||
Copy(ctx, ids, parentID) error
|
||||
Rename(ctx, id, title) error
|
||||
Delete(ctx, ids) error
|
||||
}
|
||||
```
|
||||
|
||||
`Searcher` — поиск (необязательный):
|
||||
|
||||
```go
|
||||
type Searcher interface {
|
||||
Search(ctx, q SearchQuery) ([]SearchHit, error)
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
|
||||
`TextIndex` (`file_es_text.go`) — свой индекс: `Put`, `Delete`, `Search`,
|
||||
`Name`. `ESTextIndex` реализует и `Searcher`, и `TextIndex`.
|
||||
|
||||
## Бэкенды
|
||||
|
||||
| бэкенд | провайдер | файл | что умеет |
|
||||
|--------|-----------|------|-----------|
|
||||
| REST | `rest` | `file_rest.go` | read + write, Documents API |
|
||||
| WebDAV | `dav` | `file_dav.go` | read + write, Documents fileops |
|
||||
| SQL | `postgres` / `mysql` | `file_pg.go` | **read-only** |
|
||||
| OnlyOffice ES | `elasticsearch` | `file_es.go` | поиск (имя + контент Office) |
|
||||
| свой ES-индекс | `es-text` | `file_es_text.go` | поиск + запись (PDF/сканы) |
|
||||
|
||||
- REST: `Stat` знает только файлы; папки — через `List`.
|
||||
- WebDAV: `Move`/`Copy`/`Delete` сперва `Stat`-ят id (папка/файл), потом зовут
|
||||
fileops.
|
||||
- SQL: `List`/`Stat`/`Download`/`Search` (по имени). Все write-методы →
|
||||
`ErrReadOnly`. `Download` идёт в S3/MinIO по layout портала.
|
||||
- OnlyOffice ES: индекс `files_file`, контент только для docx/xlsx/pptx.
|
||||
- Свой ES: индекс `oo_docs_text`, контент из `internal/docpipe`, в т.ч.
|
||||
встроенные PDF-вложения.
|
||||
|
||||
## Фасад `FileClient`
|
||||
|
||||
`c.Files()` → `*FileClient`. Он же реализует `FileStore`, старый код
|
||||
компилируется.
|
||||
|
||||
- `Read()` — первый зарегистрированный из `readOrder`:
|
||||
`postgres` → `rest` → `dav`.
|
||||
- `Write()` — первый из `writeOrder`: `rest` → `dav`. SQL не пишет.
|
||||
- `Search()` — первый из `searchOrder`: `elasticsearch`. Нет бэкенда →
|
||||
ошибка (`ONLYOFFICE_ES_URL`).
|
||||
- `RegisterStore(name, s)` / `RegisterSearcher(name, s)` — добавить бэкенд.
|
||||
|
||||
Fallback:
|
||||
|
||||
- `List`/`Stat` идут по `readOrder`; переходят к следующему только на
|
||||
transient-ошибке (429/502/503/504). Иначе ошибка финальная.
|
||||
- `Download` **без** fallback: часть байтов уже в `w`, второй бэкенд допишет.
|
||||
- Запись (`CreateFolder`/`Upload`/`Move`/`Copy`/`Rename`/`Delete`) — только
|
||||
`Write()`, без fallback.
|
||||
|
||||
`newFileClient` сам кладёт `rest` и `dav`; ES-поиск — если задан
|
||||
`ONLYOFFICE_ES_URL`. SQL-стор регистрирует вызывающий: фасад создаётся на
|
||||
каждый `c.Files()`, регистрируй на том же экземпляре.
|
||||
|
||||
```go
|
||||
f := c.Files()
|
||||
if pg, err := onlyoffice.NewPGStore(onlyoffice.PGConfigFromEnv()); err == nil {
|
||||
defer pg.Close()
|
||||
f.RegisterStore(onlyoffice.ProviderPG, pg)
|
||||
}
|
||||
entries, _ := f.List(ctx, "649") // пойдёт в SQL
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
# поиск: --backend oo (индекс OnlyOffice) | own (свой oo_docs_text)
|
||||
oo search "Rechnung"
|
||||
oo search "Mahngebühr" --content
|
||||
oo search "S1021" --content --backend own --folder 634 --limit 50 --json
|
||||
|
||||
# наполнение своего индекса (PDF/сканы, idempotent upsert по file id)
|
||||
oo index folder 634
|
||||
oo index folder 634 --recursive --exts pdf,png --limit 100
|
||||
oo index files 3576 3578
|
||||
oo index folder 634 --dry-run
|
||||
```
|
||||
|
||||
`oo index` флаги: `--recursive`, `--exts` (default `pdf`), `--limit`,
|
||||
`--workers` (3), `--lang` (`deu+eng`), `--min-chars`, `--work-dir`,
|
||||
`--backend rest|dav`, `--dry-run`, `--json`.
|
||||
|
||||
Библиотека:
|
||||
|
||||
```go
|
||||
idx, _ := onlyoffice.NewESTextIndex(onlyoffice.ESTextConfigFromEnv())
|
||||
ti := onlyoffice.NewTextIndexer(store, idx) // store = FileStore
|
||||
res, _ := ti.IndexFolder(ctx, "634", onlyoffice.IndexOptions{Recursive: true})
|
||||
```
|
||||
|
||||
## Env (только имена)
|
||||
|
||||
| env | default | кто читает |
|
||||
|-----|---------|------------|
|
||||
| `ONLYOFFICE_ES_URL` | — | ES (оба индекса), обязателен |
|
||||
| `ONLYOFFICE_ES_INDEX` | `files_file` | индекс OnlyOffice |
|
||||
| `ONLYOFFICE_ES_TEXT_INDEX` | `oo_docs_text` | свой индекс |
|
||||
| `ONLYOFFICE_TENANT` | пусто | фильтр `tenantId` |
|
||||
| `ONLYOFFICE_DSN` | — | SQL DSN (MySQL/PostgreSQL) |
|
||||
| `ONLYOFFICE_PG_DRIVER` | auto | `postgres` / `mysql` |
|
||||
| `ONLYOFFICE_PG_TENANT` | `ONLYOFFICE_TENANT` | SQL tenant |
|
||||
| `ONLYOFFICE_PG_HOST` `_PORT` `_USER` `_PASSWORD` `_DBNAME` `_SSLMODE` | — | DSN по частям |
|
||||
| `MINIO_ENDPOINT` `MINIO_BUCKET` `MINIO_ACCESS_KEY` `MINIO_SECRET_KEY` | — | download SQL-стора |
|
||||
| `OO_URL` `OO_USER` `OO_PASS` | — | CLI-алиасы |
|
||||
|
||||
Имена — в [`.env.example`](../.env.example). Секретов в репо нет.
|
||||
|
||||
## Ограничения
|
||||
|
||||
- OnlyOffice ES: контент только Office-форматов. PDF — только по имени.
|
||||
Встроенные вложения PDF сервер не индексирует.
|
||||
- Свой индекс `oo_docs_text`: покрывает PDF/сканы и вложения (pdfdetach), но
|
||||
наполняется вручную (`oo index`) и идемпотентен. Фильтр `folder` — id папки,
|
||||
не путь. Дубли дают несколько строк — дедуп на потребителе.
|
||||
- SQL: read-only. `InContent` игнорируется (только имя). Download — через
|
||||
MinIO-схему, не HTTP.
|
||||
- ES: без auth, слушает localhost внутри VM — нужен SSH-туннель
|
||||
(см. [elasticsearch.md](elasticsearch.md)).
|
||||
- `oo index` качает каждый файл и для сканов гоняет OCR — медленно; отсюда
|
||||
`--limit` и `--exts`. Нужен `pdfdetach` (poppler); без него — только тело PDF.
|
||||
|
||||
## Как добавить бэкенд
|
||||
|
||||
1. Файл `file_<name>.go`. Реализуй `FileStore` (`Name` + 9 методов). Нужен
|
||||
поиск — добавь `Searcher`; нужна запись своего индекса — `TextIndex`.
|
||||
2. Добавь const провайдера рядом с `ProviderREST`/`ProviderDAV`.
|
||||
3. Зарегистрируй: в `newFileClient` или снаружи через
|
||||
`RegisterStore`/`RegisterSearcher`.
|
||||
4. Внеси имя в `readOrder` / `writeOrder` / `searchOrder`.
|
||||
5. Есть CLI-команда — добавь значение в `--backend`.
|
||||
6. Тесты: unit (чистые builders/парсеры, без сети) + интеграционный
|
||||
(`//go:build integration`, skip без кред).
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
go test ./... # unit, без сети
|
||||
go test -tags=integration ./... # live (креды в .env)
|
||||
go test ./ -run 'FileStore|Facade|ESText|PG'
|
||||
```
|
||||
|
||||
## См. также
|
||||
|
||||
- [elasticsearch.md](elasticsearch.md) — индекс OnlyOffice и свой `oo_docs_text`.
|
||||
- [community-server-db.md](community-server-db.md) — SQL-стор и схема БД.
|
||||
Reference in New Issue
Block a user