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
131 lines
5.6 KiB
Markdown
131 lines
5.6 KiB
Markdown
---
|
||
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)).
|