feat(files): read-only SQL file store over Community Server DB (#36)
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 1m56s
Tests / Test (Go stable) (pull_request) Successful in 1m57s
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 1m56s
Tests / Test (Go stable) (pull_request) Successful in 1m57s
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
---
|
||||
type: reference
|
||||
status: current
|
||||
related:
|
||||
- README.md
|
||||
- file_pg.go
|
||||
- docs/elasticsearch.md
|
||||
---
|
||||
|
||||
# Community Server DB — прямой SQL-доступ (read-only)
|
||||
|
||||
## Что это
|
||||
|
||||
Бэкенд `pgStore` (`file_pg.go`) читает файлы и папки **напрямую из БД
|
||||
Community Server**, без HTTP-слоя. Реализует `FileStore` (`List`/`Stat`/
|
||||
`Download`) и `Searcher` по имени. Запись запрещена: все write-методы
|
||||
возвращают `ErrReadOnly`.
|
||||
|
||||
## Что за БД (research, live)
|
||||
|
||||
Проверено на VM `onlyoffice-v2` (SSH `127.0.0.1:32`):
|
||||
|
||||
- Community Server работает на **MySQL 8.0**, не на PostgreSQL.
|
||||
- Хост: `127.0.0.1:3306` внутри VM, база `onlyoffice`.
|
||||
- Конфиг: `/etc/onlyoffice/communityserver/appsettings.production.json`,
|
||||
`providerName: MySql.Data.MySqlClient`.
|
||||
- Таблицы: `files_file`, `files_folder`, `files_folder_tree`,
|
||||
`files_security`, тенанты — `tenants_tenants` (не `tenants`).
|
||||
- PostgreSQL 16 в той же VM — **наш** контур (`edw_docs`, роли `edw`/`edw_ro`,
|
||||
office-assistant), к OnlyOffice отношения не имеет. `files_file` в PG нет.
|
||||
- Портал хранит файлы в **S3/MinIO** (DiscStorage только для мелочи).
|
||||
Бакет `office`, объект — по ключу (см. ниже).
|
||||
|
||||
Вывод: бэкенд назван по issue «PostgreSQL», но живой источник — MySQL.
|
||||
`database/sql` + драйвер по DSN: `mysql` для MySQL, `pgx` для PostgreSQL.
|
||||
`Name()` возвращает фактический движок (`mysql` или `postgres`).
|
||||
|
||||
## Схема
|
||||
|
||||
`files_file` — одна строка **на версию** (PK `tenant_id, id, version`):
|
||||
|
||||
| поле | смысл |
|
||||
|------|-------|
|
||||
| `id` | id файла (тот же, что в REST/ES) |
|
||||
| `version` | номер версии этой строки |
|
||||
| `version_group` | номер версии |
|
||||
| `current_version` | `1` = текущая версия, `0` = старая |
|
||||
| `folder_id` | id родительской папки |
|
||||
| `title` | имя файла с расширением |
|
||||
| `content_length` | размер в байтах |
|
||||
| `create_on`, `modified_on` | даты (UTC, без зоны) |
|
||||
| `tenant_id` | тенант (портал) |
|
||||
|
||||
`files_folder`: `id`, `parent_id`, `title`, `create_on`, `modified_on`,
|
||||
`tenant_id`. `files_folder_tree`: `folder_id`, `parent_id`, `level` — готовое
|
||||
дерево, пока не используется.
|
||||
|
||||
Текущую строку файла берём по `current_version = 1`.
|
||||
|
||||
## Доступ (SSH-туннель)
|
||||
|
||||
MySQL слушает только `127.0.0.1:3306` внутри VM. Снаружи — SSH-туннель
|
||||
(SSH в VM открыт как `127.0.0.1:32`):
|
||||
|
||||
```bash
|
||||
ssh -f -N -o ControlMaster=no -o ControlPath=none \
|
||||
-p 32 -i ~/.ssh/id_ed25519 \
|
||||
-L 13306:127.0.0.1:3306 root@127.0.0.1
|
||||
|
||||
# MySQL DSN затем:
|
||||
# root:<pw>@tcp(127.0.0.1:13306)/onlyoffice?parseTime=true
|
||||
```
|
||||
|
||||
`-o ControlMaster=no -o ControlPath=none` обязательны: иначе forward уходит в
|
||||
persistent master из `~/.ssh/config`.
|
||||
|
||||
## Переменные
|
||||
|
||||
| env | default | смысл |
|
||||
|-----|---------|-------|
|
||||
| `ONLYOFFICE_DSN` | — | DSN драйвера (MySQL `...@tcp(...)/...` или `postgres://...`) |
|
||||
| `ONLYOFFICE_PG_DRIVER` | авто | `postgres` или `mysql`; иначе по форме DSN |
|
||||
| `ONLYOFFICE_PG_TENANT` | `ONLYOFFICE_TENANT` | фильтр `tenant_id` (пусто = все) |
|
||||
| `ONLYOFFICE_PG_HOST/PORT/USER/PASSWORD/DBNAME/SSLMODE` | — | собрать PG DSN, если `ONLYOFFICE_DSN` пуст |
|
||||
|
||||
Имена — в [`.env.example`](../.env.example). Секретов нет.
|
||||
|
||||
## Download (MinIO)
|
||||
|
||||
`Download` не ходит в REST. Ключ объекта собирается из строки `files_file`:
|
||||
|
||||
```
|
||||
00/00/<tenant>/files/folder_<shard>/file_<id>/v<version>/content.<ext>
|
||||
shard = (id/1000 + 1) * 1000
|
||||
```
|
||||
|
||||
`shard` — не `folder_id`, а следующая тысяча над `id` (файл 3727 →
|
||||
`folder_4000`). Проверено live по бакету `office`.
|
||||
|
||||
Стриминг переиспользует `downloadMinioObject` из `storage_fallback.go`
|
||||
(та же подпись SigV4 и `MINIO_*`), без дублирования.
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
go test ./... # unit: rebind, csObjectKey, маппинг
|
||||
go test -race ./...
|
||||
|
||||
# integration (нужен DSN; skip без него)
|
||||
ONLYOFFICE_DSN='root:<pw>@tcp(127.0.0.1:13306)/onlyoffice?parseTime=true' \
|
||||
ONLYOFFICE_PG_TENANT=1 \
|
||||
ONLYOFFICE_PG_TEST_FILE_ID=22484 \
|
||||
ONLYOFFICE_PG_TEST_FOLDER_ID=649 \
|
||||
MINIO_ACCESS_KEY=... MINIO_SECRET_KEY=... \
|
||||
go test -tags=integration -run TestIntegrationPGStore -v ./...
|
||||
```
|
||||
|
||||
Integration сверяет `Stat`/`List`/`Download` с REST (`c.Files()`) и проверяет,
|
||||
что write-методы дают `ErrReadOnly`.
|
||||
|
||||
## Грабли
|
||||
|
||||
- MySQL хранит `datetime` без зоны; `parseTime=true` (ставится автоматически)
|
||||
читает их как UTC. REST отдаёт `+02:00` — сравнивать моменты, не строки.
|
||||
- `GetFile` (REST) не отдаёт `contentLength` — размер сверять с `Stat` SQL.
|
||||
- Один файл = много строк `files_file` (по версиям). Без `current_version = 1`
|
||||
получите дубликаты.
|
||||
- `folder_id` не входит в ключ MinIO; ключ считает `shard` от `id`.
|
||||
- Searcher SQL ищет только по имени (`LIKE`). Контент — Elasticsearch
|
||||
([elasticsearch.md](elasticsearch.md)).
|
||||
Reference in New Issue
Block a user