No description
  • Go 60.3%
  • TypeScript 32.1%
  • CSS 6%
  • Python 1.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Dreykp 3744f31a8c
Some checks failed
CI / check (push) Has been cancelled
CI / metadata-tools (push) Has been cancelled
feat: add local-first S3 synchronization and storage administration
2026-10-16 00:31:35 +03:00
.github/workflows feat: find and import book metadata from online sources 2026-10-12 21:11:15 +03:00
cmd/server feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
docker build: halve metadata image with a verified runtime closure 2026-10-12 01:39:39 +03:00
frontend feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
internal feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
.dockerignore feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
.gitignore feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
docker-compose.metadata.yml build: add optional metadata tools image and integration checks 2026-10-05 20:03:13 +03:00
docker-compose.s3.yml feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
docker-compose.upload.yml feat: upload books from admin UI into safe library folders 2026-10-12 18:56:06 +03:00
docker-compose.yml feat: find and import book metadata from online sources 2026-10-12 21:11:15 +03:00
Dockerfile build: slim metadata image to headless Calibre runtime 2026-10-12 01:10:42 +03:00
Dockerfile.metadata build: halve metadata image with a verified runtime closure 2026-10-12 01:39:39 +03:00
go.mod feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
go.sum feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00
README.md feat: add local-first S3 synchronization and storage administration 2026-10-16 00:31:35 +03:00

LiteOPDS

Легкий, швидкий OPDS-сервер на Go з SQLite (без CGO) та вбудованим веб-інтерфейсом для керування метаданими. Збирається в один бінарник із вбудованими шаблонами та працює у distroless-образі від непривілейованого користувача.

Можливості

  • Новий frontend на React + TypeScript: сітка/список книг, картки з усіма форматами, колекції авторів/серій/жанрів/тегів, адаптивне меню, світла й темна теми, пакетний редактор, сканування, словники та керування користувачами.
  • Кілька форматів однієї книги: одна картка у вебінтерфейсі й OPDS, окремі посилання для завантаження кожного формату; ручне об'єднання та розділення адміністратором.
  • OPDS 2.0 JSON-каталог: новинки, всі книги, автори, серії, жанри, теги, пошук
  • Веб-інтерфейс українською для редагування метаданих: назва, автори, серія, мова, видавець, рік, опис, жанр, теги, обкладинка; ручні зміни зберігаються при скануванні
  • Просунутий редактор файлів: 24 поля, пакетне редагування форматів книги, підказки з назв і початку книг, OCR PDF, попередній перегляд, резервні копії та відновлення; запис в оригінали вмикається окремо.
  • Пошук зовнішніх метаданих: Open Library та Google Books, пошук за ISBN або назвою й автором, вибірковий імпорт, обкладинки, JSON-експорт та перенесення знайдених полів у редактор файлів.
  • Безпечна авторизація: bcrypt (cost ≥ 10), серверні сесії, HTTP-only cookies, підписані HMAC-SHA256, CSRF-токени на всі зміни, rate-limit на невдалі входи (по IP і по акаунту)
  • Формати: EPUB, FB2, FB2.ZIP, PDF, MOBI, AZW/AZW3, CBZ, CBR, CB7, DJVU, TXT, RTF, HTML
  • SQLite FTS5 для повнотекстового пошуку
  • Автоматичне сканування бібліотеки з періодичним оновленням
  • Необов'язкова S3-копія книг із локальної /library: розклад, ручний запуск, перевірка SHA‑256, захист від перезапису та відновлення відсутніх файлів.
  • Немає вбудованої читалки — використовуйте клієнт із підтримкою OPDS 2.0 та HTTP Basic Auth

Швидкий старт (Docker)

git clone <this-repo> liteopds && cd liteopds
mkdir -p data library
# покладіть свої книги в ./library

LITEOPDS_UID=$(id -u) LITEOPDS_GID=$(id -g) docker compose up -d --build

Контейнер використовує UID/GID власника каталогу data, щоб мати доступ до SQLite та обкладинок. Якщо ці змінні не задано, Compose використовує 1000:1000.

Відкрийте http://localhost:8280 — перший зареєстрований користувач отримує роль адміністратора. Після реєстрації запустіть сканування в розділі «Сканування».

