chore: move business tooling out of the public library; tidy filestore naming
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.
This commit is contained in:
2026-09-23 09:05:40 +01:00
parent a9a77e110d
commit dc62570a5c
46 changed files with 69 additions and 2390 deletions
+1 -4
View File
@@ -18,10 +18,7 @@ related:
- [elasticsearch.md](elasticsearch.md) — поиск: индекс OnlyOffice `files_file`
и свой `oo_docs_text` (PDF/сканы), туннель.
- [index-and-search.md](index-and-search.md) — карта контуров поиска и как
обновлять индексы (`oo index`, `ooscan`/`pdfamount` для match).
- [rclone-webdav.md](rclone-webdav.md) — rclone-монтирование Documents
(`deploy/docker-compose.rclone-webdav.yml`), smoke, ограничения.
- [crm-associations.md](crm-associations.md) — правила ассоциаций CRM.
обновлять индексы (`oo index`, `oo search`).
- [rate-limiting.md](rate-limiting.md) — rate limit, exponential backoff,
`Retry-After`, общий cooldown против 429; env `OO_RATE_LIMIT`/`OO_BURST`/
`OO_RETRY_*`.
+2 -2
View File
@@ -3,7 +3,7 @@ type: reference
status: current
related:
- README.md
- file_pg.go
- filestore_pg.go
- docs/elasticsearch.md
---
@@ -11,7 +11,7 @@ related:
## Что это
Бэкенд `pgStore` (`file_pg.go`) читает файлы и папки **напрямую из БД
Бэкенд `pgStore` (`filestore_pg.go`) читает файлы и папки **напрямую из БД
Community Server**, без HTTP-слоя. Реализует `FileStore` (`List`/`Stat`/
`Download`) и `Searcher` по имени. Запись запрещена: все write-методы
возвращают `ErrReadOnly`.
-124
View File
@@ -1,124 +0,0 @@
# CRM associations (company ↔ person ↔ deal ↔ project ↔ invoice ↔ mail)
Operational rules for the `oo` CLI and this library. Business SSOT remains
OnlyOffice Workspace CRM + Projects.
## Canonical graph
One **legal company** owns the relationship. Do not invent a second “bill-to”
company just for PDF layout.
```text
Company
├── Person (buyer contact) oo persons create --company-id
├── Opportunity / Deal oo opportunities … ; member-add company + person
├── Project (hub) oo projects … ; contacts add company + person
│ └── Epic + subtasks
└── Invoice (Draft → …) oo invoices create --contact COMPANY --opportunity DEAL
└── PDF file oo invoices pdf ID
└── Mail draft oo mails draft-invoice --invoice ID --to …
```
| Layer | CLI | Must link |
|-------|-----|-----------|
| Company | `oo companies create` | website, email, phone, **one** Billing address |
| Person | `oo persons create --company-id` / `oo persons update ID` | job title; never encode employer in `lastName`; **update uses JSON** (form PUT ignores `companyId`/`about`) |
| Deal | `oo opportunities create` + `member-add` | company **and** person as members |
| Project | `oo projects create` + `contacts add` | same company + person |
| Invoice | `oo invoices create --contact COMPANY --opportunity DEAL` | `entityId` at **create** |
| Mail | `oo mails draft-invoice` | attach current PDF; **do not send** until confirmed |
UI checks (same company card):
- `#contacts` → person
- `#deals` → opportunity
- `#projects` → hub project
- `#invoices` on the **deal** → invoice (needs `entity`)
- `#files` → preferably **one** current invoice PDF
**Project Team ≠ Project Contacts.** Team = portal users. CRM people/companies
show under the project **Contacts** tab (`oo projects contacts list`).
## Hard rules
1. **One company per legal entity.** Duplicate “bill-to” contacts empty Deals /
Projects / Contacts tabs and break merge. Prefer
`oo contacts merge FROM INTO` (keeps `INTO`) or `oo companies dedupe`.
2. **Link invoice → deal at create.**
`POST /crm/invoice` with `entityId` + `entityType: 0` (Opportunity).
`oo invoices update … --opportunity` often returns **400**
(“Value does not fall within the expected range”). If the link is missing,
delete the Draft and recreate with `--opportunity`.
3. **Bill To = company id**, not a throwaway contact. Person stays under the
company (`companyId`). Optional `consigneeId` for Empfänger when the portal
template prints it.
4. **Stay Draft until mail is ready.** Billed (`status id=2`) is **not editable**
via content PUT. Going Billed → Draft via `…/crm/invoice/status/1` usually
**does not work** — delete + recreate Draft instead.
5. **Do not regenerate PDF in a loop** without cleanup. Each
`GET …/crm/invoice/{id}/pdf` attaches a new file to the company (and often
the deal). Keep `invoice.fileID`; delete older PDFs with
`oo invoices pdf-cleanup ID` / Documents `fileops/delete`.
## Invoice PDF quirks
| Symptom | Workaround |
|---------|------------|
| Cached / stale PDF | Touch invoice (Draft PUT that clears `fileID`), then `GET …/pdf` — `oo invoices pdf ID --force` |
| Billing address missing on **new** PDFs | Temporary multiline `companyName` (`Line1\nLine2\n…`) on the **canonical** company → force PDF → restore clean name. Cached `fileID` keeps the multiline Bill To. |
| Separate bill-to company for newlines | **Forbidden** — merge back to the real company |
| Invoice **number** won’t change on PUT | Delete Draft and recreate with the desired number |
| Notizen / Bedingungen spacing | Leading `\n` and blank lines only — no HTML (tags print literally) |
| Issuer street lines | Organisation profile address (`street` with `\n`), not only terms |
Status ids commonly used: `1` Draft, `2` Billed, `3` Rejected, `4` Paid.
## Mail quirks
| Symptom | Workaround |
|---------|------------|
| Signature / body doubles chat URL | Put chat in **one** place only. UI drafts: signature. API send: body (API **does not** append signature). |
| Signature / body cuts URL at `#` | Plain text URLs — avoid `<a href="…#…">` (or encode `#` as `%23` in href) |
| German letter spacing | Blank `<p>&nbsp;</p>` between blocks (`MailHTMLWithBlankParagraphs`) |
| Send | `PUT /api/2.0/mail/messages/send.json` with `id/from/to/subject/body`; omit empty `cc`/`bcc`. Never auto-send; draft only until the human confirms |
Prefer OnlyOffice Mail (`/addons/mail/#drafts`) for invoice delivery until confirmed.
## Project / task quirks
- Hub title: `CC | Company` (e.g. `DE | Acme GmbH`).
- Streams = epics/tasks under the hub, not a third title segment (unless the
project itself is a named delivery stream).
- Closing a **subtask**:
`PUT /api/2.0/project/task/{epicId}/{subtaskId}/status` with `status=2`.
`oo tasks update SUBTASK -s closed` returns **404** for subtasks.
- After deleting a CRM contact, `GET /project/contact/{deletedId}` may still
return projects (ghost). Official project contact list should only show live
ids; unlink may 400 if the contact is gone.
## Merge / cleanup cheat sheet
```bash
# Keep the preferred company (INTO), drop the duplicate (FROM)
oo contacts merge FROM_ID INTO_ID
# Or by normalized name (careful — whole CRM)
oo companies dedupe
# Invoice ↔ deal must exist at create
oo invoices create --number P-YYYY-NN --contact COMPANY_ID --item ITEM_ID \
--price 300 --opportunity DEAL_ID --language de-DE …
# Fresh PDF + prune older PDFs on company/deal
oo invoices pdf INVOICE_ID --force
oo invoices pdf-cleanup INVOICE_ID
# Mail draft (no send)
oo mails draft-invoice --invoice INVOICE_ID --to billing@example.com
```
## Related
- README § invoices / mail / CRM cleanup
- Personal workspace tooling (disk inventory, dossier sync) lives in a private
companion repo (`oo-workspace`, the `oow` CLI).
+7 -7
View File
@@ -3,7 +3,7 @@ type: reference
status: current
related:
- README.md
- file_es.go
- filestore_es.go
---
# Elasticsearch — полнотекстовый поиск OnlyOffice
@@ -85,12 +85,12 @@ oo search "Rechnung" --folder 649 --limit 50 --json
## Обновление индекса и карта поиска
Обзор всех контуров поиска и как обновлять индексы (`oo index`,
`ooscan`/`pdfamount` для match) — [index-and-search.md](index-and-search.md).
Обзор всех контуров поиска и как обновлять индексы (`oo index`) —
[index-and-search.md](index-and-search.md).
## Библиотека
`file_es.go` — `ESSearcher` (`Name() = "elasticsearch"`), прямой ES REST на
`filestore_es.go` — `ESSearcher` (`Name() = "elasticsearch"`), прямой ES REST на
stdlib `net/http`:
```go
@@ -103,7 +103,7 @@ hits, _ := es.Search(ctx, onlyoffice.SearchQuery{
Запрос: `multi_match` по `title^2` (+ `document.attachment.content` при
`InContent`), фильтры `tenantId` и `folders.folderId`, `_source`
id/title/folders, `highlight` для фрагмента. Ответ → `[]SearchHit` (модель из
эпика #34; пока объявлена в `file_es.go`, переедет в `file_core.go` с F1 #35).
эпика #34; пока объявлена в `filestore_es.go`, переедет в `filestore_core.go` с F1 #35).
## Тесты
@@ -168,11 +168,11 @@ OnlyOffice PDF лежит только по имени.
## Устройство
- `file_es_text.go` — `ESTextIndex` (`Name() = "es-text"`):
- `filestore_es_text.go` — `ESTextIndex` (`Name() = "es-text"`):
`Ensure` (создаёт индекс с явным маппингом), `Put` (bulk, `refresh`),
`Delete` (по `id`), `Search` (`multi_match` по `title^2` + `content`,
фильтры `folder`/`ext`, highlight).
- `file_text_index.go` — `TextIndexer`: листает папки (`FileStore.List`),
- `filestore_text_index.go` — `TextIndexer`: листает папки (`FileStore.List`),
качает файлы (`FileStore.Download`), извлекает текст через
`internal/docpipe` (`pdftotext`, для сканов — `ocrmypdf`/`tesseract`),
пишет в `TextIndex`. Пул воркеров (по умолчанию 3).
+10 -25
View File
@@ -17,21 +17,20 @@ related:
| REST `@search` | только имена в БД | нет | — (живой запрос) | `oo search` (по умолчанию `--backend oo`) |
| ES `files_file` | имя + текст Office | Elasticsearch портала | сервер, асинхронно | `oo search --content` |
| ES `oo_docs_text` | PDF/сканы (свой) | Elasticsearch портала | `oo index` | `oo search --backend own` |
| TSV `ooscan` | файлы папок | файл `*.tsv` | `ooscan <folder...>` | потребитель (не библиотека) |
## Карта кода
- `file_core.go` — интерфейсы `Searcher`, модели `SearchQuery`/`SearchHit`.
- `file_es.go` — `ESSearcher` (индекс OnlyOffice `files_file`).
- `file_es_text.go` — `ESTextIndex` (`oo_docs_text`): `Ensure`, `Put`, `Delete`,
- `filestore_core.go` — интерфейсы `Searcher`, модели `SearchQuery`/`SearchHit`.
- `filestore_es.go` — `ESSearcher` (индекс OnlyOffice `files_file`).
- `filestore_es_text.go` — `ESTextIndex` (`oo_docs_text`): `Ensure`, `Put`, `Delete`,
`Search`.
- `file_text_index.go` — `TextIndexer`: обход папок (`FileStore.List`), download
- `filestore_text_index.go` — `TextIndexer`: обход папок (`FileStore.List`), download
(`FileStore.Download`), извлечение текста (`internal/docpipe`), запись в
`TextIndex`; пул воркеров.
- `file_facade.go` — связка бэкендов (`Files().Search()`, порядок и fallback).
- `filestore_facade.go` — связка бэкендов (`Files().Search()`, порядок и fallback).
- CLI: `cmd/oo/search.go`, `cmd/oo/index.go`.
- Разовые бинари для match: `cmd/ooscan/` (TSV-индекс папок),
`cmd/pdfamount/` (суммы по PDF в папке).
- Разовые бинари для match (`ooscan`, `pdfamount`) живут в приватном
`oo-workspace`.
- `internal/docpipe` — текст из PDF (pdftotext), для сканов OCR
(ocrmypdf/tesseract), вложения PDF (pdfdetach).
@@ -81,24 +80,10 @@ oo index folder 649 --recursive --dry-run
не индексируй корень целиком.
- Индексация PDF в `files_file` не делается — только `oo_docs_text`.
## Bulk-инструменты `ooscan` / `pdfamount`
## Bulk-инструменты
Плоский TSV без Elasticsearch, для внешних потребителей.
```bash
# рекурсивный индекс папок: file_id, folder_id, path, title
ooscan <FOLDER_ID>... > index.tsv
# суммы по PDF папки: file_id, title, amount
pdfamount <FOLDER_ID> [TITLE_FILTER] > amounts.tsv
```
- `ooscan`: троттлинг 350 мс на папку, `DoRetry` на 429, глубина до 8, `path` —
путь внутри просканированного корня.
- `pdfamount`: строка с `%`/`MwSt`/`USt`/`Prozent`/`Steuer` суммой не считается;
приоритет меток (`zu zahlender betrag` > `rechnungsbetrag` > … > `summe`).
- Какие корни сканировать и как обновлять индекс — решает потребитель; это не
часть библиотеки. Сверка Excel — `office-assistant` (`cmd/match`,
`docs/reference/match-index.md`).
Плоские TSV-инструменты (`ooscan`, `pdfamount`) и сверка Excel живут в
приватном `oo-workspace`, не в публичной библиотеке.
## Грабли
-109
View File
@@ -1,109 +0,0 @@
---
type: reference
status: current
related:
- deploy/docker-compose.rclone-webdav.yml
- docs/unified-file-client.md
---
# rclone WebDAV mount — OnlyOffice Documents как файловая ФС
`deploy/docker-compose.rclone-webdav.yml` монтирует WebDAV-дерево OnlyOffice
через `rclone`. Не systemd, только compose. Контейнер — `rclone-webdav`.
## Что монтируется
- Источник — сайдкар `oo-webdav` (`ghcr.io/eslider/oo-webdav`).
- Адрес — `http://172.17.0.1:8098`, префикс `/webdav`.
- Basic-auth — креды портала OnlyOffice (те же, что у `oo`).
- Корень — `Dokumente der Projekte/…` (папка `Fibu EDL` внутри).
## Конфиг
- Образ — `rclone/rclone`.
- Remote — on-the-fly `:webdav:` плюс флаги `--webdav-url`, `--webdav-user`,
`--webdav-pass`.
- На старте контейнер пишет `/config/rclone/rclone.conf` (`[webdav]`), чтобы
`rclone ls webdav:` внутри контейнера работал без флагов.
- `ONLYOFFICE_PASSWORD` — plain; rclone сам зовёт `rclone obscure`.
- Флаги mount: `--vfs-cache-mode writes`, `--cache-dir /cache`,
`--dir-cache-time 1m`, `--allow-other`, `--allow-non-empty`.
- FUSE: `cap_add: [SYS_ADMIN]`, `devices: [/dev/fuse]`,
`security_opt: apparmor:unconfined`.
- Тома: `onlyoffice-mnt` → `/mnt/onlyoffice`, `rclone-cache` → `/cache`.
### env (только имена)
| Переменная | Значение |
|------------|----------|
| `ONLYOFFICE_USER` | пользователь портала |
| `ONLYOFFICE_PASSWORD` | пароль портала (алиас `ONLYOFFICE_PASS`) |
| `ONLYOFFICE_WEBDAV_URL` | по умолчанию `http://172.17.0.1:8098/webdav` |
## Команды
```bash
# креды (или .env рядом с compose)
set -a; . .secrets/oo.env; set +a
docker compose -f deploy/docker-compose.rclone-webdav.yml up -d
docker compose -f deploy/docker-compose.rclone-webdav.yml ps
docker compose -f deploy/docker-compose.rclone-webdav.yml down
# дерево
docker exec rclone-webdav rclone ls webdav:
docker exec rclone-webdav rclone lsf webdav:
# содержимое точки монтирования
docker exec rclone-webdav ls /mnt/onlyoffice
# чтение/запись как обычная ФС
docker exec rclone-webdav cat "/mnt/onlyoffice/Meine Dokumente/x.txt"
```
## Smoke (проверено 2026-09-16)
```text
$ docker exec rclone-webdav rclone ls webdav:
5357109 Meine Dokumente/ONLYOFFICE-Audiobeispiel.mp3
44805 Meine Dokumente/ONLYOFFICE-Beispiel-Tabellenblatt.xlsx
58988 Meine Dokumente/ONLYOFFICE-Beispieldokument.docx
...
$ TS=20260916-212459
$ docker exec rclone-webdav sh -c "printf 'rclone-webdav round-trip $TS\n' \
> '/mnt/onlyoffice/Meine Dokumente/rclone-smoke-$TS/hello.txt'"
# ждём появления на сервере (host HTTP, мимо монтирования):
GET /webdav/Meine%20Dokumente/rclone-smoke-$TS/hello.txt -> 200 (через 7s)
server bytes: rclone-webdav round-trip 20260916-212459
# читаем обратно через монтирование:
mount read: rclone-webdav round-trip 20260916-212459
MATCH: yes
$ docker exec rclone-webdav rm -f \
"/mnt/onlyoffice/Meine Dokumente/rclone-smoke-$TS/hello.txt"
GET /webdav/Meine%20Dokumente/rclone-smoke-$TS/hello.txt -> 404 (через 1s)
# пустой каталог: rmdir через монтирование на сервер не доходит,
# удаляем напрямую:
$ docker exec rclone-webdav rclone rmdir "webdav:Meine Dokumente/rclone-smoke-$TS"
PROPFIND rclone-smoke-20260916-212459/ -> 404
no rclone-smoke leftovers
```
Вывод: запись → чтение → удаление файла подтверждены. Файл удалён.
## Ограничения
- Нужен FUSE: `SYS_ADMIN` + `/dev/fuse`. На хосте без FUSE не поедет.
- Сайдкар слушает docker-bridge `172.17.0.1:8098`, публично не выставлен.
Только localhost/локальные контейнеры.
- Запись идёт через VFS write-back (по умолчанию ~5s). Перед проверкой
«файл на сервере» опрашивать сервер, не доверять сразу после `write`.
- `rmdir` через монтирование НЕ доходит до `oo-webdav` (пустой каталог
остаётся на сервере). Удалять каталоги напрямую:
`rclone rmdir webdav:<path>` или `rclone purge webdav:<path>`.
- VFS-кэш растёт в томе `rclone-cache`; ограничить `--vfs-cache-max-size`.
- Блокировок между редактором OnlyOffice и монтированием нет. Не редактировать
один и тот же файл одновременно.
- Данные не шифруются на диске хоста в `rclone-cache` (том Docker).
+9 -10
View File
@@ -3,8 +3,8 @@ type: reference
status: current
related:
- README.md
- file_core.go
- file_facade.go
- filestore_core.go
- filestore_facade.go
- docs/elasticsearch.md
- docs/community-server-db.md
---
@@ -14,7 +14,7 @@ related:
## Что это
Один файловый клиент на все бэкенды (эпик #34). Модель и интерфейсы —
`file_core.go`. Фасад `FileClient` — `file_facade.go`. Бэкенды:
`filestore_core.go`. Фасад `FileClient` — `filestore_facade.go`. Бэкенды:
REST, WebDAV, SQL (PostgreSQL/MySQL), Elasticsearch. Правило одно:
код зовёт `c.Files()` и не знает про транспорт.
@@ -56,18 +56,18 @@ type Searcher interface {
}
```
`TextIndex` (`file_es_text.go`) — свой индекс: `Put`, `Delete`, `Search`,
`TextIndex` (`filestore_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 | `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 (папка/файл), потом зовут
@@ -202,4 +202,3 @@ 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-стор и схема БД.
- [rclone-webdav.md](rclone-webdav.md) — монтирование Documents как ФС.