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 stable) (pull_request) Successful in 1m19s
Tests / Test (Go 1.25) (pull_request) Successful in 1m24s
Keep the public tree project-generic. Business/one-off tools, deployment and business docs move to the private oo-workspace repo. Moved to oo-workspace: - cmd/ooscan, cmd/pdfamount, cmd/kontoblatt, cmd/kontolink - internal/xlspipe (cutover-portugal workbook) -> oow workbook build (drops the --template/--title flags from oo docs put-xlsx) - deploy/docker-compose.rclone-webdav.yml + docs/rclone-webdav.md - docs/crm-associations.md Removed GitHub-era leftovers: - .github/workflows/release-please.yml, release-please-config.json, .release-please-manifest.json (tags are created on Gitea per SemVer) Naming: the FileStore subsystem is now filestore_*.go (was file_*.go) to match files_*.go (project Documents). Docs/AGENTS/README updated.
205 lines
9.4 KiB
Markdown
205 lines
9.4 KiB
Markdown
---
|
||
type: reference
|
||
status: current
|
||
related:
|
||
- README.md
|
||
- filestore_core.go
|
||
- filestore_facade.go
|
||
- docs/elasticsearch.md
|
||
- docs/community-server-db.md
|
||
---
|
||
|
||
# Unified file client — контракт файловых бэкендов
|
||
|
||
## Что это
|
||
|
||
Один файловый клиент на все бэкенды (эпик #34). Модель и интерфейсы —
|
||
`filestore_core.go`. Фасад `FileClient` — `filestore_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` (`filestore_es_text.go`) — свой индекс: `Put`, `Delete`, `Search`,
|
||
`Name`. `ESTextIndex` реализует и `Searcher`, и `TextIndex`.
|
||
|
||
## Бэкенды
|
||
|
||
| бэкенд | провайдер | файл | что умеет |
|
||
|--------|-----------|------|-----------|
|
||
| REST | `rest` | `filestore_rest.go` | read + write, Documents API |
|
||
| WebDAV | `dav` | `filestore_dav.go` | read + write, Documents fileops |
|
||
| SQL | `postgres` / `mysql` | `filestore_pg.go` | **read-only** |
|
||
| OnlyOffice ES | `elasticsearch` | `filestore_es.go` | поиск (имя + контент Office) |
|
||
| свой ES-индекс | `es-text` | `filestore_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` → `mysql` → `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
|
||
sql, err := c.SQLFileStore() // открыть из env (ONLYOFFICE_DSN)
|
||
if err != nil { /* нет DSN */ }
|
||
if closer, ok := sql.(interface{ Close() error }); ok { defer closer.Close() }
|
||
|
||
f := c.Files()
|
||
f.RegisterStore(onlyoffice.ProviderPG, sql) // или sql.Name() == "mysql"
|
||
e, _ := f.Stat(ctx, "19423") // e.Provider == "mysql"
|
||
entries, _ := f.List(ctx, "676") // пойдёт в SQL
|
||
```
|
||
|
||
`Client.FileStore("pg"|"sql"|"postgres"|"mysql")` — одноразовый доступ к
|
||
SQL-стору без фасада: открывает из env; при ошибке возвращает заглушку,
|
||
которая отдаёт ошибку открытия на каждом вызове (не `nil`). `SQLFileStore()`
|
||
— тот же открыватель, но с ошибкой. Отвечавший бэкенд видно по
|
||
`Entry.Provider` (`mysql` / `postgres` у SQL, `rest` у REST).
|
||
|
||
## 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'
|
||
```
|
||
|
||
## См. также
|
||
|
||
- [README.md](README.md) — индекс справочников.
|
||
- [elasticsearch.md](elasticsearch.md) — индекс OnlyOffice и свой `oo_docs_text`.
|
||
- [community-server-db.md](community-server-db.md) — SQL-стор и схема БД.
|