Головна адреса відкриває новий інтерфейс /app/books; вхід — /app/login. React-інтерфейс — єдиний веб-інтерфейс; для нього потрібен JavaScript. Go обслуговує JSON API /api/*, OPDS, завантаження файлів, обкладинки та зібрані статичні файли React. Старі Go-шаблони й /static/* видалено. Старі адреси сторінок (/books, /login, /admin/* тощо) лише перенаправляють до відповідних сторінок /app/*, зберігаючи параметри URL. Старі POST-маршрути видалено: усі зміни виконуються через API з перевірками сесії, CSRF та прав.

Frontend і оновлення на VPS

Інтерфейс орієнтований на адміністрування, без промоблоків. На великих екранах усі інструменти доступні в постійній бічній навігації; на телефоні — у меню, яке підтримує Escape та клавіатурну навігацію. Профіль і вихід доступні у випадаючому меню користувача праворуч угорі, зокрема на телефоні. На робочих сторінках немає дубльованих вступних заголовків; каталог починається з фільтрів, колекції — зі списку. Примітки про безпеку запису та зберігання резервних копій залишаються видимими.

Результати дій і помилки форм показуються власними спливаючими сповіщеннями в нижньому правому куті, без зсуву сторінки. Успіх та інформація зникають через 5 секунд, помилки й попередження — через 8; можна закрити їх вручну. Таймер призупиняється під курсором, при клавіатурному фокусі та в прихованій вкладці браузера. Одночасно видно до чотирьох повідомлень; повторне однакове повідомлення оновлює таймер замість дублювання. Повідомлення про виконані дії не повторюються під час оновлення сторінки чи фонового опитування. Примітки, журнали й підтвердження небезпечних операцій залишаються на місці.

Єдиний пошук у верхній панелі фільтрує поточну сторінку: книги, авторів, серії, жанри, теги, активну вкладку словника мов/жанрів/тегів, користувачів, історію змін або історію сканувань. Фільтрація спрацьовує під час введення; для каталогу — з паузою 300 мс або відразу після Enter. Пошук книг виконується сервером до пагінації, зберігає інші фільтри й скидає номер сторінки. Інші списки фільтруються локально, без повторних запитів та перезавантаження форм; історія обмежена останніми 100 операціями / 30 скануваннями. Пошук і вкладка словника зберігаються в URL, тому відновлюються після оновлення сторінки. На сторінках без списків пошук прихований. Пошук усередині випадаючих списків мов, жанрів і тегів залишається окремим інструментом вибору.

Активні поля мають контрастну рамку, зовнішній ореол і легке тонування, узгоджені зі світлою або темною темою. Головний пошук підсвічується цілим блоком; у випадаючих списках підсвічується також рядок пошуку. Це працює при виборі мишею, дотиком і клавіатурою, не змінюючи розміри полів.

Адміністратор за замовчуванням бачить таблицю книг: автор, шлях, мова/жанри, формати, обсяг, незаповнені поля, швидкі переходи до метаданих і вихідних файлів. Сітка та список також доступні. Фільтри за форматом, мовою, жанром, автором, серією, шляхом та станом метаданих виконуються сервером до пагінації. Сортування включає час зміни та обсяг файлів. Повернення з редактора зберігає фільтри й сторінку каталогу в межах вкладки.

Виберіть до 10 книг для пакетного файлового редактора; дві вибрані книги можна об’єднати з підтвердженням. Розділення форматів, скидання ручних метаданих та видалення доступні в деталях книги. Швидке видалення з таблиці прибирає лише картку, залишаючи файли на диску (сканування може додати їх знову). Запис метаданих у файли, як і раніше, проходить перегляд змін, підтвердження та резервне копіювання; звичайний редактор змінює лише БД.

/app/admin/dashboard — огляд системи, доступний тільки адміністратору: кількість логічних книг і фізичних файлів, обсяг книг, SQLite/WAL/SHM, обкладинок і резервних копій, розподіл форматів, відсутні метадані, стан сканування, наявність інструментів, Go heap і RSS процесу (RSS на Linux). Обсяг книг береться з останнього індексу сканування, не з повторного обходу бібліотеки. Сховище показує довжини файлів, не зайняті блоки файлової системи; обкладинки та резервні копії підраховуються без переходу за символічними посиланнями, з обмеженням часу. Недоступний вимір відображається як невідомий, а не нуль. Показники оновлюються кнопкою; це не метрики всього VPS/контейнера. Конфігурація на огляді тільки відображається, змінюється через env. Для закритого сервісу встановіть LITEOPDS_ALLOW_REGISTER=false.

Тема й вигляд зберігаються локально в браузері. Зовнішні шрифти, картинки або сервіси для дизайну не завантажуються.

Backend залишається на Go. React-збірка вбудована в бінарник через go:embed: окремого Node-сервера, порту чи нового контейнера під час роботи немає. Витрати RAM нового інтерфейсу переважно припадають на браузер користувача. Каталог завантажується сторінками, обкладинки — ліниво; у браузер не копіюється вся БД.

Обидва Dockerfile автоматично збирають frontend на окремому Node-етапі, а потім Go-бінарник. Node, npm і Go-компілятор не потрапляють у фінальний образ. У контекст збірки не включаються локальні залежності, браузерні звіти та готовий frontend: він завжди збирається заново з джерел. Окремі COPY зберігають кеш збірки при змінах README, compose-файлів або браузерних тестів. Для оновлення стандартного образу:

docker compose up -d --build liteopds

Для metadata-образу використовуйте ті самі -f docker-compose.yml -f docker-compose.metadata.yml, що під час його встановлення. Дані та ключ сесії залишаються у data; перенесення БД не потрібне. У Traefik проксуйте весь домен до того самого порту 8080, включно з /app/, /api/, /files/, /covers/ і /opds. Для HTTPS встановіть LITEOPDS_COOKIE_SECURE=true.

Frontend викликає /api/... на тому самому домені, використовує HttpOnly-сесію та CSRF-захист. Паролі й токени сесій не зберігаються в localStorage; там лише тема та вигляд каталогу. Go перевіряє роль і права на кожну операцію; JSON не містить хешів паролів. OPDS 2.0 та Basic Auth читалок не змінюються.

Розробка frontend

Потрібен Node.js 22.12+ і npm. Готова збірка також зберігається в репозиторії, тому звичайний go run ./cmd/server працює без встановлення Node.

npm --prefix frontend ci
npm --prefix frontend run build
npm --prefix frontend test
npm --prefix frontend exec -- playwright install chromium
npm --prefix frontend run test:e2e

Після зміни frontend перебудуйте його перед Go-бінарником. Для швидкої розробки запустіть Go на 127.0.0.1:8080, а npm --prefix frontend run dev — в іншому терміналі; відкрийте http://127.0.0.1:5173/app/. Vite проксує API, файли й обкладинки до Go. У локальному dev не використовуйте production HTTPS cookie або production LITEOPDS_BASE_URL.

Browser-тести запускають Go-сервер з окремими тимчасовими БД і бібліотекою на 127.0.0.1:18085, перевіряють вхід, пошук, редагування, підказки, preview → commit → restore, мобільний екран та збереження теми. Production data та library при цьому не використовуються.

OPDS-клієнтам давайте URL:

http://<host>:8280/opds

і ваші логін/пароль (HTTP Basic). Рекомендується ставити за HTTPS-проксі.

OPDS 2.0 та перехід із OPDS 1.2

Каталог відповідає формату OPDS 2.0 на тих самих адресах /opds, /opds/all, /opds/new та інших розділах. Тип відповіді — application/opds+json; замість Atom/XML використовуються metadata, links, navigation та publications. Окрема картка книги доступна через /opds/books/{id} з типом application/opds-publication+json.

Це несумісна зміна протоколу: OPDS 1.x / Atom та OpenSearch XML більше не видаються. Адреса /opds/search.xml прибрана. Старому клієнту, який розуміє лише OPDS 1.x, потрібен клієнт із підтримкою OPDS 2.0; перевірте можливості саме вашої версії читалки. Бібліотека, БД та вебінтерфейс не потребують перенесення.

Пошук оголошено шаблонним посиланням /opds/search{?query}; наприклад, /opds/search?query=Фентезі. Параметр q залишено як допоміжний псевдонім, але стандартний query має пріоритет. Пагінація використовує offset, numberOfItems, itemsPerPage, currentPage і посилання first / last / previous / next, які зберігають пошуковий запит. Для порожніх результатів каталог повертає навігацію до кореня замість порожньої колекції; numberOfItems містить кількість знайдених книг.

Метадані книг використовують модель Readium: author, publisher, language, subject для жанрів/тегів та belongsTo.series для назви й номера серії. Опис не обрізається; додатково видаються підзаголовок, перекладачі й редактори. Ідентифікатор — ISBN URN для валідного ISBN, інакше стабільний URN запису каталогу. Поле published потребує повної дати: рік 2026 представляється як 2026-01-01 лише у JSON. Рік у картці та вихідні файли не змінюються. Некоректні старі дати й мовні позначення, які не відповідають синтаксису BCP 47, не включаються у JSON; збережені метадані при цьому не переписуються.

Кожен формат має окреме посилання з rel: acquisition; download не використовується, оскільки файли потребують авторизації. Обкладинки та мініатюри містяться у images, а не в посиланнях Atom. Авторизація HTTP Basic залишається обов'язковою для каталогу, карток, завантажень і зображень. LITEOPDS_BASE_URL застосовується також до вкладених посилань і шаблону пошуку.

Конфігурація (env)

Змінна За замовчуванням Опис
LITEOPDS_LISTEN :8080 адреса HTTP
LITEOPDS_DATA_DIR ./data каталог для БД, ключів, обкладинок
LITEOPDS_LIBRARY_DIR ./library каталог з книгами; read-only, якщо запис в оригінали не потрібен
LITEOPDS_BASE_URL `` абсолютний URL, якщо за проксі (використовується в OPDS)
LITEOPDS_SESSION_KEY auto 64 hex-символи; якщо не задано, генерується і зберігається
LITEOPDS_COOKIE_SECURE false true, якщо сайт за HTTPS
LITEOPDS_ALLOW_REGISTER true чи можна реєструватися новим користувачам
LITEOPDS_SCAN_ON_START true сканувати при старті; початкове значення для «Налаштувань»
LITEOPDS_SCAN_INTERVAL 30m періодичне сканування (Go duration; 0 вимикає); початкове значення для «Налаштувань»
LITEOPDS_BCRYPT_COST 12 вартість bcrypt (10–15)
LITEOPDS_FOLDER_SERIES fallback серія з папки: fallback лише без серії у файлі, override завжди, off ніколи
LITEOPDS_TRUSTED_PROXIES порожньо IP/CIDR довірених проксі через кому; лише від них приймаються заголовки IP
LITEOPDS_METADATA_WRITE false дозволити адміністратору запис метаданих у вихідні файли
LITEOPDS_UPLOAD_MAX_BYTES 536870912 загальний розмір файлів одного upload у байтах; від 1 MiB до 2 GiB; кожен файл не більше 256 MiB
LITEOPDS_DOWNLOAD_NAME {authors} — {title} шаблон імені завантаження; розширення зберігається, файл на диску не перейменовується
LITEOPDS_EXIFTOOL exiftool виконуваний файл ExifTool для PDF
LITEOPDS_EBOOK_META ebook-meta виконуваний файл Calibre для EPUB
LITEOPDS_QPDF qpdf перевірка PDF та видалення старих інкрементальних метаданих
LITEOPDS_PDFTOTEXT pdftotext текст перших трьох сторінок PDF
LITEOPDS_PDFTOPPM pdftoppm обкладинка з першої сторінки PDF та рендеринг перших двох сторінок для OCR
LITEOPDS_TESSERACT tesseract OCR; запускається лише за явним вибором
LITEOPDS_OCR_LANGUAGE ukr+eng встановлені мовні пакети Tesseract

S3: віддалена копія з локальною бібліотекою

За замовчуванням використовується LITEOPDS_STORAGE=local: жодних звернень до S3. Режим s3 додає асинхронну віддалену копію, не замінюючи файлову систему: нові книги, завантаження, сканування, редагування метаданих і завантаження у читалку працюють із /library. Спершу файл записується локально, потім після індексації потрапляє до S3 за розкладом або ручним запуском. Недоступність S3 не блокує читання чи редагування локальних книг; невдалу синхронізацію можна повторити.

Підключення через Docker Compose

Додайте у захищений .env (приклад, не справжні ключі):

LITEOPDS_S3_BUCKET=my-private-books
LITEOPDS_S3_REGION=us-east-1
# Для Amazon S3 endpoint можна не задавати. Для іншого провайдера — його origin:
LITEOPDS_S3_ENDPOINT=https://s3.example.com
LITEOPDS_S3_PREFIX=liteopds
LITEOPDS_S3_ACCESS_KEY=replace-with-access-key
LITEOPDS_S3_SECRET_KEY=replace-with-secret-key
LITEOPDS_S3_PATH_STYLE=true
LITEOPDS_S3_SYNC_INTERVAL=5m
chmod 600 .env
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d --build

За потреби додайте також docker-compose.metadata.yml для інструментів запису метаданих. S3 overlay монтує ./library:/library:rw; права UID/GID на library та data потрібно надати користувачу контейнера. Сервіс не створює bucket — створіть приватний bucket у провайдера заздалегідь. Використовуйте окремий префікс для кожної бібліотеки та лише один екземпляр сервісу на одну локальну бібліотеку / базу. Не використовуйте це підключення для синхронізації між VPS.

Змінна Типове значення Призначення
LITEOPDS_STORAGE local local або s3; S3 overlay встановлює s3
LITEOPDS_S3_ENDPOINT порожньо HTTPS origin S3-сумісного API; порожньо — стандартний Amazon S3
LITEOPDS_S3_BUCKET обов'язково для S3 існуючий приватний bucket
LITEOPDS_S3_REGION us-east-1 регіон підпису; для провайдерів із регіоном auto задайте auto
LITEOPDS_S3_PREFIX порожньо префікс; ключ книги: <prefix>/library/<відносний шлях у library>
LITEOPDS_S3_PATH_STYLE true path-style адресація; false для провайдерів, що потребують virtual-host style
LITEOPDS_S3_ALLOW_HTTP false явний дозвіл HTTP лише для довіреної приватної мережі, наприклад MinIO
LITEOPDS_S3_ACCESS_KEY обов'язково ключ доступу або відповідний _FILE
LITEOPDS_S3_SECRET_KEY обов'язково секретний ключ або відповідний _FILE
LITEOPDS_S3_SESSION_TOKEN порожньо необов'язковий session token або відповідний _FILE
LITEOPDS_S3_SYNC_INTERVAL 5m початковий інтервал, від 1m до 8760h; збережений розклад з інтерфейсу має пріоритет

Endpoint, bucket, region, prefix і ключі змінюються лише через ENV з перезапуском контейнера. Вони не редагуються через браузер. Ключі не записуються до БД, журналів чи API. Однак звичайні ENV доступні адміністратору Docker через inspect; для секретів бажано використовувати Docker secrets.

Для Docker secrets замість значень LITEOPDS_S3_ACCESS_KEY / SECRET_KEY задайте LITEOPDS_S3_ACCESS_KEY_FILE=/run/secrets/s3_access_key та LITEOPDS_S3_SECRET_KEY_FILE=/run/secrets/s3_secret_key. Не задавайте одночасно значення й _FILE для одного ключа. У власному Compose override підключіть:

services:
  liteopds:
    secrets:
      - s3_access_key
      - s3_secret_key
secrets:
  s3_access_key:
    file: ./secrets/s3_access_key
  s3_secret_key:
    file: ./secrets/s3_secret_key

Тека secrets/ виключена з Git і Docker build context. Файли секретів мають читатися UID контейнера. ENV / Docker secrets не замінюють ротацію ключів: після ротації перезапустіть контейнер. Для тимчасових credentials їх оновлення зовнішнє; автоматичного пошуку ключів у ~/.aws або metadata endpoint VPS немає.

Налаштування та захист синхронізації

У Налаштування → Сховище книг доступні параметри підключення без секретів, розклад, ручний запуск, стан операції, кількість/обсяг записів маніфесту та журнали. Перший автоматичний запуск — через 30 секунд після старту; наступні — через збережений інтервал після завершення. Збереження розкладу починає відлік заново. Вимкнення автоматичного режиму не перериває активну операцію та не забороняє ручний запуск. Показники маніфесту — не вимірювання всього bucket.

Кнопка перевірки підключення виконує лише ListObjectsV2 з префіксом і максимум одним результатом: вона не створює тестових об'єктів і не доводить права запису. Для роботи потрібні s3:ListBucket (із доступом до префікса бібліотеки), s3:GetObject (також для HEAD) і s3:PutObject у її префіксі; DeleteObject не потрібен. Використовується офіційний AWS SDK for Go v2, TLS перевіряється, підписані запити не перенаправляються на інший endpoint. Провайдер має підтримувати SHA‑256 checksum і умовний PutObject з If-None-Match / If-Match; несумісність відображається помилкою, без переходу до небезпечного перезапису.

За кожен запуск повністю хешуються всі проіндексовані файли, включно з усіма форматами книги. Непроскановані файли, база, обкладинки сервісу та резервні копії метаданих не копіюються. Потрібне вільне місце в data/storage-staging розміром до одного найбільшого файлу; тимчасові копії створюються лише для нових/змінених файлів або повторного запису відсутнього об'єкта S3, а не всієї бібліотеки на кожен запуск. Передача послідовна, пам'ять не зростає на розмір книги. Зміни в інтерфейсі виключаються тільки на час перевірки / створення локального знімка, не на час мережевого запиту. Якщо сканер/редактор зайнятий, файл позначається помилкою для повторного запуску. Максимальний файл — 5 GiB, без multipart upload.

SHA‑256, розмір, ідентифікатор бібліотеки та ETag зберігаються у маніфесті БД; checksum передається S3, а результат перевіряється через HEAD. Чужий об'єкт або незалежно змінений ETag не перезаписуються. Якщо процес перервався між записом об'єкта й оновленням попереднього маніфесту, наступний запуск може повідомити конфлікт ETag: перевірте обидві копії вручну, сервіс не вгадує правильну версію. Журнали доступні лише адміну; виводяться коди помилок S3 без сирих відповідей провайдера. Зберігаються останні 100 операцій, один журнал обмежений приблизно 64 KiB. Після перезапуску незавершені операції позначаються перерваними.

Локальний кеш, перенесення і відновлення

Автоматичного очищення /library та ліміту кешу немає навмисно: без зміни сканера відсутній локальний файл означає видалену книгу. Тому /library наразі є повною робочою копією, а не кешем із витісненням. Файли не завантажуються з S3 прозоро під час читання. Не видаляйте локальні копії для економії диска.

Після зміни метаданих наступний запуск оновить відповідний об'єкт S3. Після перенесення книги до теки автора/серії створиться об'єкт за новим шляхом; старий об'єкт залишиться. Видалення книги з каталогу/локального диска не видаляє її віддалену копію. Це не двостороння синхронізація і не історія всіх версій: для захисту попередніх редакцій увімкніть versioning / резервування у провайдера.

У розділі відновлення можна явно повернути лише відсутні локальні файли з останнього маніфесту поточного підключення. Наявні файли не змінюються. Дані повністю перевіряються за розміром і SHA‑256 перед атомарною публікацією; симлінки не підтримуються. Відновлення потребує додаткового місця в data та library до розміру одного файлу, а локальна файлова система має підтримувати hard links. Вимкніть автоматичне сканування на час відновлення, потім запустіть сканування вручну. Можуть повернутися й навмисно видалені книги. Якщо сканер уже видалив запис, повторна індексація може призначити новий ID і не відновлює ручні правки БД.

Зміна bucket / endpoint / prefix — не міграція: для нового підключення ведеться окремий маніфест, файли з повної локальної копії потраплять туди за наступної синхронізації; старе сховище не очищається. Для відновлення старого маніфесту поверніть старі параметри підключення.

S3-копія книг не є резервною копією всього сервісу. Обов'язково окремо резервуйте data / консистентну SQLite БД (включно з маніфестом та ідентифікатором бібліотеки), налаштування й секрети. Без маніфесту інтерфейс не може відновити повний каталог із довільного bucket.

Локальний запуск (без Docker)

Потрібен Go 1.26 або новіший; Docker і CI використовують Go 1.27. Каталог бібліотеки має існувати до запуску.

go build -o liteopds ./cmd/server
mkdir -p my-books
LITEOPDS_LIBRARY_DIR=./my-books ./liteopds

Модель безпеки

  • Паролі: bcrypt, мінімум 10 символів, літери + цифри, максимум 72 байти UTF-8
  • Сесії: 32 випадкові байти, ID у БД зберігається як SHA-256 хеш; cookie підписаний HMAC; HttpOnly, SameSite=Lax
  • CSRF: токен зв'язаний з сесією, перевіряється на всіх POST-запитах авторизованого веб-інтерфейсу; для форм входу/реєстрації перевіряються Origin та Sec-Fetch-Site
  • Rate-limit: акаунт блокується на 15 хв після 8 невдач, IP — на 15 хв після 20
  • OPDS: HTTP Basic Auth, той самий користувач; клієнт має підтримувати цей метод
  • Успішні Basic Auth перевірки кешуються на одну хвилину, максимум 256 записів. Паролі у кеші не зберігаються; кожен запит перевіряє поточний парольний хеш, роль, існування користувача й блокування IP/акаунта в БД.
  • Обкладинки: до 10 МіБ, JPEG/PNG/GIF/WebP, перевірка формату та розмірів; OPDS-мініатюри генеруються окремо і кешуються на диску.
  • Видача книг обмежена коренем бібліотеки, включно з перевіркою символічних посилань.

Обкладинки у читалках

В OPDS 2.0 колекція images містить обкладинку та JPEG-мініатюру з версіонованими посиланнями: після заміни зображення їхні URL змінюються так само, як у вебкаталозі. MIME-тип визначається за вмістом, а не лише за розширенням: старі файли .img не оголошуються як образи диска. Для WebP та нестандартних типів обкладинок OPDS пропонує JPEG-перегляд, залишаючи оригінал у вебкаталозі. Якщо обкладинка вже використовує URL мініатюри, однакове посилання не дублюється. Такий набір зображень відповідає вимогам OPDS 2.0. Завантаження зображень підтримує GET/HEAD, Content-Type, Content-Length, Last-Modified та ETag. Перевірка кешу за ETag не повертає старе зображення при кількох змінах протягом однієї секунди.

Читалка має передавати Basic Auth не лише для OPDS-фіду, а й для /covers/.... Без авторизації сервер відповідає 401, а не відкриває обкладинки публічно. Якщо використовується LITEOPDS_BASE_URL, він має збігатися з доступною читалці адресою сервісу; інший домен/протокол може завадити передаванню авторизації.

Обкладинка в OPDS і обкладинка вже завантаженої книги — різні речі. Завантаження зображення у звичайному редакторі змінює лише каталог. Щоб обкладинка потрапила у EPUB/FB2, потрібно записати її через просунутий редактор файлів і повторно завантажити книгу. Для PDF запис обкладинки в оригінал не підтримується: каталог може мати ручну обкладинку, але перша сторінка самого PDF при цьому не зміниться.

Для публічного розгортання обов'язково:

  • HTTPS (Caddy/Nginx/Traefik спереду)
  • LITEOPDS_COOKIE_SECURE=true
  • LITEOPDS_BASE_URL=https://your-domain
  • LITEOPDS_ALLOW_REGISTER=false після створення потрібних акаунтів
  • LITEOPDS_TRUSTED_PROXIES — лише адреси вашого проксі, якщо потрібний IP клієнта

Сканування та ручні метадані

Автоматичне сканування

Адміністратор може змінити розклад у «Налаштування» → «Автоматичне сканування» (/app/admin/settings): увімкнути чи вимкнути періодичне сканування, задати інтервал у хвилинах (від 1 до 525600, тобто 365 днів), а також окремо обрати сканування при старті сервісу.

Зміни зберігаються у SQLite й застосовуються до розкладу без перезапуску. Після першого збереження вони мають пріоритет над LITEOPDS_SCAN_INTERVAL та LITEOPDS_SCAN_ON_START; до цього використовуються значення ENV. Зміна сканування при старті діє з наступного запуску сервісу й не запускає скан зараз.

Після збереження або завершення автоматичного сканування відлік інтервалу починається заново, без черги пропущених запусків. Якщо вже триває сканування або редагування файлів, автоматичний запуск пропускається. Вимкнення розкладу не перериває поточну операцію й не забороняє ручне сканування у «Скануванні». Інтервал залишається збереженим, навіть якщо розклад вимкнений. «Огляд системи» показує поточний збережений розклад, а не початкове значення ENV.

Додавання книг через інтерфейс

Адміністратор може натиснути «Додати книгу» праворуч у header, перед профілем, або відкрити /app/admin/upload. Виберіть файли однієї книги: до 10 різних форматів, до 256 MiB на файл і 512 MiB загалом. Назва попередньо заповнюється з першого імені файлу; її можна змінити. Якщо автора не вказано, він читається з основного файлу. Порожня серія означає книгу без серії.

Файли зберігаються у LITEOPDS_LIBRARY_DIR:

Автор/Серія/Назва.epub
Автор/Серія/Назва.fb2.zip
Автор/Назва.pdf           # без серії
Без автора/Назва.txt      # автора немає і в метаданих

Теки створюються автоматично, назви очищуються від розділювачів шляхів, керуючих символів і небезпечних компонентів та обмежуються 120 байтами UTF-8. Розширення зберігаються у нижньому регістрі; всі формати отримують спільне ім’я. Інші видання з такою самою назвою потрібно назвати по-іншому: наявні файли чи картки не перезаписуються й не об’єднуються непомітно.

Книга одразу додається в каталог разом з метаданими й обкладинкою основного файлу; повне сканування не потрібне. Назва та серія, включно з порожньою, зберігаються як ручні значення; явно введений автор також. Тому тека автора не стане серією після повторного сканування. Вміст завантажених файлів не переписується: для запису метаданих у самі книги використовуйте редактор файлів.

Потрібен запис до library і data; LITEOPDS_METADATA_WRITE не потрібен. Стандартний compose залишає бібліотеку read-only. Для дозволу завантаження:

docker compose -f docker-compose.yml -f docker-compose.upload.yml up -d --build

Metadata-override вже монтує бібліотеку з rw. UID/GID сервісу повинні мати права створення тек і файлів; не використовуйте chmod 777. Загальний ліміт задає LITEOPDS_UPLOAD_MAX_BYTES (типово 536870912; від 1 MiB до 2 GiB). Ліміт reverse proxy та його таймаути також мають дозволяти такий upload. Завантаження потребує сесії адміністратора й CSRF; під час сканування чи іншого запису воно відхиляється. Файли приймаються з обмеженим RAM-буфером, решта тимчасово зберігається на диску. При звичайній помилці операція прибирає лише створені нею файли, обкладинку й порожні теки. Після аварійної зупинки перевірте файли та запис нової книги. Можуть залишитися приховані .upload-* у бібліотеці; не прибирайте їх, доки сервіс працює. Успішні завантаження такі тимчасові теки не залишають.

Як читається ієрархія тек

Корінь бібліотеки задає LITEOPDS_LIBRARY_DIR (типово ./library). Сканер рекурсивно обходить усі вкладені теки без обмеження глибини. Обов'язкової структури «автор → серія → книга» немає: кожен звичайний файл підтримуваного формату додається до каталогу, а формати з однаковим іменем в одній теці об'єднуються в одну книгу (див. нижче). Непідтримувані файли ігноруються. Розширення розпізнаються незалежно від регістру. Архіви підтримуваних форматів обробляються як одна книга, а не як вкладена бібліотека.

Для серії з теки береться лише назва безпосередньої батьківської теки файлу. Вищі рівні шляху не враховуються. Приклад нижче показує кандидатів на назву серії, а не остаточні значення метаданих:

library/
├── Окрема книга.epub              # немає серії з теки
├── Автор/
│   ├── Інша книга.fb2             # кандидат на серію: Автор
│   └── Назва серії/
│       ├── Книга 1.epub           # кандидат на серію: Назва серії
│       └── Частина/
│           └── Книга 2.pdf        # кандидат на серію: Частина
└── .чернетки/
    └── Книга.epub                 # не сканується

Поведінку визначає LITEOPDS_FOLDER_SERIES:

  • fallback (типово): назва батьківської теки стає серією, лише якщо у файлі серія відсутня.
  • override: назва батьківської теки замінює серію з файлу. Для книги безпосередньо в корені серія з файлу зберігається.
  • off: назви тек не використовуються для серії.

Ручне значення серії, навіть порожнє, має пріоритет у всіх режимах. Автор, жанр і теги з назв тек не визначаються; номер книги в серії читається з метаданих файлу, а не з назви файлу чи порядку обходу. Ієрархія тек не відтворюється в OPDS: каталог групує книги за їхніми метаданими.

Вкладені теки, назви яких починаються з ., пропускаються разом з усім вмістом. Це правило не поширюється на сам корінь бібліотеки чи назви окремих файлів. Символічні посилання всередині бібліотеки не обходяться і не додаються до каталогу; сам корінь бібліотеки може бути символічним посиланням.

Кілька форматів однієї книги

Для автоматичного об'єднання покладіть файли в одну теку й дайте їм однакове ім'я без розширення. .fb2.zip враховується як одне складене розширення:

library/
└── Автор/
    └── Назва серії/
        ├── Книга.epub
        ├── Книга.fb2.zip
        └── Книга.pdf

Це одна книга з трьома файлами, а не три записи. Серія з теки для всіх форматів — «Назва серії». Ім'я без розширення порівнюється точно, з урахуванням регістру: Книга.epub і книга.pdf автоматично не об'єднуються. Копії в різних теках, файли з різними іменами або лише однаковими автором і заголовком теж не об'єднуються: так можна зберігати окремі переклади й видання.

Метадані й обкладинка спільні для книги, а шлях, розмір і час зміни зберігаються окремо для кожного файлу. Джерело автоматичних метаданих і формат завантаження за замовчуванням вибираються у стабільному порядку: EPUB → FB2 → FB2.ZIP → MOBI/AZW/AZW3 → CBZ → PDF → решта. За однакового пріоритету вирішує порядок шляхів. Метадані беруться з одного основного файлу, не змішуються з усіх форматів; ручні правки завжди мають пріоритет. Після видалення основного файлу його замінює найкращий із решти форматів. Книга й ручні правки залишаються, доки існує хоча б один її файл. Кнопки завантаження у вебінтерфейсі та посилання OPDS видають кожен формат окремо; /files/<ID книги> завантажує формат за замовчуванням.

Адміністратор у редакторі може:

  • Об'єднати записи, вказавши ID іншої книги: усі її файли перейдуть до поточної книги. Метадані поточної книги зберігаються як ручні, метадані іншого запису замінюються. Об'єднання наявних файлів зберігається під час наступних сканувань.
  • Відокремити файл в окрему книгу: його автоматичні метадані відновлюються, а спільні ручні правки копіюються. Наступне сканування не об'єднує файл назад. Єдиний файл книги відокремлювати не потрібно.

Під час оновлення наявний каталог мігрує автоматично. Формати з однаковим іменем об'єднуються зі збереженням найстарішого ID та несуперечливих ручних полів. Якщо ручні значення одного поля відрізняються, записи залишаються окремими: їх можна об'єднати вручну після перевірки. Перше сканування після міграції повторно читає файли. Перед оновленням варто зробити резервну копію каталогу.

Переміщення всіх форматів до іншої теки або зміна їхнього спільного імені створює нову книгу: ручні метадані за новим шляхом автоматично не переносяться. Видалення книги з каталогу не видаляє файли, якщо не вибрати «Також видалити всі файли книги з диска»; без цієї опції наступне сканування імпортує їх знову.

Повторне сканування та ручні правки

Якщо розмір і час зміни файлу не змінилися, сканер не перечитує його метадані. Після зміни LITEOPDS_FOLDER_SERIES для вже проіндексованої книги можна натиснути «Оновлювати метадані з файлу» в редакторі та запустити сканування; це також скидає пріоритет усіх її ручних полів.

Після помилки обходу бібліотеки, читання файлу, запису в БД або скасування сканування непобачені книги не видаляються з каталогу. Після повного успішного обходу записи відсутніх файлів прибираються. Помилка парсингу наявної книги зберігає її попередні метадані, щоб пошкоджений файл не затирав назву чи автора.

Ручні поля, включно з порожніми значеннями й видаленою обкладинкою, мають пріоритет над метаданими файлу. У редакторі кнопка «Оновлювати метадані з файлу» скидає цей пріоритет; наступне сканування повторно читає файл.

У збірці metadata-tools автоматична обкладинка PDF — це рендеринг першої видимої сторінки через pdftoppm, а не найбільший JPEG усередині файлу: PDF може містити старі обкладинки або великі ілюстрації з інших сторінок. Без Poppler або якщо рендеринг не вдався використовується попередній спосіб витягування JPEG. Після оновлення наступне сканування одноразово перечитає PDF, навіть незмінені; ручні поля й завантажені обкладинки каталогу залишаться недоторканими. Обкладинки та мініатюри перевіряються за ETag вмісту, а посилання в вебкаталозі змінюються після заміни зображення, щоб браузер не показував старий кеш.

Під час оновлення зі старої версії всі наявні метадані зберігаються як ручні: стара схема не дозволяє визначити, які саме поля редагувалися. За потреби скиньте цей пріоритет для конкретної книги. Історія зберігає останні 100 сканувань, лог одного сканування обмежений приблизно 1 МіБ.

Пам'ять після сканування

Після завершення обходу, збереження журналу та очищення історії сервіс відкидає буфер журналу, звільняє SQLite-кеш і викликає debug.FreeOSMemory(). Це працює також для скасованого або невдалого сканування. У логах контейнера з'являється scan: memory after cleanup з показниками Go: heap_alloc — живі об'єкти, heap_committed — пам'ять купи, ще не повернена ОС, heap_released — повернена ОС пам'ять. Це не загальна RAM контейнера й не пам'ять зовнішніх PDF-інструментів.

RAM не обов'язково повертається точно до значення до запуску: у процесу залишаються робочі структури й завантажені бібліотеки, а Linux кешує прочитані файли. Порівнюйте docker stats --no-stream liteopds, RSS процесу та anon/file у /sys/fs/cgroup/memory.stat (cgroup v2). Docker CLI віднімає неактивний файловий кеш, тоді як сирі cgroup-показники включають його (документація Docker). Не потрібно скидати системний кеш Linux або перезапускати сервіс після кожного сканування. Для підозри на витік важливе зростання після кількох однакових сканувань у стані простою, а не лише різниця з холодним запуском.

Пошук метаданих у зовнішніх джерелах

Кнопка «Знайти метадані» доступна адміністратору в картці книги, звичайному та файловому редакторах. Назва, автори, мова й ISBN підставляються з каталогу; їх можна уточнити перед пошуком. Запити виконуються лише після натискання кнопки, не під час відкриття сторінки або сканування. Зовнішнім джерелам передаються пошукові поля, але не файли книг, назви тек, паролі чи текст перших сторінок.

Перші джерела:

  • Open Library — без API-ключа. Результати стосуються конкретних видань: рік, видавництво та ISBN не збираються із різних видань твору. Опис/теми твору можуть використовуватись як доповнення; це зазначено у звіті.
  • Google Books — після налаштування серверного API-ключа. Без ключа джерело показується вимкненим.
  • Лабораторія, Vivat та КСД — українські метадані за посиланням на конкретну книгу, без API-ключів. Це не автоматичний пошук у книгарнях: їхні внутрішні API не використовуються, каталог не обходиться масово. Сторінки пошуку, авторів і серій не підтримуються.

Спочатку шукається ISBN, якщо його вказано. Якщо джерело не знаходить ISBN, пошук повторюється за назвою та автором, потім без автора; якщо потрібно — із коротшою назвою без примітки в кінцевих дужках та суфікса відомої серії. Google Books додатково пробує слова назви без вимоги точної фрази. Не більше п'яти запитів до кожного джерела в межах 30 секунд; помилки й вичерпання квоти не запускають повторні пошуки. Ширші збіги мають окрему позначку та попередження. Open Library отримує мовний пріоритет (lang=uk для української), Google Books спочатку обмежує пошук назви вибраною мовою, а останню ширшу спробу виконує без мовного обмеження. ISBN не обмежується мовою. «Будь-яка мова» вимикає пріоритет; мова результату, що відрізняється від запиту, має попередження. До 6 кандидатів Open Library і до 12 Google Books. Назва, мова, рік і ISBN допомагають вибрати потрібне видання; результат пошуку не є гарантією відповідності. Недоступність або вичерпання квоти одного джерела не приховує результати іншого.

Українські видання та імпорт за посиланням

Якщо української книги немає в міжнародних каталогах, у блоці «Завантажити зі сторінки книги» вставте HTTPS-посилання, наприклад:

  • https://laboratory.ua/products/ty-ya-vona — «Ти, я, вона», Сью Вотсон;
  • https://vivat.com.ua/product/strilets/ — «Стрілець», Стівен Кінг;
  • https://vivat.com.ua/product/kriz-chas/ — «Крізь час», Стівен Кінг;
  • https://ksd.ua/product/nichna-zmina-2 — «Нічна зміна», Стівен Кінг.

Натисніть «Завантажити метадані», перегляньте порівняння та позначте поля для імпорту. Ця кнопка лише читає сторінку; сама по собі не змінює каталог або файли. Для «Темної вежі» беріть сторінку конкретного тому: назви «Шукач» / «Стрілець» і ISBN різних перевидань можуть відрізнятися.

Читаються наявні назва, автори, мова, рік, видавництво, серія, жанри, ISBN, перекладачі, оригінальна назва/мова, опис та обкладинка. Відсутні дані не вигадуються. Сторінки Open Library (/books/OL…M) і Google Books (/books?id=…) також підтримуються; для Google Books потрібен той самий серверний API-ключ.

Для КСД використовуйте актуальні адреси https://ksd.ua/product/…. Доменні варіанти www.ksd.ua, bookclub.ua, www.bookclub.ua з таким самим шляхом /product/… канонізуються без мережевих перенаправлень. Старі адреси bookclub.ua/catalog/… не зіставляються навмання: відкрийте актуальну сторінку книги й скопіюйте нове посилання. Обкладинки читаються лише з ksd.ua/storage/products/covers/…; дозволений параметр версії v має бути числовим.

Метадані книгарні можуть стосуватися паперового видання або містити помилки, зокрема ISBN оригіналу замість перекладу. Перевіряйте їх перед імпортом. ISBN українських книгарень не позначається автоматично навіть для порожнього поля: після перевірки виберіть його вручну. Зміна структури сторінки, CAPTCHA або HTTP 403 показуються як помилка; блокування не обходиться. Читаються лише дозволені домени джерел, а адреса канонізується з ID книги: параметри відстеження та довільні URL не передаються. HTML обмежений 2 МіБ, кеш має спільний з API ліміт; JavaScript не виконується, приватні адреси й перенаправлення на інші домени заборонені.

Після вибору результату доступні:

  • Порівняння з каталогом і редагування знайдених значень. Автоматично позначаються лише порожні поля. Наявні замінюються тільки за явного вибору; непозначені залишаються без змін.
  • Зіставлення мов/жанрів/тегів із вашими «Списками». Невідомі значення не додаються автоматично та не очищають поточні жанри: виберіть відповідники або додайте потрібні значення у «Списки» та повторно відкрийте результат.
  • Перегляд і завантаження обкладинки в каталог за явного вибору. Помилка обкладинки скасовує весь імпорт, не залишаючи частково збережених полів. Зображення повністю перевіряється; ліміт — 10 МіБ і 16 мегапікселів.
  • «Імпортувати в каталог» — лише вибрані поля/обкладинка; книги на диску не змінюються. Можна використовувати звичайний образ, бібліотеку :ro та LITEOPDS_METADATA_WRITE=false.
  • «Перенести в редактор файлів» — вибрані поля підставляються у форму та позначаються для запису. Запис виконується лише через попередній перегляд і підтвердження, з чинними резервними копіями. Обкладинку спершу потрібно імпортувати в каталог, а потім окремо позначити її запис у файловому редакторі. Для перенесених даних підстановка шаблонів вимкнена: дужки в назвах і описах зберігаються дослівно. Підстановку можна окремо ввімкнути у формі.
  • «Завантажити JSON» — вибрані значення з посиланням та ідентифікатором джерела. API-ключі й дозволи на обкладинки до файлу не потрапляють.

Налаштування .env для Docker Compose:

LITEOPDS_GOOGLE_BOOKS_API_KEY=your-google-books-api-key
LITEOPDS_METADATA_CONTACT=library-admin@example.com

Google Books API потрібно ввімкнути для свого проєкту Google Cloud і створити ключ; зберігайте його на сервері, не комітьте .env і не передавайте ключ у браузер. docker-compose.yml уже передає ці змінні контейнеру. Перебудуйте та перестворіть контейнер із тими самими Compose override-файлами, які використовуєте зараз. LITEOPDS_METADATA_CONTACT необов’язковий; це реальний контактний email у User-Agent для правил Open Library.

Серверу потрібен вихідний HTTPS/443 та DNS. Для пошуку використовуються лише дозволені хости, із блокуванням приватних IP і зовнішніх перенаправлень; проксі з HTTP_PROXY/HTTPS_PROXY не використовуються. Ліміт — один запит за секунду на джерело в межах процесу, до 6 результатів Open Library і 12 Google Books. Є тайм-аути, обмеження відповідей та спільний кеш публічних JSON/HTML-відповідей до 8 МіБ / 64 записів на 10 хвилин; обкладинки не кешуються в RAM.

Імпорт захищений від конкурентних змін у каталозі, навіть у межах однієї секунди. Якщо книга змінилася після відкриття сторінки, оновіть сторінку та перевірте вибір ще раз. Масового фонового збору метаданих немає.

Перевірка метаданих однієї книги

Журнал запису та структура бібліотеки

На сторінці результату запису та в «Історії змін» є «Показати журнал». Для нових операцій журнал містить команди ebook-meta / ExifTool / qpdf, stdout і stderr (також для успішних команд), а при помилці перевірки — очікуване та фактично прочитане значення поля. Це допомагає розрізнити відмову інструмента, невідповідність метаданих і помилку файлової системи. Журнал доступний лише адміністратору, завантажується за запитом та обмежений 64 КіБ на операцію; окремий вивід команди скорочується до 16 КіБ. Старі операції мають лише раніше збережену помилку: їхній повний журнал неможливо відновити заднім числом. Змінні оточення не записуються.

У файловому редакторі за замовчуванням увімкнено «Після успішного запису перемістити всі формати до теки автора / серії». Можна вимкнути цю опцію перед переглядом. Структура така сама, як для upload: Автор/Серія/файл.ext, або Автор/файл.ext без серії; кілька авторів утворюють одну теку з їхніми іменами через &. Назви файлів, включно з .fb2.zip, залишаються без змін. Використовуються поля каталогу та зміни, які вибрані формати підтримують і зберегли; непідтримуване поле не змінює структуру.

Попередній перегляд показує всі шляхи «звідки → куди». Переміщуються всі проскановані формати цієї книги, навіть не вибрані для запису; їхній вміст не змінюється. Не більше 20 форматів однієї книги. Книга й файли зберігають ID, ручні значення, історію та URL завантаження; повторне сканування не створює дублікати. Звичайне збереження тільки в каталог, імпорт метаданих і сканування самі файли не переміщують.

Переміщення починається тільки після успішного запису всіх вибраних файлів. Колізії назв, чужі записи каталогу, зміни файлів після перегляду та symlink відхиляються; існуючі файли не перезаписуються. Перенесення використовує безпечні hard link + unlink у межах бібліотеки, тому ціль має бути на тій самій файловій системі з підтримкою hard links. Якщо файловий або SQL-крок не вдається, шляхи відкочуються; уже успішно записані метадані залишаються з резервними копіями та окремою помилкою перенесення у журналі. Порожні старі теки не видаляються автоматично.

Відновлення операції повертає її файл і вибрані поля каталогу. Якщо книга була переміщена, також повертає всі формати до старих шляхів, але не змінює вміст інших форматів. Змінений стороннім інструментом формат або зайнятий старий шлях блокують відновлення до будь-яких змін.

Для PDF автори читаються з явно зазначеного XMP-dc:Creator, окремо від PDF:Creator (програма-генератор документа). Це усуває хибну помилку перевірки авторів у PDF, де присутні обидва однойменні теги.

Порівняння каталогу з вихідними файлами

Адміністратор може натиснути «Перевірити метадані» у картці книги або звичайному редакторі. Перевірка читає поточні вихідні файли всіх форматів цієї книги, а не збережені під час сканування знімки, та порівнює їх зі збереженими значеннями в БД. Незбережені правки у формі не враховуються.

Для кожного файлу звіт показує розбіжності у стовпцях «У базі» / «У файлі», ручні правки, збіги та непідтримувані поля. Рік порівнюється без місяця й дня; порядок жанрів і тегів, зайві пробіли та дефіси в ISBN не створюють розбіжностей. Дані, виведені з назв тек, і ручні зміни можуть навмисно відрізнятися від файлу. Відсутній, пошкоджений або непідтримуваний файл не позначається як збіг.

Підтримуються EPUB, FB2, FB2.ZIP та PDF. Для PDF потрібен ExifTool із збірки metadata-tools; непідтримувані PDF-поля, зокрема серія, виводяться окремо. Інші формати поки позначаються як неперевірені. Обкладинки не порівнюються.

Перевірка не змінює книги, базу чи історію редагування, працює з бібліотекою :ro та з LITEOPDS_METADATA_WRITE=false. Використовуються тимчасові копії в data (який має бути поза бібліотекою); символічні посилання та файли понад 256 МіБ не читаються. Під час сканування або файлової операції потрібно дочекатися її завершення та повторити перевірку. Масової перевірки поки немає.

API: POST /api/books/{id}/metadata/check, лише для адміністратора, із X-CSRF-Token. Звіт не кешується й актуальний на момент перевірки; після зовнішніх змін файлів перевірку потрібно запустити знову.

Просунутий редактор: метадані у вихідних файлах

Звичайний редактор картки змінює лише каталог. «Просунутий редактор файлів» (/app/books/<ID>/advanced, або «Редактор файлів» у меню адміністратора) окремо записує вибрані поля у вихідні файли, а потім оновлює каталог із перевірених даних. Завантаження у вебінтерфейсі та OPDS отримують саме ці оновлені файли. Сам текст книги не редагується; титульні сторінки не переверстуються.

Увімкнення у Docker

Стандартний distroless-образ не містить зовнішніх інструментів, а бібліотека монтується read-only. Для запису використовуйте окремий, значно більший образ:

LITEOPDS_UID=$(id -u) LITEOPDS_GID=$(id -g) \
  docker compose -f docker-compose.yml -f docker-compose.metadata.yml up -d --build

Цей override використовує Dockerfile.metadata, вмикає LITEOPDS_METADATA_WRITE=true і монтує ./library:/library:rw. Образ містить Calibre (ebook-meta), ExifTool, qpdf, Poppler та Tesseract із мовами ukr, eng.

Metadata-образ містить CLI-оточення Calibre, а не повну настільну програму: не встановлюються Qt WebEngine, Qt-модулі мультимедіа та Python-пакети для конвертації шрифтів із їхніми залежностями. Пакет Debian calibre-bin і потрібні Python/Qt-бібліотеки встановлюються штатно; оригінальні модулі, ресурси, ліцензії, ebook-meta та його службовий calibre-parallel беруться з пакета тієї самої версії джерел у репозиторії Debian. Підтримувані сервісом операції EPUB/PDF/FB2 та OCR залишаються доступними; цей образ не призначений для GUI Calibre чи ebook-convert.

Фінальний образ збирається FROM scratch: з підготовчого Debian-оточення копіюються CLI-інструменти та їхні транзитивні ELF-залежності, повні дерева Python/Perl-модулів, ресурси Calibre, Qt-плагіни для зображень, шрифти, сертифікати, часові пояси й OCR-моделі. Не переносяться невикористані GPU/відеобібліотеки Mesa/LLVM/FFmpeg та пакетний менеджер. Версії вихідних пакетів і перелік відібраних файлів збережено у /usr/share/liteopds/runtime-manifest.json, а метадані пакетів — у /var/lib/dpkg/status для сканерів вразливостей. Оновлення інструментів виконуються перебудовою образу, не через apt усередині контейнера.

У контрольній збірці linux/amd64 від 2026-10-12 metadata-образ зменшився з попередніх 820 MB до приблизно 421 MB (ще на 49%, близько 399 MB). Порівняно з початковими 1,47 GB це зменшення приблизно на 71%. Стандартний distroless-образ із пошуком зовнішніх метаданих займає приблизно 16,4 MB. Це сума нестиснутих шарів за docker image inspect --format '{{.Size}}', а не розмір архіву або кешу BuildKit; значення залежать від архітектури та оновлень пакетів. Після перебудови старі образи й кеш автоматично не видаляються.

UID/GID контейнера повинен мати доступ до data, читання файлів і запис у батьківські теки книг — оригінали замінюються атомарно, а не змінюються на місці. Не використовуйте chmod 777; виправте власника/права конкретних каталогів.

Для локального запуску встановіть потрібні інструменти засобами своєї ОС та задайте LITEOPDS_METADATA_WRITE=true. FB2/FB2.ZIP записуються без зовнішніх утиліт. Підказки доступні навіть при вимкненому записі; PDF/OCR потребують відповідних утиліт незалежно від цього прапорця. Каталог data для редактора має існувати поза коренем бібліотеки, щоб копії не потрапляли у сканування.

Поля та формати

Є назва, автори, серія/номер, мова, видавництво, рік видання, жанри, теги, опис, ISBN, DOI, підзаголовок, оригінальна назва, перекладачі, редактори, сортування авторів/назви, видання, права, джерело, виробник електронної книги, місце видання й мова оригіналу.

Мова та мова оригіналу вибираються випадаючими списками, по одному значенню або «Не вказано». Жанри — випадаючий список із прапорцями: можна вибрати кілька або зняти всі. Це працює у звичайному та просунутому редакторах. У відкритому списку введіть початок назви, щоб відфільтрувати варіанти без урахування регістру; мову також можна знайти за початком коду (uk, en). Пошук не змінює вже вибрані значення, навіть якщо вони приховані фільтром, і сам по собі не позначає поле для запису.

Адміністратор керує довідниками в меню «Списки» (/app/admin/options): додавання, редагування значення/назви, ввімкнення/вимкнення та видалення пунктів. Для кожного пункту показано кількість логічних книг, де він призначений; для мов враховується також мова оригіналу, а різні формати однієї книги не подвоюють підрахунок. Перед вимкненням уживаного пункту та перед видаленням сервер перевіряє актуальне використання і запитує підтвердження. Скасування нічого не змінює. Видалені пункти не відновлюються після перезапуску. Код мови — наприклад uk, en, uk-UA; значення жанру чи тегу — назва без коми. Початкові списки містять поширені мови, жанри FB2 та значення наявного каталогу. Налаштування зберігаються в БД й не скидаються після перезапуску. Зміна довідника не переписує книги або файли. Неактивні/видалені/імпортовані значення залишаються доступними у вже призначених книгах; щоб масово змінити книги, використовуйте просунутий редактор. Невідомі жанри/мови/теги з підказок спочатку додайте в довідник, потім повторно відкрийте редактор.

Теги вибираються множинним випадаючим списком із пошуком за початком назви або значення — як жанри, у звичайному та файловому редакторах. На вкладці «Теги» можна додавати, перейменовувати, вимикати й видаляти пункти та обирати колір із палітри 36 базових кольорів. У каталозі (картки, список і таблиця) та на сторінці книги позначки мають цей колір і контрастний текст; натискання фільтрує каталог за точним тегом. Зміна кольору застосовується при наступному відкритті сторінки, не потребує пересканування та не змінює файли книг.

Під час оновлення наявні теги одноразово додаються до довідника з нейтральним сірим кольором. Неактивні теги зберігають колір у вже позначених книгах. Видалення пункту не видаляє тег із книг, але позначка стає нейтральною; видалені пункти не створюються заново під час рестарту. Теги з нових просканованих файлів, яких немає у довіднику, теж відображаються нейтрально і залишаються доступними у відповідних книгах. Колір зберігається лише у довіднику сервісу; редактор файлів записує у вихідні файли самі теги, а не оформлення вебінтерфейсу.

Рік видання задається як YYYY (0001–9999) або залишається порожнім. Каталог показує тільки рік зі старих повних дат; дати в оригіналах не переписуються, доки це поле не вибране для запису. Якщо старі метадані містили кілька мов, для картки береться перша. Після оновлення наступне сканування перечитує всі жанри FB2/FB2.ZIP, не скидаючи ручних правок.

Після оновлення перше сканування перечитує метадані наявних файлів, щоб імпортувати додаткові поля; попередні ручні правки зберігаються.

Формат Запис та особливості
EPUB ebook-meta + точкове редагування OPF для додаткових полів та явного очищення. Стандартні DC/OPF, ролі авторів/перекладачів/редакторів, серія Calibre; поля без стандартного аналога — liteopds:*. Кожен вибраний жанр записується окремим dc:subject, додаткова позначка відділяє жанри від тегів. Обкладинка JPEG/PNG записується через Calibre.
FB2 / FB2.ZIP Власний XML-редактор зі збереженням тексту та ілюстрацій; нативні поля й custom-info info-type="liteopds:*" для додаткових даних. Рік видання — publish-info/year. Теги — keywords; стандартні жанри записуються окремими genre, довільні назви додатково зберігаються в liteopds-полі. Обкладинка вбудовується як binary. У ZIP має бути рівно один FB2.
PDF ExifTool + qpdf: назва, автори, опис, жанр (PDF:Subject), теги, мова, видавництво, дата, ISBN/DOI (PRISM/XMP), права й джерело. Інші поля та обкладинка не записуються й явно зазначаються в перегляді. qpdf прибирає старі інкрементальні метадані.
Інші формати Як і раніше, доступні для сканування/завантаження та редагування картки в БД; запис у файли не підтримується.

Підтримка розширених полів залежить від читалки: невідомі liteopds:* поля вона може не показувати. Підписані/зашифровані PDF, EPUB із encryption.xml (у тому числі обфускованими шрифтами) або підписами, XML із DTD/підписами та небезпечні ZIP-архіви не редагуються. Calibre може нормалізувати службові метадані EPUB. EPUB package-ідентифікатор не можна очистити, якщо ним є ISBN/DOI.

Робочий процес

  1. Відкрийте одну книгу або позначте до 10 книг у бібліотеці та натисніть «Редагувати вибрані книги у файлах». Виберіть потрібні формати, до 20 файлів.
  2. Для однієї книги отримайте підказки з метаданих, назви та початку файлу: перші три розділи EPUB, початок FB2 або перші три сторінки PDF. Для сканованого PDF окремо увімкніть OCR перших двох сторінок. Підказки з позначених рядків та ISBN потрібно приймати вручну; це не автоматичне визначення достовірних назви/автора.
  3. За потреби задайте RE2-шаблон назви з іменованими групами, наприклад ^(?P<authors>.+) - (?P<title>.+)$. Без нього назва Автор - Книга.fb2 дає просте припущення щодо автора й назви.
  4. Позначте лише поля для зміни. Порожнє позначене поле очищається; назва не може бути порожньою, порожній номер серії означає 0. Рік видання: YYYY; для ISBN перевіряється контрольна сума. Мова — одна, жанрів і тегів може бути кілька. Шаблони {title}, {authors}, {series}, {isbn} та інші ключі полів підставляються окремо зі спільних метаданих каталогу кожної книги.
  5. Перевірте «У файлі зараз → Після запису», обмеження кожного формату й підтвердьте запис. Перегляд дійсний 20 хвилин, прив'язаний до користувача, стану каталогу й повного SHA-256 файлів та не може використовуватися повторно.
  6. В «Історії та відновленні» можна повернути повний оригінал конкретного файлу та прочитані з нього значення змінених полів каталогу. Інші формати книги й окремо завантажена обкладинка каталогу при цьому не відновлюються.

Пакет пишеться послідовно: при помилці наступні файли не обробляються, вже успішні записи не відкочуються. Їх можна відновити окремо з історії. Вибрані файли отримують однакові спільні метадані, але непідтримувані поля залишаються незмінними. Файл без жодної підтримуваної вибраної зміни пропускається з явним повідомленням; пакет лише з таких файлів не приймається. Для каталогу кожне поле береться з першого успішно записаного вибраного формату у порядку пріоритету форматів; каталог показує рік видання. Перегляд показує точне значення у файлі; PDF доповнює рік до 1 січня, оскільки його поле дати потребує повної дати. Ручні поля каталогу мають пріоритет при скануванні; для додаткових полів пріоритет записується окремо для кожного зміненого поля.

Безпека та резервні копії

Інструменти працюють лише з приватною тимчасовою копією, без shell. Оригінал замінюється після повторного читання й перевірки значень; при помилці SQLite сервіс намагається повернути оригінал. Повні копії та журнал операцій зберігаються у data/metadata-backups і SQLite; prepared означає незавершену операцію, наприклад після аварійного завершення процесу. Відновлення дозволено лише за збігу SHA-256 поточного файлу з записаною версією та цілісності резервної копії. Резервні копії не видаляються автоматично; історія показує останні 100 записів. Забезпечте місце для повних копій і тимчасових файлів. Після аварії можуть залишатися data/.metadata-work-*; прибирайте їх лише коли сервіс зупинено.

Запис недоступний одночасно зі скануванням або іншими змінами бібліотеки через сервіс. Символічні посилання не редагуються; файли обмежені 256 МіБ, FB2/XML — 64 МіБ, ZIP — 20 000 записів / 1 ГіБ розпакованих даних. Обкладинки для запису — JPEG/PNG до 5 МіБ. Одна утиліта має таймаут 60 секунд, вебоперація — 5 хвилин. Не запускайте паралельно інші редактори/синхронізацію цієї бібліотеки: перевірка хешу перед атомарною заміною зменшує ризик, але не є блокуванням зовнішніх програм. Також не запускайте два екземпляри сервісу, що записують одну бібліотеку.

Ім'я скачаного файлу формується з метаданих, типово Автор — Назва.epub. Без автора використовується лише назва. LITEOPDS_DOWNLOAD_NAME дозволяє, наприклад, {authors} — {title} ({published}); некоректний/порожній результат повертається до назви. Це не перейменовує файли на диску й не змінює групування форматів.

Перевірки розробника

go test -race ./...
go vet ./...
npm --prefix frontend test
npm --prefix frontend run build
npm --prefix frontend run test:e2e
CGO_ENABLED=0 go build ./cmd/server
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...

CI перевіряє форматування, залежності, тести з race detector, пошук у списках мов/жанрів/тегів, збірку без CGO, вразливості та Docker-образ. Окремий job збирає metadata-образ і запускає інтеграційні тести зі справжніми Calibre, ExifTool, Poppler, qpdf і Tesseract без мережі, з read-only rootfs і тимчасовим /tmp. Прапорець LITEOPDS_METADATA_INTEGRATION=1 забороняє пропуск перевірок через відсутні CLI-інструменти або OCR-мови. CI також перевіряє бюджети нестиснутих образів: 32 MB для стандартного та 500 MB для metadata-образу. Додатково перевіряються збирач залежностей, PNG/JPEG-обкладинки EPUB, TLS, шрифти та запуск Go-сервісу і нового frontend від непривілейованого користувача з тимчасовими даними.

Обмеження

  • Вбудованої читалки немає за вимогою
  • DJVU метадані не витягуються (лише fallback з імені файлу)
  • Формат RAR (CBR) не розпакується без зовнішнього інструмента — метадані не витягуються, але файли доступні для завантаження
  • CB7 також доступний для завантаження, але метадані 7z-архівів не витягуються
  • XML-метадані EPUB/CBZ обмежені 1 МіБ, вбудовані обкладинки EPUB/CBZ — 5 МіБ, FB2 — 64 МіБ прочитаних даних; завеликі метадані не імпортуються