mirror of
https://github.com/miniflux/v2.git
synced 2025-09-30 19:22:11 +00:00
feat: multi db support
This commit is contained in:
parent
10b2b36895
commit
e9b8279241
27 changed files with 2770 additions and 1464 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
use flake . --show-trace
|
||||||
|
dotenv_if_exists .env
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@
|
||||||
*.deb
|
*.deb
|
||||||
*.rpm
|
*.rpm
|
||||||
miniflux-*
|
miniflux-*
|
||||||
|
.direnv/
|
||||||
|
|
44
.helix/languages.toml
Normal file
44
.helix/languages.toml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
[language-server]
|
||||||
|
nil = { command = "nil" }
|
||||||
|
taplo = { command = "taplo", args = ["lsp", "stdio"] }
|
||||||
|
yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
|
||||||
|
marksman = { command = "marksman", args = ["server"] }
|
||||||
|
vscode-json-language-server = { command = "vscode-json-language-server", args = [
|
||||||
|
"--stdio",
|
||||||
|
], config = { json = { validate = { enable = true } } } }
|
||||||
|
gopls = { command = "gopls", config = { staticcheck = true } }
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "nix"
|
||||||
|
auto-format = true
|
||||||
|
formatter = { command = "nixpkgs-fmt" }
|
||||||
|
language-servers = ["nil"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "toml"
|
||||||
|
auto-format = true
|
||||||
|
language-servers = ["taplo"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "yaml"
|
||||||
|
auto-format = true
|
||||||
|
formatter = { command = "prettier", args = ["--parser", "yaml"] }
|
||||||
|
language-servers = ["yaml-language-server"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "json"
|
||||||
|
auto-format = true
|
||||||
|
formatter = { command = "prettier", args = ["--parser", "json"] }
|
||||||
|
language-servers = ["vscode-json-language-server"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "markdown"
|
||||||
|
auto-format = true
|
||||||
|
formatter = { command = "prettier", args = ["--parser", "markdown"] }
|
||||||
|
language-servers = ["marksman"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "go"
|
||||||
|
auto-format = true
|
||||||
|
language-servers = ["gopls"]
|
||||||
|
formatter = { command = "gofumpt" }
|
|
@ -37,17 +37,21 @@ When reporting bugs:
|
||||||
- **Git**
|
- **Git**
|
||||||
- **Go >= 1.24**
|
- **Go >= 1.24**
|
||||||
- **PostgreSQL**
|
- **PostgreSQL**
|
||||||
|
- **CockroachDB**
|
||||||
|
- **SQLite**
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
1. **Fork the repository** on GitHub
|
1. **Fork the repository** on GitHub
|
||||||
2. **Clone your fork locally:**
|
2. **Clone your fork locally:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/miniflux.git
|
git clone https://github.com/YOUR_USERNAME/miniflux.git
|
||||||
cd miniflux
|
cd miniflux
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Build the application binary:**
|
3. **Build the application binary:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make miniflux
|
make miniflux
|
||||||
```
|
```
|
||||||
|
@ -59,15 +63,11 @@ When reporting bugs:
|
||||||
|
|
||||||
### Database Setup
|
### Database Setup
|
||||||
|
|
||||||
For development and testing, you can run a local PostgreSQL database with Docker:
|
For development and testing, you can run PostgreSQL and CockroachDB via docker compose:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start PostgreSQL container
|
# Start PostgreSQL and CockroachDB containers
|
||||||
docker run --rm --name miniflux2-db -p 5432:5432 \
|
docker compose -d
|
||||||
-e POSTGRES_DB=miniflux2 \
|
|
||||||
-e POSTGRES_USER=postgres \
|
|
||||||
-e POSTGRES_PASSWORD=postgres \
|
|
||||||
postgres
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE_URL` environment variable accordingly.
|
You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE_URL` environment variable accordingly.
|
||||||
|
@ -77,20 +77,27 @@ You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE
|
||||||
### Code Quality
|
### Code Quality
|
||||||
|
|
||||||
1. **Run the linter:**
|
1. **Run the linter:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make lint
|
make lint
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires `staticcheck` and `golangci-lint` to be installed.
|
Requires `staticcheck` and `golangci-lint` to be installed.
|
||||||
|
|
||||||
2. **Run unit tests:**
|
2. **Run unit tests:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run integration tests:**
|
3. **Run integration tests:**
|
||||||
```bash
|
```bash
|
||||||
make integration-test
|
make integration-test-postgresql
|
||||||
make clean-integration-test
|
make clean-integration-test-postgresql
|
||||||
|
make integration-test-cockroachdb
|
||||||
|
make clean-integration-test-cockroachdb
|
||||||
|
make integration-test-sqlite
|
||||||
|
make clean-integration-test-sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
@ -103,6 +110,7 @@ You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE
|
||||||
### Cross-Platform Support
|
### Cross-Platform Support
|
||||||
|
|
||||||
Miniflux supports multiple architectures. When making changes, ensure compatibility across:
|
Miniflux supports multiple architectures. When making changes, ensure compatibility across:
|
||||||
|
|
||||||
- Linux (amd64, arm64, armv7, armv6, armv5)
|
- Linux (amd64, arm64, armv7, armv6, armv5)
|
||||||
- macOS (amd64, arm64)
|
- macOS (amd64, arm64)
|
||||||
- FreeBSD, OpenBSD, Windows (amd64)
|
- FreeBSD, OpenBSD, Windows (amd64)
|
||||||
|
@ -155,11 +163,13 @@ When creating a pull request, please include:
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### Unit Tests
|
### Unit Tests
|
||||||
|
|
||||||
- Write unit tests for new functions and methods
|
- Write unit tests for new functions and methods
|
||||||
- Ensure tests are fast and don't require external dependencies
|
- Ensure tests are fast and don't require external dependencies
|
||||||
- Aim for good test coverage
|
- Aim for good test coverage
|
||||||
|
|
||||||
### Integration Tests
|
### Integration Tests
|
||||||
|
|
||||||
- Add integration tests for new API endpoints
|
- Add integration tests for new API endpoints
|
||||||
- Tests run against a real PostgreSQL database
|
- Tests run against a real PostgreSQL database
|
||||||
- Ensure tests clean up after themselves
|
- Ensure tests clean up after themselves
|
||||||
|
|
71
Makefile
71
Makefile
|
@ -3,7 +3,6 @@ DOCKER_IMAGE := miniflux/miniflux
|
||||||
VERSION := $(shell git describe --tags --exact-match 2>/dev/null)
|
VERSION := $(shell git describe --tags --exact-match 2>/dev/null)
|
||||||
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)'"
|
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)'"
|
||||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
|
||||||
DOCKER_PLATFORM := amd64
|
DOCKER_PLATFORM := amd64
|
||||||
|
|
||||||
export PGPASSWORD := postgres
|
export PGPASSWORD := postgres
|
||||||
|
@ -28,8 +27,12 @@ export PGPASSWORD := postgres
|
||||||
add-string \
|
add-string \
|
||||||
test \
|
test \
|
||||||
lint \
|
lint \
|
||||||
integration-test \
|
integration-test-postgresql \
|
||||||
clean-integration-test \
|
clean-integration-test-postgresql \
|
||||||
|
integration-test-cockroachdb \
|
||||||
|
clean-integration-test-cockroachdb \
|
||||||
|
integration-test-sqlite \
|
||||||
|
clean-integration-test-sqlite \
|
||||||
docker-image \
|
docker-image \
|
||||||
docker-image-distroless \
|
docker-image-distroless \
|
||||||
docker-images \
|
docker-images \
|
||||||
|
@ -103,17 +106,18 @@ lint:
|
||||||
staticcheck ./...
|
staticcheck ./...
|
||||||
golangci-lint run --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
golangci-lint run --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||||
|
|
||||||
integration-test:
|
integration-test-postgresql:
|
||||||
psql -U postgres -c 'drop database if exists miniflux_test;'
|
psql -U postgres -p 5432 -d postgres -c 'drop database if exists miniflux2;'
|
||||||
psql -U postgres -c 'create database miniflux_test;'
|
psql -U postgres -p 5432 -d postgres -c 'create database miniflux2;'
|
||||||
|
|
||||||
DATABASE_URL=$(DB_URL) \
|
go build -o miniflux-test main.go
|
||||||
|
DATABASE_URL='postgres://postgres:postgres@localhost:5432/miniflux2?sslmode=disable' \
|
||||||
ADMIN_USERNAME=admin \
|
ADMIN_USERNAME=admin \
|
||||||
ADMIN_PASSWORD=test123 \
|
ADMIN_PASSWORD=test123 \
|
||||||
CREATE_ADMIN=1 \
|
CREATE_ADMIN=1 \
|
||||||
RUN_MIGRATIONS=1 \
|
RUN_MIGRATIONS=1 \
|
||||||
LOG_LEVEL=debug \
|
LOG_LEVEL=debug \
|
||||||
go run main.go >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||||
|
|
||||||
while ! nc -z localhost 8080; do sleep 1; done
|
while ! nc -z localhost 8080; do sleep 1; done
|
||||||
|
|
||||||
|
@ -122,10 +126,57 @@ integration-test:
|
||||||
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
||||||
go test -v -count=1 ./internal/api
|
go test -v -count=1 ./internal/api
|
||||||
|
|
||||||
clean-integration-test:
|
clean-integration-test-postgresql:
|
||||||
@ kill -9 `cat /tmp/miniflux.pid`
|
@ kill -9 `cat /tmp/miniflux.pid`
|
||||||
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
||||||
@ psql -U postgres -c 'drop database if exists miniflux_test;'
|
@ psql -U postgres -d postgres -c 'drop database if exists miniflux2;'
|
||||||
|
|
||||||
|
integration-test-cockroachdb:
|
||||||
|
psql -U postgres -d postgres -p 26257 -c 'drop database if exists miniflux2;'
|
||||||
|
psql -U postgres -d postgres -p 26257 -c 'create database miniflux2;'
|
||||||
|
|
||||||
|
go build -o miniflux-test main.go
|
||||||
|
DATABASE_URL='cockroach://postgres:postgres@localhost:26257/miniflux2?sslmode=disable' \
|
||||||
|
ADMIN_USERNAME=admin \
|
||||||
|
ADMIN_PASSWORD=test123 \
|
||||||
|
CREATE_ADMIN=1 \
|
||||||
|
RUN_MIGRATIONS=1 \
|
||||||
|
LOG_LEVEL=debug \
|
||||||
|
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||||
|
|
||||||
|
while ! nc -z localhost 8080; do sleep 1; done
|
||||||
|
|
||||||
|
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
|
||||||
|
TEST_MINIFLUX_ADMIN_USERNAME=admin \
|
||||||
|
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
||||||
|
go test -v -count=1 ./internal/api
|
||||||
|
|
||||||
|
clean-integration-test-cockroachdb:
|
||||||
|
@ kill -9 `cat /tmp/miniflux.pid`
|
||||||
|
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
||||||
|
@ psql -U postgres -d postgres -c 'drop database if exists miniflux2;'
|
||||||
|
|
||||||
|
integration-test-sqlite:
|
||||||
|
go build -o miniflux-test main.go
|
||||||
|
DATABASE_URL='file::memory:?cache=shared&_pragma=foreign_keys(ON)' \
|
||||||
|
ADMIN_USERNAME=admin \
|
||||||
|
ADMIN_PASSWORD=test123 \
|
||||||
|
CREATE_ADMIN=1 \
|
||||||
|
RUN_MIGRATIONS=1 \
|
||||||
|
DEBUG=1 \
|
||||||
|
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||||
|
|
||||||
|
while ! nc -z localhost 8080; do sleep 1; done
|
||||||
|
|
||||||
|
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
|
||||||
|
TEST_MINIFLUX_ADMIN_USERNAME=admin \
|
||||||
|
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
||||||
|
go test -v -count=1 ./internal/api
|
||||||
|
|
||||||
|
clean-integration-test-sqlite:
|
||||||
|
@ kill -9 `cat /tmp/miniflux.pid`
|
||||||
|
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
||||||
|
@ rm miniflux-test
|
||||||
|
|
||||||
docker-image:
|
docker-image:
|
||||||
docker build --pull -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/alpine/Dockerfile .
|
docker build --pull -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/alpine/Dockerfile .
|
||||||
|
|
33
README.md
33
README.md
|
@ -1,13 +1,11 @@
|
||||||
Miniflux 2
|
# Miniflux 2
|
||||||
==========
|
|
||||||
|
|
||||||
Miniflux is a minimalist and opinionated feed reader.
|
Miniflux is a minimalist and opinionated feed reader.
|
||||||
It's simple, fast, lightweight and super easy to install.
|
It's simple, fast, lightweight and super easy to install.
|
||||||
|
|
||||||
Official website: <https://miniflux.app>
|
Official website: <https://miniflux.app>
|
||||||
|
|
||||||
Features
|
## Features
|
||||||
--------
|
|
||||||
|
|
||||||
### Feed Reader
|
### Feed Reader
|
||||||
|
|
||||||
|
@ -19,7 +17,7 @@ Features
|
||||||
- Share individual articles publicly.
|
- Share individual articles publicly.
|
||||||
- Fetches website icons (favicons).
|
- Fetches website icons (favicons).
|
||||||
- Saves articles to third-party services.
|
- Saves articles to third-party services.
|
||||||
- Provides full-text search (powered by Postgres).
|
- Provides full-text search.
|
||||||
- Available in 20 languages: Portuguese (Brazilian), Chinese (Simplified and Traditional), Dutch, English (US), Finnish, French, German, Greek, Hindi, Indonesian, Italian, Japanese, Polish, Romanian, Russian, Taiwanese POJ, Ukrainian, Spanish, and Turkish.
|
- Available in 20 languages: Portuguese (Brazilian), Chinese (Simplified and Traditional), Dutch, English (US), Finnish, French, German, Greek, Hindi, Indonesian, Italian, Japanese, Polish, Romanian, Russian, Taiwanese POJ, Ukrainian, Spanish, and Turkish.
|
||||||
|
|
||||||
### Privacy and Security
|
### Privacy and Security
|
||||||
|
@ -34,7 +32,7 @@ Features
|
||||||
- Supports alternative YouTube video players such as [Invidious](https://invidio.us).
|
- Supports alternative YouTube video players such as [Invidious](https://invidio.us).
|
||||||
- Blocks external JavaScript to prevent tracking and enhance security.
|
- Blocks external JavaScript to prevent tracking and enhance security.
|
||||||
- Sanitizes external content before rendering it.
|
- Sanitizes external content before rendering it.
|
||||||
- Enforces a [Content Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) and a [Trusted Types Policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to only application JavaScript and blocks inline scripts and styles.
|
- Enforces a [Content Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) and a [Trusted Types Policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to only application JavaScript and blocks inline scripts and styles.
|
||||||
|
|
||||||
### Bot Protection Bypass Mechanisms
|
### Bot Protection Bypass Mechanisms
|
||||||
|
|
||||||
|
@ -63,12 +61,12 @@ Features
|
||||||
- Optional touch gesture support for navigation on mobile devices.
|
- Optional touch gesture support for navigation on mobile devices.
|
||||||
- Custom stylesheets and JavaScript to personalize the user interface to your preferences.
|
- Custom stylesheets and JavaScript to personalize the user interface to your preferences.
|
||||||
- Themes:
|
- Themes:
|
||||||
- Light (Sans-Serif)
|
- Light (Sans-Serif)
|
||||||
- Light (Serif)
|
- Light (Serif)
|
||||||
- Dark (Sans-Serif)
|
- Dark (Sans-Serif)
|
||||||
- Dark (Serif)
|
- Dark (Serif)
|
||||||
- System (Sans-Serif) – Automatically switches between Dark and Light themes based on system preferences.
|
- System (Sans-Serif) – Automatically switches between Dark and Light themes based on system preferences.
|
||||||
- System (Serif)
|
- System (Serif)
|
||||||
|
|
||||||
### Integrations
|
### Integrations
|
||||||
|
|
||||||
|
@ -90,7 +88,7 @@ Features
|
||||||
|
|
||||||
- Written in [Go (Golang)](https://golang.org/).
|
- Written in [Go (Golang)](https://golang.org/).
|
||||||
- Single binary compiled statically without dependency.
|
- Single binary compiled statically without dependency.
|
||||||
- Works only with [PostgreSQL](https://www.postgresql.org/).
|
- Works with [PostgreSQL](https://www.postgresql.org/), [CockroachDB](https://www.cockroachlabs.com/) and [SQLite](https://sqlite.org/).
|
||||||
- Does not use any ORM or any complicated frameworks.
|
- Does not use any ORM or any complicated frameworks.
|
||||||
- Uses modern vanilla JavaScript only when necessary.
|
- Uses modern vanilla JavaScript only when necessary.
|
||||||
- All static files are bundled into the application binary using the Go `embed` package.
|
- All static files are bundled into the application binary using the Go `embed` package.
|
||||||
|
@ -109,8 +107,7 @@ Features
|
||||||
- Only uses a couple of MB of memory and a negligible amount of CPU, even with several hundreds of feeds.
|
- Only uses a couple of MB of memory and a negligible amount of CPU, even with several hundreds of feeds.
|
||||||
- Respects/sends Last-Modified, If-Modified-Since, If-None-Match, Cache-Control, Expires and ETags headers, and has a default polling interval of 1h.
|
- Respects/sends Last-Modified, If-Modified-Since, If-None-Match, Cache-Control, Expires and ETags headers, and has a default polling interval of 1h.
|
||||||
|
|
||||||
Documentation
|
## Documentation
|
||||||
-------------
|
|
||||||
|
|
||||||
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
|
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
|
||||||
|
|
||||||
|
@ -130,8 +127,7 @@ The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man
|
||||||
- [Internationalization](https://miniflux.app/docs/i18n.html)
|
- [Internationalization](https://miniflux.app/docs/i18n.html)
|
||||||
- [Frequently Asked Questions](https://miniflux.app/faq.html)
|
- [Frequently Asked Questions](https://miniflux.app/faq.html)
|
||||||
|
|
||||||
Screenshots
|
## Screenshots
|
||||||
-----------
|
|
||||||
|
|
||||||
Default theme:
|
Default theme:
|
||||||
|
|
||||||
|
@ -141,8 +137,7 @@ Dark theme when using keyboard navigation:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Credits
|
## Credits
|
||||||
-------
|
|
||||||
|
|
||||||
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
|
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
|
||||||
- Distributed under Apache 2.0 License
|
- Distributed under Apache 2.0 License
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
# Provides: miniflux
|
# Provides: miniflux
|
||||||
# Required-Start: $syslog $network
|
# Required-Start: $syslog $network
|
||||||
# Required-Stop: $syslog
|
# Required-Stop: $syslog
|
||||||
# Should-Start: postgresql
|
# Should-Start: postgresql cockroachdb
|
||||||
# Should-Stop: postgresql
|
# Should-Stop: postgresql cockroachdb
|
||||||
# Default-Start: 2 3 4 5
|
# Default-Start: 2 3 4 5
|
||||||
# Default-Stop: 0 1 6
|
# Default-Stop: 0 1 6
|
||||||
# Short-Description: A rss reader
|
# Short-Description: A rss reader
|
||||||
|
|
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: postgres:17.6
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: miniflux2
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready -d miniflux2 -U postgres"]
|
||||||
|
timeout: 30s
|
||||||
|
interval: 5s
|
||||||
|
|
||||||
|
cockroachdb:
|
||||||
|
image: cockroachdb/cockroach:v23.2.28
|
||||||
|
command: start-single-node --insecure
|
||||||
|
volumes:
|
||||||
|
- cockroach-data:/cockroach/cockroach-data
|
||||||
|
environment:
|
||||||
|
COCKROACH_DATABASE: miniflux2
|
||||||
|
COCKROACH_USER: postgres
|
||||||
|
ports:
|
||||||
|
- "26257:26257"
|
||||||
|
- "26258:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "cockroach", "sql", "--insecure", "--host=localhost:26257", "-e", "select 1"]
|
||||||
|
timeout: 30s
|
||||||
|
interval: 5s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
|
cockroach-data:
|
||||||
|
driver: local
|
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1757545623,
|
||||||
|
"narHash": "sha256-mCxPABZ6jRjUQx3bPP4vjA68ETbPLNz9V2pk9tO7pRQ=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "8cd5ce828d5d1d16feff37340171a98fc3bf6526",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
59
flake.nix
Normal file
59
flake.nix
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ nixpkgs, flake-utils, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
PGHOST = "localhost";
|
||||||
|
PGPORT = 5432;
|
||||||
|
PGPASSWORD = "postgres";
|
||||||
|
PGUSER = "postgres";
|
||||||
|
PGDATABASE = "miniflux2";
|
||||||
|
|
||||||
|
COCKROACH_URL = "postgresql://postgres:postgres@localhost:26257/miniflux2";
|
||||||
|
COCKROACH_INSECURE = true;
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
git
|
||||||
|
|
||||||
|
gnumake
|
||||||
|
foreman
|
||||||
|
|
||||||
|
nil
|
||||||
|
nixfmt-rfc-style
|
||||||
|
|
||||||
|
nodePackages.prettier
|
||||||
|
nodePackages.yaml-language-server
|
||||||
|
nodePackages.vscode-langservers-extracted
|
||||||
|
markdownlint-cli
|
||||||
|
nodePackages.markdown-link-check
|
||||||
|
marksman
|
||||||
|
taplo
|
||||||
|
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
go-tools
|
||||||
|
gofumpt
|
||||||
|
golangci-lint
|
||||||
|
|
||||||
|
postgresql
|
||||||
|
cockroachdb
|
||||||
|
sqlite
|
||||||
|
usql
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
9
go.mod
9
go.mod
|
@ -16,6 +16,7 @@ require (
|
||||||
golang.org/x/net v0.44.0
|
golang.org/x/net v0.44.0
|
||||||
golang.org/x/oauth2 v0.31.0
|
golang.org/x/oauth2 v0.31.0
|
||||||
golang.org/x/term v0.35.0
|
golang.org/x/term v0.35.0
|
||||||
|
modernc.org/sqlite v1.39.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -28,21 +29,29 @@ require (
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
modernc.org/libc v1.66.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
45
go.sum
45
go.sum
|
@ -13,6 +13,8 @@ github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmr
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
|
@ -28,6 +30,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
@ -42,10 +46,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
@ -56,6 +64,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
@ -85,6 +95,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
@ -92,6 +104,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
@ -112,12 +126,15 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
@ -153,6 +170,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
@ -161,3 +180,29 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||||
|
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||||
|
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||||
|
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|
|
@ -168,7 +168,13 @@ func Parse() {
|
||||||
printErrorAndExit(fmt.Errorf("unable to generate javascript bundle: %v", err))
|
printErrorAndExit(fmt.Errorf("unable to generate javascript bundle: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kind, err := database.DetectKind(config.Opts.DatabaseURL())
|
||||||
|
if err != nil {
|
||||||
|
printErrorAndExit(fmt.Errorf("unable to parse database kind: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
db, err := database.NewConnectionPool(
|
db, err := database.NewConnectionPool(
|
||||||
|
kind,
|
||||||
config.Opts.DatabaseURL(),
|
config.Opts.DatabaseURL(),
|
||||||
config.Opts.DatabaseMinConns(),
|
config.Opts.DatabaseMinConns(),
|
||||||
config.Opts.DatabaseMaxConns(),
|
config.Opts.DatabaseMaxConns(),
|
||||||
|
@ -179,14 +185,14 @@ func Parse() {
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
store := storage.NewStorage(db)
|
store := storage.NewStorage(kind, db)
|
||||||
|
|
||||||
if err := store.Ping(); err != nil {
|
if err := store.Ping(); err != nil {
|
||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagMigrate {
|
if flagMigrate {
|
||||||
if err := database.Migrate(db); err != nil {
|
if err := database.Migrate(kind, db); err != nil {
|
||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -228,12 +234,12 @@ func Parse() {
|
||||||
|
|
||||||
// Run migrations and start the daemon.
|
// Run migrations and start the daemon.
|
||||||
if config.Opts.RunMigrations() {
|
if config.Opts.RunMigrations() {
|
||||||
if err := database.Migrate(db); err != nil {
|
if err := database.Migrate(kind, db); err != nil {
|
||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.IsSchemaUpToDate(db); err != nil {
|
if err := database.IsSchemaUpToDate(kind, db); err != nil {
|
||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,8 +183,8 @@ func NewConfigOptions() *configOptions {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"DATABASE_URL": {
|
"DATABASE_URL": {
|
||||||
ParsedStringValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable",
|
ParsedStringValue: "postgres://postgres:postgres/postgres?sslmode=disable",
|
||||||
RawValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable",
|
RawValue: "postgres://postgres:postgres/postgres?sslmode=disable",
|
||||||
ValueType: stringType,
|
ValueType: stringType,
|
||||||
Secret: true,
|
Secret: true,
|
||||||
},
|
},
|
||||||
|
@ -779,7 +779,7 @@ func (c *configOptions) IsAuthProxyUserCreationAllowed() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configOptions) IsDefaultDatabaseURL() bool {
|
func (c *configOptions) IsDefaultDatabaseURL() bool {
|
||||||
return c.options["DATABASE_URL"].RawValue == "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
return c.options["DATABASE_URL"].RawValue == "postgres://postgres:postgres/postgres?sslmode=disable"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configOptions) IsOAuth2UserCreationAllowed() bool {
|
func (c *configOptions) IsOAuth2UserCreationAllowed() bool {
|
||||||
|
|
|
@ -348,7 +348,7 @@ func TestDatabaseMinConnsOptionParsing(t *testing.T) {
|
||||||
func TestDatabaseURLOptionParsing(t *testing.T) {
|
func TestDatabaseURLOptionParsing(t *testing.T) {
|
||||||
configParser := NewConfigParser()
|
configParser := NewConfigParser()
|
||||||
|
|
||||||
if configParser.options.DatabaseURL() != "user=postgres password=postgres dbname=miniflux2 sslmode=disable" {
|
if configParser.options.DatabaseURL() != "postgres://postgres:postgres/postgres?sslmode=disable" {
|
||||||
t.Fatal("Expected DATABASE_URL to have default value")
|
t.Fatal("Expected DATABASE_URL to have default value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
396
internal/database/cockroach.go
Normal file
396
internal/database/cockroach.go
Normal file
|
@ -0,0 +1,396 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package database // import "miniflux.app/v2/internal/database"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cockroachSchemaVersion = len(cockroachMigrations)
|
||||||
|
|
||||||
|
// Order is important. Add new migrations at the end of the list.
|
||||||
|
var cockroachMigrations = []Migration{
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
CREATE TABLE schema_version (
|
||||||
|
version STRING NOT NULL,
|
||||||
|
rowid INT8 NOT VISIBLE NOT NULL DEFAULT unique_rowid(),
|
||||||
|
CONSTRAINT schema_version_pkey PRIMARY KEY (rowid ASC)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
CREATE TYPE entry_status AS ENUM ('unread', 'read', 'removed');
|
||||||
|
CREATE TYPE entry_sorting_direction AS ENUM ('asc', 'desc');
|
||||||
|
CREATE TYPE webapp_display_mode AS ENUM ('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
||||||
|
CREATE TYPE entry_sorting_order AS ENUM ('published_at', 'created_at');
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
username STRING NOT NULL,
|
||||||
|
password STRING NULL,
|
||||||
|
is_admin BOOL NULL DEFAULT false,
|
||||||
|
language STRING NULL DEFAULT 'en_US':::STRING,
|
||||||
|
timezone STRING NULL DEFAULT 'UTC':::STRING,
|
||||||
|
theme STRING NULL DEFAULT 'light_serif':::STRING,
|
||||||
|
last_login_at TIMESTAMPTZ NULL,
|
||||||
|
entry_direction entry_sorting_direction NULL DEFAULT 'asc':::entry_sorting_direction,
|
||||||
|
keyboard_shortcuts BOOL NULL DEFAULT true,
|
||||||
|
entries_per_page INT8 NULL DEFAULT 100:::INT8,
|
||||||
|
show_reading_time BOOL NULL DEFAULT true,
|
||||||
|
entry_swipe BOOL NULL DEFAULT true,
|
||||||
|
stylesheet STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
google_id STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
openid_connect_id STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
display_mode webapp_display_mode NULL DEFAULT 'standalone':::webapp_display_mode,
|
||||||
|
entry_order entry_sorting_order NULL DEFAULT 'published_at':::entry_sorting_order,
|
||||||
|
default_reading_speed INT8 NULL DEFAULT 265:::INT8,
|
||||||
|
cjk_reading_speed INT8 NULL DEFAULT 500:::INT8,
|
||||||
|
default_home_page STRING NULL DEFAULT 'unread':::STRING,
|
||||||
|
categories_sorting_order STRING NOT NULL DEFAULT 'unread_count':::STRING,
|
||||||
|
gesture_nav STRING NOT NULL DEFAULT 'tap':::STRING,
|
||||||
|
mark_read_on_view BOOL NULL DEFAULT true,
|
||||||
|
media_playback_rate DECIMAL NULL DEFAULT 1:::DECIMAL,
|
||||||
|
block_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
keep_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
mark_read_on_media_player_completion BOOL NULL DEFAULT false,
|
||||||
|
custom_js STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
external_font_hosts STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
always_open_external_links BOOL NULL DEFAULT false,
|
||||||
|
open_external_links_in_new_tab BOOL NULL DEFAULT true,
|
||||||
|
CONSTRAINT users_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX users_username_key (username ASC),
|
||||||
|
UNIQUE INDEX users_google_id_idx (google_id ASC) WHERE google_id != '':::STRING,
|
||||||
|
UNIQUE INDEX users_openid_connect_id_idx (openid_connect_id ASC) WHERE openid_connect_id != '':::STRING
|
||||||
|
);
|
||||||
|
CREATE TABLE user_sessions (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
token STRING NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
user_agent STRING NULL,
|
||||||
|
ip STRING NULL,
|
||||||
|
CONSTRAINT sessions_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX sessions_token_key (token ASC),
|
||||||
|
UNIQUE INDEX sessions_user_id_token_key (user_id ASC, token ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
title STRING NOT NULL,
|
||||||
|
hide_globally BOOL NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT categories_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX categories_user_id_title_key (user_id ASC, title ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE feeds (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
category_id INT8 NOT NULL,
|
||||||
|
title STRING NOT NULL,
|
||||||
|
feed_url STRING NOT NULL,
|
||||||
|
site_url STRING NOT NULL,
|
||||||
|
checked_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
etag_header STRING NULL DEFAULT '':::STRING,
|
||||||
|
last_modified_header STRING NULL DEFAULT '':::STRING,
|
||||||
|
parsing_error_msg STRING NULL DEFAULT '':::STRING,
|
||||||
|
parsing_error_count INT8 NULL DEFAULT 0:::INT8,
|
||||||
|
scraper_rules STRING NULL DEFAULT '':::STRING,
|
||||||
|
rewrite_rules STRING NULL DEFAULT '':::STRING,
|
||||||
|
crawler BOOL NULL DEFAULT false,
|
||||||
|
username STRING NULL DEFAULT '':::STRING,
|
||||||
|
password STRING NULL DEFAULT '':::STRING,
|
||||||
|
user_agent STRING NULL DEFAULT '':::STRING,
|
||||||
|
disabled BOOL NULL DEFAULT false,
|
||||||
|
next_check_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
ignore_http_cache BOOL NULL DEFAULT false,
|
||||||
|
fetch_via_proxy BOOL NULL DEFAULT false,
|
||||||
|
blocklist_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
keeplist_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
allow_self_signed_certificates BOOL NOT NULL DEFAULT false,
|
||||||
|
cookie STRING NULL DEFAULT '':::STRING,
|
||||||
|
hide_globally BOOL NOT NULL DEFAULT false,
|
||||||
|
url_rewrite_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
no_media_player BOOL NULL DEFAULT false,
|
||||||
|
apprise_service_urls STRING NULL DEFAULT '':::STRING,
|
||||||
|
disable_http2 BOOL NULL DEFAULT false,
|
||||||
|
description STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_enabled BOOL NULL DEFAULT false,
|
||||||
|
ntfy_priority INT8 NULL DEFAULT 3:::INT8,
|
||||||
|
webhook_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
pushover_enabled BOOL NULL DEFAULT false,
|
||||||
|
pushover_priority INT8 NULL DEFAULT 0:::INT8,
|
||||||
|
ntfy_topic STRING NULL DEFAULT '':::STRING,
|
||||||
|
proxy_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
block_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
keep_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
CONSTRAINT feeds_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX feeds_user_id_feed_url_key (user_id ASC, feed_url ASC),
|
||||||
|
INDEX feeds_user_category_idx (user_id ASC, category_id ASC),
|
||||||
|
INDEX feeds_feed_id_hide_globally_idx (id ASC, hide_globally ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE entries (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
feed_id INT8 NOT NULL,
|
||||||
|
hash STRING NOT NULL,
|
||||||
|
published_at TIMESTAMPTZ NOT NULL,
|
||||||
|
title STRING NOT NULL,
|
||||||
|
url STRING NOT NULL,
|
||||||
|
author STRING NULL,
|
||||||
|
content STRING NULL,
|
||||||
|
status entry_status NULL DEFAULT 'unread':::entry_status,
|
||||||
|
starred BOOL NULL DEFAULT false,
|
||||||
|
comments_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
document_vectors TSVECTOR NULL,
|
||||||
|
changed_at TIMESTAMPTZ NOT NULL,
|
||||||
|
share_code STRING NOT NULL DEFAULT '':::STRING,
|
||||||
|
reading_time INT8 NOT NULL DEFAULT 0:::INT8,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
tags STRING[] NULL DEFAULT ARRAY[]:::STRING[],
|
||||||
|
CONSTRAINT entries_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX entries_feed_id_hash_key (feed_id ASC, hash ASC),
|
||||||
|
INDEX entries_feed_idx (feed_id ASC),
|
||||||
|
INDEX entries_user_status_idx (user_id ASC, status ASC),
|
||||||
|
INVERTED INDEX document_vectors_idx (document_vectors),
|
||||||
|
UNIQUE INDEX entries_share_code_idx (share_code ASC) WHERE share_code != '':::STRING,
|
||||||
|
INDEX entries_user_feed_idx (user_id ASC, feed_id ASC),
|
||||||
|
INDEX entries_id_user_status_idx (id ASC, user_id ASC, status ASC),
|
||||||
|
INDEX entries_feed_id_status_hash_idx (feed_id ASC, status ASC, hash ASC),
|
||||||
|
INDEX entries_user_id_status_starred_idx (user_id ASC, status ASC, starred ASC),
|
||||||
|
INDEX entries_user_status_feed_idx (user_id ASC, status ASC, feed_id ASC),
|
||||||
|
INDEX entries_user_status_changed_idx (user_id ASC, status ASC, changed_at ASC),
|
||||||
|
INDEX entries_user_status_published_idx (user_id ASC, status ASC, published_at ASC),
|
||||||
|
INDEX entries_user_status_created_idx (user_id ASC, status ASC, created_at ASC),
|
||||||
|
INDEX entries_user_status_changed_published_idx (user_id ASC, status ASC, changed_at ASC, published_at ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE enclosures (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
entry_id INT8 NOT NULL,
|
||||||
|
url STRING NOT NULL,
|
||||||
|
size INT8 NULL DEFAULT 0:::INT8,
|
||||||
|
mime_type STRING NULL DEFAULT '':::STRING,
|
||||||
|
media_progression INT8 NULL DEFAULT 0:::INT8,
|
||||||
|
CONSTRAINT enclosures_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX enclosures_user_entry_url_unique_idx (user_id ASC, entry_id ASC, url ASC),
|
||||||
|
INDEX enclosures_entry_id_idx (entry_id ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE icons (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
hash STRING NOT NULL,
|
||||||
|
mime_type STRING NOT NULL,
|
||||||
|
content BYTES NOT NULL,
|
||||||
|
external_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
CONSTRAINT icons_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX icons_hash_key (hash ASC),
|
||||||
|
UNIQUE INDEX icons_external_id_idx (external_id ASC) WHERE external_id != '':::STRING
|
||||||
|
);
|
||||||
|
CREATE TABLE feed_icons (
|
||||||
|
feed_id INT8 NOT NULL,
|
||||||
|
icon_id INT8 NOT NULL,
|
||||||
|
CONSTRAINT feed_icons_pkey PRIMARY KEY (feed_id ASC, icon_id ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE integrations (
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
pinboard_enabled BOOL NULL DEFAULT false,
|
||||||
|
pinboard_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
pinboard_tags STRING NULL DEFAULT 'miniflux':::STRING,
|
||||||
|
pinboard_mark_as_unread BOOL NULL DEFAULT false,
|
||||||
|
instapaper_enabled BOOL NULL DEFAULT false,
|
||||||
|
instapaper_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
instapaper_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
fever_enabled BOOL NULL DEFAULT false,
|
||||||
|
fever_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
fever_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_enabled BOOL NULL DEFAULT false,
|
||||||
|
wallabag_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_client_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_client_secret STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
nunux_keeper_enabled BOOL NULL DEFAULT false,
|
||||||
|
nunux_keeper_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
nunux_keeper_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
telegram_bot_enabled BOOL NULL DEFAULT false,
|
||||||
|
telegram_bot_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
telegram_bot_chat_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
googlereader_enabled BOOL NULL DEFAULT false,
|
||||||
|
googlereader_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
googlereader_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
espial_enabled BOOL NULL DEFAULT false,
|
||||||
|
espial_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
espial_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
espial_tags STRING NULL DEFAULT 'miniflux':::STRING,
|
||||||
|
linkding_enabled BOOL NULL DEFAULT false,
|
||||||
|
linkding_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkding_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
wallabag_only_url BOOL NULL DEFAULT false,
|
||||||
|
matrix_bot_enabled BOOL NULL DEFAULT false,
|
||||||
|
matrix_bot_user STRING NULL DEFAULT '':::STRING,
|
||||||
|
matrix_bot_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
matrix_bot_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
matrix_bot_chat_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkding_tags STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkding_mark_as_unread BOOL NULL DEFAULT false,
|
||||||
|
notion_enabled BOOL NULL DEFAULT false,
|
||||||
|
notion_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
notion_page_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
readwise_enabled BOOL NULL DEFAULT false,
|
||||||
|
readwise_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
apprise_enabled BOOL NULL DEFAULT false,
|
||||||
|
apprise_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
apprise_services_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
shiori_enabled BOOL NULL DEFAULT false,
|
||||||
|
shiori_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
shiori_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
shiori_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
shaarli_enabled BOOL NULL DEFAULT false,
|
||||||
|
shaarli_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
shaarli_api_secret STRING NULL DEFAULT '':::STRING,
|
||||||
|
webhook_enabled BOOL NULL DEFAULT false,
|
||||||
|
webhook_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
webhook_secret STRING NULL DEFAULT '':::STRING,
|
||||||
|
telegram_bot_topic_id INT8 NULL,
|
||||||
|
telegram_bot_disable_web_page_preview BOOL NULL DEFAULT false,
|
||||||
|
telegram_bot_disable_notification BOOL NULL DEFAULT false,
|
||||||
|
telegram_bot_disable_buttons BOOL NULL DEFAULT false,
|
||||||
|
rssbridge_enabled BOOL NULL DEFAULT false,
|
||||||
|
rssbridge_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
omnivore_enabled BOOL NULL DEFAULT false,
|
||||||
|
omnivore_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
omnivore_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkace_enabled BOOL NULL DEFAULT false,
|
||||||
|
linkace_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkace_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkace_tags STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkace_is_private BOOL NULL DEFAULT true,
|
||||||
|
linkace_check_disabled BOOL NULL DEFAULT true,
|
||||||
|
linkwarden_enabled BOOL NULL DEFAULT false,
|
||||||
|
linkwarden_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
linkwarden_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
readeck_enabled BOOL NULL DEFAULT false,
|
||||||
|
readeck_only_url BOOL NULL DEFAULT false,
|
||||||
|
readeck_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
readeck_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
readeck_labels STRING NULL DEFAULT '':::STRING,
|
||||||
|
raindrop_enabled BOOL NULL DEFAULT false,
|
||||||
|
raindrop_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
raindrop_collection_id STRING NULL DEFAULT '':::STRING,
|
||||||
|
raindrop_tags STRING NULL DEFAULT '':::STRING,
|
||||||
|
betula_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
betula_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
betula_enabled BOOL NULL DEFAULT false,
|
||||||
|
ntfy_enabled BOOL NULL DEFAULT false,
|
||||||
|
ntfy_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_topic STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_api_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_username STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_password STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_icon_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
cubox_enabled BOOL NULL DEFAULT false,
|
||||||
|
cubox_api_link STRING NULL DEFAULT '':::STRING,
|
||||||
|
discord_enabled BOOL NULL DEFAULT false,
|
||||||
|
discord_webhook_link STRING NULL DEFAULT '':::STRING,
|
||||||
|
ntfy_internal_links BOOL NULL DEFAULT false,
|
||||||
|
slack_enabled BOOL NULL DEFAULT false,
|
||||||
|
slack_webhook_link STRING NULL DEFAULT '':::STRING,
|
||||||
|
pushover_enabled BOOL NULL DEFAULT false,
|
||||||
|
pushover_user STRING NULL DEFAULT '':::STRING,
|
||||||
|
pushover_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
pushover_device STRING NULL DEFAULT '':::STRING,
|
||||||
|
pushover_prefix STRING NULL DEFAULT '':::STRING,
|
||||||
|
rssbridge_token STRING NULL DEFAULT '':::STRING,
|
||||||
|
karakeep_enabled BOOL NULL DEFAULT false,
|
||||||
|
karakeep_api_key STRING NULL DEFAULT '':::STRING,
|
||||||
|
karakeep_url STRING NULL DEFAULT '':::STRING,
|
||||||
|
CONSTRAINT integrations_pkey PRIMARY KEY (user_id ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id STRING NOT NULL,
|
||||||
|
data JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
CONSTRAINT sessions_pkey PRIMARY KEY (id ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
token STRING NOT NULL,
|
||||||
|
description STRING NOT NULL,
|
||||||
|
last_used_at TIMESTAMPTZ NULL,
|
||||||
|
created_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
CONSTRAINT api_keys_pkey PRIMARY KEY (id ASC),
|
||||||
|
UNIQUE INDEX api_keys_token_key (token ASC),
|
||||||
|
UNIQUE INDEX api_keys_user_id_description_key (user_id ASC, description ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE acme_cache (
|
||||||
|
key VARCHAR(400) NOT NULL,
|
||||||
|
data BYTES NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
|
CONSTRAINT acme_cache_pkey PRIMARY KEY (key ASC)
|
||||||
|
);
|
||||||
|
CREATE TABLE webauthn_credentials (
|
||||||
|
handle BYTES NOT NULL,
|
||||||
|
cred_id BYTES NOT NULL,
|
||||||
|
user_id INT8 NOT NULL,
|
||||||
|
key BYTES NOT NULL,
|
||||||
|
attestation_type VARCHAR(255) NOT NULL,
|
||||||
|
aaguid BYTES NULL,
|
||||||
|
sign_count INT8 NULL,
|
||||||
|
clone_warning BOOL NULL,
|
||||||
|
name STRING NULL,
|
||||||
|
added_on TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
last_seen_on TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||||
|
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (handle ASC),
|
||||||
|
UNIQUE INDEX webauthn_credentials_cred_id_key (cred_id ASC)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
ALTER TABLE user_sessions ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE categories ADD CONSTRAINT categories_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE feeds ADD CONSTRAINT feeds_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE feeds ADD CONSTRAINT feeds_category_id_fkey FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE entries ADD CONSTRAINT entries_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE entries ADD CONSTRAINT entries_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE enclosures ADD CONSTRAINT enclosures_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE enclosures ADD CONSTRAINT enclosures_entry_id_fkey FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE feed_icons ADD CONSTRAINT feed_icons_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE feed_icons ADD CONSTRAINT feed_icons_icon_id_fkey FOREIGN KEY (icon_id) REFERENCES icons(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE api_keys ADD CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE webauthn_credentials ADD CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; `
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
ALTER TABLE user_sessions VALIDATE CONSTRAINT sessions_user_id_fkey;
|
||||||
|
ALTER TABLE categories VALIDATE CONSTRAINT categories_user_id_fkey;
|
||||||
|
ALTER TABLE feeds VALIDATE CONSTRAINT feeds_user_id_fkey;
|
||||||
|
ALTER TABLE feeds VALIDATE CONSTRAINT feeds_category_id_fkey;
|
||||||
|
ALTER TABLE entries VALIDATE CONSTRAINT entries_user_id_fkey;
|
||||||
|
ALTER TABLE entries VALIDATE CONSTRAINT entries_feed_id_fkey;
|
||||||
|
ALTER TABLE enclosures VALIDATE CONSTRAINT enclosures_user_id_fkey;
|
||||||
|
ALTER TABLE enclosures VALIDATE CONSTRAINT enclosures_entry_id_fkey;
|
||||||
|
ALTER TABLE feed_icons VALIDATE CONSTRAINT feed_icons_feed_id_fkey;
|
||||||
|
ALTER TABLE feed_icons VALIDATE CONSTRAINT feed_icons_icon_id_fkey;
|
||||||
|
ALTER TABLE api_keys VALIDATE CONSTRAINT api_keys_user_id_fkey;
|
||||||
|
ALTER TABLE webauthn_credentials VALIDATE CONSTRAINT webauthn_credentials_user_id_fkey;
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
|
@ -7,13 +7,68 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DBKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DBKindPostgres DBKind = iota
|
||||||
|
DBKindCockroach
|
||||||
|
DBKindSqlite
|
||||||
|
)
|
||||||
|
|
||||||
|
var dbKindProto = map[DBKind]string{
|
||||||
|
DBKindPostgres: "postgresql",
|
||||||
|
DBKindCockroach: "cockroachdb",
|
||||||
|
DBKindSqlite: "sqlite",
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbKindDriver = map[DBKind]string{
|
||||||
|
DBKindPostgres: "postgres",
|
||||||
|
DBKindCockroach: "postgres",
|
||||||
|
DBKindSqlite: "sqlite",
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectKind(conn string) (DBKind, error) {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(conn, "postgres"),
|
||||||
|
strings.HasPrefix(conn, "postgresql"):
|
||||||
|
return DBKindPostgres, nil
|
||||||
|
case strings.HasPrefix(conn, "cockroach"),
|
||||||
|
strings.HasPrefix(conn, "cockroachdb"):
|
||||||
|
return DBKindCockroach, nil
|
||||||
|
case strings.HasPrefix(conn, "file"),
|
||||||
|
strings.HasPrefix(conn, "sqlite"):
|
||||||
|
return DBKindSqlite, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown db kind in conn string: %q", conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Migration func(*sql.Tx) error
|
||||||
|
|
||||||
|
var dbKindMigrations = map[DBKind][]Migration{
|
||||||
|
DBKindPostgres: postgresMigrations,
|
||||||
|
DBKindCockroach: cockroachMigrations,
|
||||||
|
DBKindSqlite: sqliteMigrations,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbKindSchemaVersion = map[DBKind]int{
|
||||||
|
DBKindPostgres: postgresSchemaVersion,
|
||||||
|
DBKindCockroach: cockroachSchemaVersion,
|
||||||
|
DBKindSqlite: sqliteSchemaVersion,
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate executes database migrations.
|
// Migrate executes database migrations.
|
||||||
func Migrate(db *sql.DB) error {
|
func Migrate(kind DBKind, db *sql.DB) error {
|
||||||
var currentVersion int
|
var currentVersion int
|
||||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||||
|
|
||||||
|
migrations := dbKindMigrations[kind]
|
||||||
|
schemaVersion := dbKindSchemaVersion[kind]
|
||||||
|
|
||||||
slog.Info("Running database migrations",
|
slog.Info("Running database migrations",
|
||||||
slog.Int("current_version", currentVersion),
|
slog.Int("current_version", currentVersion),
|
||||||
slog.Int("latest_version", schemaVersion),
|
slog.Int("latest_version", schemaVersion),
|
||||||
|
@ -32,14 +87,24 @@ func Migrate(db *sql.DB) error {
|
||||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(`TRUNCATE schema_version`); err != nil {
|
if kind == DBKindSqlite {
|
||||||
tx.Rollback()
|
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
|
||||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
tx.Rollback()
|
||||||
}
|
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||||
|
}
|
||||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES (?)`, newVersion); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := tx.Exec(`TRUNCATE schema_version`); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
|
@ -51,7 +116,9 @@ func Migrate(db *sql.DB) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSchemaUpToDate checks if the database schema is up to date.
|
// IsSchemaUpToDate checks if the database schema is up to date.
|
||||||
func IsSchemaUpToDate(db *sql.DB) error {
|
func IsSchemaUpToDate(kind DBKind, db *sql.DB) error {
|
||||||
|
schemaVersion := dbKindSchemaVersion[kind]
|
||||||
|
|
||||||
var currentVersion int
|
var currentVersion int
|
||||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||||
if currentVersion < schemaVersion {
|
if currentVersion < schemaVersion {
|
||||||
|
@ -59,3 +126,25 @@ func IsSchemaUpToDate(db *sql.DB) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewConnectionPool(kind DBKind, dsn string, minConnections, maxConnections int, connectionLifetime time.Duration) (*sql.DB, error) {
|
||||||
|
driver := dbKindDriver[kind]
|
||||||
|
|
||||||
|
// replace cockroachdb protocol with postgres
|
||||||
|
// we use cockroachdb protocol to detect cockroachdb but go wants postgres
|
||||||
|
if kind == DBKindCockroach {
|
||||||
|
split := strings.SplitN(dsn, ":", 2)
|
||||||
|
dsn = fmt.Sprintf("postgres:%s", split[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open(driver, dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(maxConnections)
|
||||||
|
db.SetMaxIdleConns(minConnections)
|
||||||
|
db.SetConnMaxLifetime(connectionLifetime)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
378
internal/database/sqlite.go
Normal file
378
internal/database/sqlite.go
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package database // import "miniflux.app/v2/internal/database"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sqliteSchemaVersion = len(sqliteMigrations)
|
||||||
|
|
||||||
|
// Order is important. Add new migrations at the end of the list.
|
||||||
|
var sqliteMigrations = []Migration{
|
||||||
|
func(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(`
|
||||||
|
CREATE TABLE schema_version (
|
||||||
|
version INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT,
|
||||||
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||||
|
language TEXT NOT NULL DEFAULT 'en_US',
|
||||||
|
timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||||
|
theme TEXT NOT NULL DEFAULT 'light_serif',
|
||||||
|
last_login_at DATETIME,
|
||||||
|
entry_direction TEXT NOT NULL DEFAULT 'asc' CHECK (entry_direction IN ('asc','desc')),
|
||||||
|
keyboard_shortcuts INTEGER NOT NULL DEFAULT 1,
|
||||||
|
entries_per_page INTEGER NOT NULL DEFAULT 100,
|
||||||
|
show_reading_time INTEGER NOT NULL DEFAULT 1,
|
||||||
|
entry_swipe INTEGER NOT NULL DEFAULT 1,
|
||||||
|
stylesheet TEXT NOT NULL DEFAULT '',
|
||||||
|
google_id TEXT NOT NULL DEFAULT '',
|
||||||
|
openid_connect_id TEXT NOT NULL DEFAULT '',
|
||||||
|
display_mode TEXT NOT NULL DEFAULT 'standalone' CHECK (display_mode IN ('fullscreen','standalone','minimal-ui','browser')),
|
||||||
|
entry_order TEXT NOT NULL DEFAULT 'published_at' CHECK (entry_order IN ('published_at','created_at')),
|
||||||
|
default_reading_speed INTEGER NOT NULL DEFAULT 265,
|
||||||
|
cjk_reading_speed INTEGER NOT NULL DEFAULT 500,
|
||||||
|
default_home_page TEXT NOT NULL DEFAULT 'unread',
|
||||||
|
categories_sorting_order TEXT NOT NULL DEFAULT 'unread_count',
|
||||||
|
gesture_nav TEXT NOT NULL DEFAULT 'tap',
|
||||||
|
mark_read_on_view INTEGER NOT NULL DEFAULT 1,
|
||||||
|
media_playback_rate REAL NOT NULL DEFAULT 1.0,
|
||||||
|
block_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
keep_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
mark_read_on_media_player_completion INTEGER NOT NULL DEFAULT 0,
|
||||||
|
custom_js TEXT NOT NULL DEFAULT '',
|
||||||
|
external_font_hosts TEXT NOT NULL DEFAULT '',
|
||||||
|
always_open_external_links INTEGER NOT NULL DEFAULT 0,
|
||||||
|
open_external_links_in_new_tab INTEGER NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
CREATE TABLE user_sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
user_agent TEXT,
|
||||||
|
ip TEXT,
|
||||||
|
UNIQUE(user_id, token),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
hide_globally INTEGER NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE(user_id, title),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE feeds (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
feed_url TEXT NOT NULL,
|
||||||
|
site_url TEXT NOT NULL,
|
||||||
|
checked_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
etag_header TEXT NOT NULL DEFAULT '',
|
||||||
|
last_modified_header TEXT NOT NULL DEFAULT '',
|
||||||
|
parsing_error_msg TEXT NOT NULL DEFAULT '',
|
||||||
|
parsing_error_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
scraper_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
rewrite_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
crawler INTEGER NOT NULL DEFAULT 0,
|
||||||
|
username TEXT NOT NULL DEFAULT '',
|
||||||
|
password TEXT NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL DEFAULT '',
|
||||||
|
disabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_check_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
ignore_http_cache INTEGER NOT NULL DEFAULT 0,
|
||||||
|
fetch_via_proxy INTEGER NOT NULL DEFAULT 0,
|
||||||
|
blocklist_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
keeplist_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
allow_self_signed_certificates INTEGER NOT NULL DEFAULT 0,
|
||||||
|
cookie TEXT NOT NULL DEFAULT '',
|
||||||
|
hide_globally INTEGER NOT NULL DEFAULT 0,
|
||||||
|
url_rewrite_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
no_media_player INTEGER NOT NULL DEFAULT 0,
|
||||||
|
apprise_service_urls TEXT NOT NULL DEFAULT '',
|
||||||
|
disable_http2 INTEGER NOT NULL DEFAULT 0,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ntfy_priority INTEGER NOT NULL DEFAULT 3,
|
||||||
|
webhook_url TEXT NOT NULL DEFAULT '',
|
||||||
|
pushover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
pushover_priority INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ntfy_topic TEXT NOT NULL DEFAULT '',
|
||||||
|
proxy_url TEXT NOT NULL DEFAULT '',
|
||||||
|
block_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
keep_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(user_id, feed_url),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE entries (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
feed_id INTEGER NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
published_at DATETIME NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
author TEXT,
|
||||||
|
content TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'unread' CHECK (status IN ('unread','read','removed')),
|
||||||
|
starred INTEGER NOT NULL DEFAULT 0,
|
||||||
|
comments_url TEXT NOT NULL DEFAULT '',
|
||||||
|
changed_at DATETIME NOT NULL,
|
||||||
|
share_code TEXT NOT NULL DEFAULT '',
|
||||||
|
reading_time INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
tags TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(feed_id, hash),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE enclosures (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
entry_id INTEGER NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
|
mime_type TEXT NOT NULL DEFAULT '',
|
||||||
|
media_progression INTEGER NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE(user_id, entry_id, url),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE icons (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
external_id TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(hash)
|
||||||
|
);
|
||||||
|
CREATE TABLE feed_icons (
|
||||||
|
feed_id INTEGER NOT NULL,
|
||||||
|
icon_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (feed_id, icon_id),
|
||||||
|
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (icon_id) REFERENCES icons(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE integrations (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
pinboard_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
pinboard_token TEXT NOT NULL DEFAULT '',
|
||||||
|
pinboard_tags TEXT NOT NULL DEFAULT 'miniflux',
|
||||||
|
pinboard_mark_as_unread INTEGER NOT NULL DEFAULT 0,
|
||||||
|
instapaper_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
instapaper_username TEXT NOT NULL DEFAULT '',
|
||||||
|
instapaper_password TEXT NOT NULL DEFAULT '',
|
||||||
|
fever_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
fever_username TEXT NOT NULL DEFAULT '',
|
||||||
|
fever_token TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
wallabag_url TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_client_id TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_client_secret TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_username TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_password TEXT NOT NULL DEFAULT '',
|
||||||
|
nunux_keeper_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
nunux_keeper_url TEXT NOT NULL DEFAULT '',
|
||||||
|
nunux_keeper_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
telegram_bot_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
telegram_bot_token TEXT NOT NULL DEFAULT '',
|
||||||
|
telegram_bot_chat_id TEXT NOT NULL DEFAULT '',
|
||||||
|
googlereader_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
googlereader_username TEXT NOT NULL DEFAULT '',
|
||||||
|
googlereader_password TEXT NOT NULL DEFAULT '',
|
||||||
|
espial_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
espial_url TEXT NOT NULL DEFAULT '',
|
||||||
|
espial_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
espial_tags TEXT NOT NULL DEFAULT 'miniflux',
|
||||||
|
linkding_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
linkding_url TEXT NOT NULL DEFAULT '',
|
||||||
|
linkding_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
wallabag_only_url INTEGER NOT NULL DEFAULT 0,
|
||||||
|
matrix_bot_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
matrix_bot_user TEXT NOT NULL DEFAULT '',
|
||||||
|
matrix_bot_password TEXT NOT NULL DEFAULT '',
|
||||||
|
matrix_bot_url TEXT NOT NULL DEFAULT '',
|
||||||
|
matrix_bot_chat_id TEXT NOT NULL DEFAULT '',
|
||||||
|
linkding_tags TEXT NOT NULL DEFAULT '',
|
||||||
|
linkding_mark_as_unread INTEGER NOT NULL DEFAULT 0,
|
||||||
|
notion_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
notion_token TEXT NOT NULL DEFAULT '',
|
||||||
|
notion_page_id TEXT NOT NULL DEFAULT '',
|
||||||
|
readwise_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
readwise_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
apprise_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
apprise_url TEXT NOT NULL DEFAULT '',
|
||||||
|
apprise_services_url TEXT NOT NULL DEFAULT '',
|
||||||
|
shiori_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shiori_url TEXT NOT NULL DEFAULT '',
|
||||||
|
shiori_username TEXT NOT NULL DEFAULT '',
|
||||||
|
shiori_password TEXT NOT NULL DEFAULT '',
|
||||||
|
shaarli_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shaarli_url TEXT NOT NULL DEFAULT '',
|
||||||
|
shaarli_api_secret TEXT NOT NULL DEFAULT '',
|
||||||
|
webhook_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
webhook_url TEXT NOT NULL DEFAULT '',
|
||||||
|
webhook_secret TEXT NOT NULL DEFAULT '',
|
||||||
|
telegram_bot_topic_id INTEGER,
|
||||||
|
telegram_bot_disable_web_page_preview INTEGER NOT NULL DEFAULT 0,
|
||||||
|
telegram_bot_disable_notification INTEGER NOT NULL DEFAULT 0,
|
||||||
|
telegram_bot_disable_buttons INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rssbridge_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rssbridge_url TEXT NOT NULL DEFAULT '',
|
||||||
|
omnivore_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
omnivore_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
omnivore_url TEXT NOT NULL DEFAULT '',
|
||||||
|
linkace_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
linkace_url TEXT NOT NULL DEFAULT '',
|
||||||
|
linkace_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
linkace_tags TEXT NOT NULL DEFAULT '',
|
||||||
|
linkace_is_private INTEGER NOT NULL DEFAULT 1,
|
||||||
|
linkace_check_disabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
linkwarden_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
linkwarden_url TEXT NOT NULL DEFAULT '',
|
||||||
|
linkwarden_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
readeck_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
readeck_only_url INTEGER NOT NULL DEFAULT 0,
|
||||||
|
readeck_url TEXT NOT NULL DEFAULT '',
|
||||||
|
readeck_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
readeck_labels TEXT NOT NULL DEFAULT '',
|
||||||
|
raindrop_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
raindrop_token TEXT NOT NULL DEFAULT '',
|
||||||
|
raindrop_collection_id TEXT NOT NULL DEFAULT '',
|
||||||
|
raindrop_tags TEXT NOT NULL DEFAULT '',
|
||||||
|
betula_url TEXT NOT NULL DEFAULT '',
|
||||||
|
betula_token TEXT NOT NULL DEFAULT '',
|
||||||
|
betula_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ntfy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ntfy_url TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_topic TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_api_token TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_username TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_password TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_icon_url TEXT NOT NULL DEFAULT '',
|
||||||
|
cubox_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
cubox_api_link TEXT NOT NULL DEFAULT '',
|
||||||
|
discord_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
discord_webhook_link TEXT NOT NULL DEFAULT '',
|
||||||
|
ntfy_internal_links INTEGER NOT NULL DEFAULT 0,
|
||||||
|
slack_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
slack_webhook_link TEXT NOT NULL DEFAULT '',
|
||||||
|
pushover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
pushover_user TEXT NOT NULL DEFAULT '',
|
||||||
|
pushover_token TEXT NOT NULL DEFAULT '',
|
||||||
|
pushover_device TEXT NOT NULL DEFAULT '',
|
||||||
|
pushover_prefix TEXT NOT NULL DEFAULT '',
|
||||||
|
rssbridge_token TEXT NOT NULL DEFAULT '',
|
||||||
|
karakeep_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
karakeep_api_key TEXT NOT NULL DEFAULT '',
|
||||||
|
karakeep_url TEXT NOT NULL DEFAULT '',
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (DATETIME('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
last_used_at DATETIME,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
UNIQUE(user_id, description),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE acme_cache (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE webauthn_credentials (
|
||||||
|
handle BLOB PRIMARY KEY,
|
||||||
|
cred_id BLOB NOT NULL UNIQUE,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
key BLOB NOT NULL,
|
||||||
|
attestation_type TEXT NOT NULL,
|
||||||
|
aaguid BLOB,
|
||||||
|
sign_count INTEGER,
|
||||||
|
clone_warning INTEGER,
|
||||||
|
name TEXT,
|
||||||
|
added_on DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
last_seen_on DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(`
|
||||||
|
CREATE UNIQUE INDEX icons_external_id_uq ON icons(external_id) WHERE external_id != '';
|
||||||
|
CREATE UNIQUE INDEX users_google_id_uq ON users(google_id) WHERE google_id != '';
|
||||||
|
CREATE UNIQUE INDEX users_openid_connect_id_uq ON users(openid_connect_id) WHERE openid_connect_id != '';
|
||||||
|
CREATE UNIQUE INDEX entries_share_code_uq ON entries(share_code) WHERE share_code != '';
|
||||||
|
|
||||||
|
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
|
||||||
|
CREATE INDEX feeds_id_hide_globally_idx ON feeds(id, hide_globally);
|
||||||
|
CREATE INDEX entries_feed_idx ON entries(feed_id);
|
||||||
|
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
|
||||||
|
CREATE INDEX entries_user_feed_idx ON entries(user_id, feed_id);
|
||||||
|
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||||
|
CREATE INDEX entries_user_status_published_idx ON entries(user_id, status, published_at);
|
||||||
|
CREATE INDEX entries_user_status_created_idx ON entries(user_id, status, created_at);
|
||||||
|
CREATE INDEX entries_id_user_status_idx ON entries(id, user_id, status);
|
||||||
|
CREATE INDEX entries_feed_id_status_hash_idx ON entries(feed_id, status, hash);
|
||||||
|
CREATE INDEX entries_user_id_status_starred_idx ON entries(user_id, status, starred);
|
||||||
|
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||||
|
CREATE INDEX entries_user_status_changed_published_idx ON entries(user_id, status, changed_at, published_at);
|
||||||
|
CREATE INDEX enclosures_entry_id_idx ON enclosures(entry_id);
|
||||||
|
CREATE INDEX feed_icons_icon_id_idx ON feed_icons(icon_id);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(`
|
||||||
|
CREATE VIRTUAL TABLE entries_fts USING fts5(
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
entry_id UNINDEXED,
|
||||||
|
content='',
|
||||||
|
-- as close to PostgreSQL as possible while being multilingual
|
||||||
|
tokenize = "unicode61 remove_diacritics 2 tokenchars '-_'"
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER entries_ai AFTER INSERT ON entries BEGIN
|
||||||
|
INSERT INTO entries_fts(rowid,title,content,tags,entry_id)
|
||||||
|
VALUES (new.id,new.title,COALESCE(new.content,''),COALESCE(new.tags,''),new.id);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER entries_au AFTER UPDATE ON entries BEGIN
|
||||||
|
INSERT INTO entries_fts(entries_fts,rowid) VALUES('delete', old.id);
|
||||||
|
INSERT INTO entries_fts(rowid,title,content,tags,entry_id)
|
||||||
|
VALUES (new.id,new.title,COALESCE(new.content,''),COALESCE(new.tags,''),new.id);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER entries_ad AFTER DELETE ON entries BEGIN
|
||||||
|
INSERT INTO entries_fts(entries_fts,rowid) VALUES('delete', old.id);
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/database"
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
@ -49,7 +50,6 @@ func (s *Storage) GetEnclosures(entryID int64) (model.EnclosureList, error) {
|
||||||
&enclosure.MimeType,
|
&enclosure.MimeType,
|
||||||
&enclosure.MediaProgression,
|
&enclosure.MediaProgression,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`store: unable to fetch enclosure row: %v`, err)
|
return nil, fmt.Errorf(`store: unable to fetch enclosure row: %v`, err)
|
||||||
}
|
}
|
||||||
|
@ -150,15 +150,20 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
urlPart := "md5(url)"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
urlPart = "url"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
INSERT INTO enclosures
|
INSERT INTO enclosures
|
||||||
(url, size, mime_type, entry_id, user_id, media_progression)
|
(url, size, mime_type, entry_id, user_id, media_progression)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6)
|
($1, $2, $3, $4, $5, $6)
|
||||||
ON CONFLICT (user_id, entry_id, md5(url)) DO NOTHING
|
ON CONFLICT (user_id, entry_id, %s) DO NOTHING
|
||||||
RETURNING
|
RETURNING
|
||||||
id
|
id
|
||||||
`
|
`, urlPart)
|
||||||
if err := tx.QueryRow(
|
if err := tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
enclosureURL,
|
enclosureURL,
|
||||||
|
@ -226,7 +231,6 @@ func (s *Storage) UpdateEnclosure(enclosure *model.Enclosure) error {
|
||||||
enclosure.MediaProgression,
|
enclosure.MediaProgression,
|
||||||
enclosure.ID,
|
enclosure.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`store: unable to update enclosure #%d : %v`, enclosure.ID, err)
|
return fmt.Errorf(`store: unable to update enclosure #%d : %v`, enclosure.ID, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@ package storage // import "miniflux.app/v2/internal/storage"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/crypto"
|
"miniflux.app/v2/internal/crypto"
|
||||||
|
"miniflux.app/v2/internal/database"
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
@ -70,17 +72,24 @@ func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
||||||
// UpdateEntryTitleAndContent updates entry title and content.
|
// UpdateEntryTitleAndContent updates entry title and content.
|
||||||
func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
||||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||||
query := `
|
vectorPart := ""
|
||||||
|
if s.kind != database.DBKindSqlite {
|
||||||
|
vectorPart = ", document_vectors = setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
vectorPart = ", document_vectors = to_tsvector(substring($4 || ' ' || $4 || ' ' || coalesce($5, '') for 1000000))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
UPDATE
|
UPDATE
|
||||||
entries
|
entries
|
||||||
SET
|
SET
|
||||||
title=$1,
|
title=$1,
|
||||||
content=$2,
|
content=$2,
|
||||||
reading_time=$3,
|
reading_time=$3
|
||||||
document_vectors = setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')
|
%s
|
||||||
WHERE
|
WHERE
|
||||||
id=$6 AND user_id=$7
|
id=$6 AND user_id=$7
|
||||||
`
|
`, vectorPart)
|
||||||
|
|
||||||
if _, err := s.db.Exec(
|
if _, err := s.db.Exec(
|
||||||
query,
|
query,
|
||||||
|
@ -100,7 +109,14 @@ func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
||||||
// createEntry add a new entry.
|
// createEntry add a new entry.
|
||||||
func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||||
query := `
|
vectorParts := [2]string{"", ""}
|
||||||
|
if s.kind != database.DBKindSqlite {
|
||||||
|
vectorParts = [2]string{",document_vectors", ",setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B')"}
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
vectorParts[1] = ",to_tsvector(substring($11 || ' ' || $11 || ' ' || coalesce($12, '') for 1000000))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
INSERT INTO entries
|
INSERT INTO entries
|
||||||
(
|
(
|
||||||
title,
|
title,
|
||||||
|
@ -114,8 +130,8 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
feed_id,
|
feed_id,
|
||||||
reading_time,
|
reading_time,
|
||||||
changed_at,
|
changed_at,
|
||||||
document_vectors,
|
|
||||||
tags
|
tags
|
||||||
|
%s
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
|
@ -130,12 +146,24 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
$9,
|
$9,
|
||||||
$10,
|
$10,
|
||||||
now(),
|
now(),
|
||||||
setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B'),
|
|
||||||
$13
|
$13
|
||||||
|
%s
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
id, status, created_at, changed_at
|
id, status, created_at, changed_at
|
||||||
`
|
`, vectorParts[0], vectorParts[1])
|
||||||
|
|
||||||
|
var tagsParam any
|
||||||
|
if s.kind == database.DBKindSqlite {
|
||||||
|
sqliteTagsParam, err := encodeTagsJSON(entry.Tags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode tags: %w", err)
|
||||||
|
}
|
||||||
|
tagsParam = sqliteTagsParam
|
||||||
|
} else {
|
||||||
|
tagsParam = pq.Array(entry.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
|
@ -150,7 +178,7 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
entry.ReadingTime,
|
entry.ReadingTime,
|
||||||
truncatedTitle,
|
truncatedTitle,
|
||||||
truncatedContent,
|
truncatedContent,
|
||||||
pq.Array(entry.Tags),
|
tagsParam,
|
||||||
).Scan(
|
).Scan(
|
||||||
&entry.ID,
|
&entry.ID,
|
||||||
&entry.Status,
|
&entry.Status,
|
||||||
|
@ -178,7 +206,14 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
// it default to time.Now() which could change the order of items on the history page.
|
// it default to time.Now() which could change the order of items on the history page.
|
||||||
func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||||
query := `
|
vectorPart := ""
|
||||||
|
if s.kind != database.DBKindSqlite {
|
||||||
|
vectorPart = ",document_vectors = setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B')"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
vectorPart = ",document_vectors = to_tsvector(substring($7 || ' ' || $7 || ' ' || coalesce($8, '') for 1000000))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
UPDATE
|
UPDATE
|
||||||
entries
|
entries
|
||||||
SET
|
SET
|
||||||
|
@ -188,13 +223,25 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
content=$4,
|
content=$4,
|
||||||
author=$5,
|
author=$5,
|
||||||
reading_time=$6,
|
reading_time=$6,
|
||||||
document_vectors = setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B'),
|
|
||||||
tags=$12
|
tags=$12
|
||||||
|
%s
|
||||||
WHERE
|
WHERE
|
||||||
user_id=$9 AND feed_id=$10 AND hash=$11
|
user_id=$9 AND feed_id=$10 AND hash=$11
|
||||||
RETURNING
|
RETURNING
|
||||||
id
|
id
|
||||||
`
|
`, vectorPart)
|
||||||
|
|
||||||
|
var tagsParam any
|
||||||
|
if s.kind == database.DBKindSqlite {
|
||||||
|
sqliteTagsParam, err := encodeTagsJSON(entry.Tags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode tags: %w", err)
|
||||||
|
}
|
||||||
|
tagsParam = sqliteTagsParam
|
||||||
|
} else {
|
||||||
|
tagsParam = pq.Array(entry.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
|
@ -208,7 +255,7 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||||
entry.UserID,
|
entry.UserID,
|
||||||
entry.FeedID,
|
entry.FeedID,
|
||||||
entry.Hash,
|
entry.Hash,
|
||||||
pq.Array(entry.Tags),
|
tagsParam,
|
||||||
).Scan(&entry.ID)
|
).Scan(&entry.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`store: unable to update entry %q: %v`, entry.URL, err)
|
return fmt.Errorf(`store: unable to update entry %q: %v`, entry.URL, err)
|
||||||
|
@ -301,7 +348,12 @@ func (s *Storage) DeleteRemovedEntriesEnclosures() (int64, error) {
|
||||||
|
|
||||||
// ClearRemovedEntriesContent clears the content fields of entries marked as "removed", keeping only their metadata.
|
// ClearRemovedEntriesContent clears the content fields of entries marked as "removed", keeping only their metadata.
|
||||||
func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
|
func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
|
||||||
query := `
|
vectorPart := ""
|
||||||
|
if s.kind != database.DBKindSqlite {
|
||||||
|
vectorPart = ",document_vectors=NULL"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
UPDATE
|
UPDATE
|
||||||
entries
|
entries
|
||||||
SET
|
SET
|
||||||
|
@ -309,8 +361,8 @@ func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
|
||||||
content=NULL,
|
content=NULL,
|
||||||
url='',
|
url='',
|
||||||
author=NULL,
|
author=NULL,
|
||||||
comments_url=NULL,
|
comments_url=NULL
|
||||||
document_vectors=NULL
|
%s
|
||||||
WHERE id IN (
|
WHERE id IN (
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM entries
|
FROM entries
|
||||||
|
@ -318,7 +370,7 @@ func (s *Storage) ClearRemovedEntriesContent(limit int) (int64, error) {
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
)
|
)
|
||||||
`
|
`, vectorPart)
|
||||||
|
|
||||||
result, err := s.db.Exec(query, model.EntryStatusRemoved, limit)
|
result, err := s.db.Exec(query, model.EntryStatusRemoved, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -737,3 +789,19 @@ func truncateStringForTSVectorField(s string, maxSize int) string {
|
||||||
// Fallback: return empty string if we can't find a valid UTF-8 boundary
|
// Fallback: return empty string if we can't find a valid UTF-8 boundary
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeTagsJSON(tags []string) (string, error) {
|
||||||
|
b, err := json.Marshal(tags)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeTagsJSON(s string) ([]string, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var tags []string
|
||||||
|
if err := json.Unmarshal([]byte(s), &tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
|
@ -9,24 +9,34 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/database"
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EntryPaginationBuilder is a builder for entry prev/next queries.
|
// EntryPaginationBuilder is a builder for entry prev/next queries.
|
||||||
type EntryPaginationBuilder struct {
|
type EntryPaginationBuilder struct {
|
||||||
store *Storage
|
store *Storage
|
||||||
conditions []string
|
conditions []string
|
||||||
args []any
|
args []any
|
||||||
entryID int64
|
entryID int64
|
||||||
order string
|
order string
|
||||||
direction string
|
direction string
|
||||||
|
useSqliteFts bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSearchQuery adds full-text search query to the condition.
|
// WithSearchQuery adds full-text search query to the condition.
|
||||||
func (e *EntryPaginationBuilder) WithSearchQuery(query string) {
|
func (e *EntryPaginationBuilder) WithSearchQuery(query string) {
|
||||||
if query != "" {
|
if query != "" {
|
||||||
e.conditions = append(e.conditions, fmt.Sprintf("e.document_vectors @@ plainto_tsquery($%d)", len(e.args)+1))
|
nArgs := len(e.args) + 1
|
||||||
e.args = append(e.args, query)
|
if e.store.kind == database.DBKindSqlite {
|
||||||
|
query = toSqliteFtsQuery(query)
|
||||||
|
e.useSqliteFts = true
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("fts MATCH $%d", nArgs))
|
||||||
|
e.args = append(e.args, query)
|
||||||
|
} else {
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("e.document_vectors @@ plainto_tsquery($%d)", nArgs))
|
||||||
|
e.args = append(e.args, query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +71,24 @@ func (e *EntryPaginationBuilder) WithStatus(status string) {
|
||||||
|
|
||||||
func (e *EntryPaginationBuilder) WithTags(tags []string) {
|
func (e *EntryPaginationBuilder) WithTags(tags []string) {
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
for _, tag := range tags {
|
if e.store.kind == database.DBKindSqlite {
|
||||||
e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
|
for _, tag := range tags {
|
||||||
e.args = append(e.args, tag)
|
n := len(e.args) + 1
|
||||||
|
cond := fmt.Sprintf(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM json_each(COALESCE(e.tags, '[]')) AS jt
|
||||||
|
WHERE lower(jt.value) = lower($%d)
|
||||||
|
)
|
||||||
|
`, n)
|
||||||
|
e.conditions = append(e.conditions, cond)
|
||||||
|
e.args = append(e.args, tag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, tag := range tags {
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
|
||||||
|
e.args = append(e.args, tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/database"
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
"miniflux.app/v2/internal/timezone"
|
"miniflux.app/v2/internal/timezone"
|
||||||
)
|
)
|
||||||
|
@ -25,6 +26,7 @@ type EntryQueryBuilder struct {
|
||||||
limit int
|
limit int
|
||||||
offset int
|
offset int
|
||||||
fetchEnclosures bool
|
fetchEnclosures bool
|
||||||
|
useSqliteFts bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithEnclosures fetches enclosures for each entry.
|
// WithEnclosures fetches enclosures for each entry.
|
||||||
|
@ -37,14 +39,23 @@ func (e *EntryQueryBuilder) WithEnclosures() *EntryQueryBuilder {
|
||||||
func (e *EntryQueryBuilder) WithSearchQuery(query string) *EntryQueryBuilder {
|
func (e *EntryQueryBuilder) WithSearchQuery(query string) *EntryQueryBuilder {
|
||||||
if query != "" {
|
if query != "" {
|
||||||
nArgs := len(e.args) + 1
|
nArgs := len(e.args) + 1
|
||||||
e.conditions = append(e.conditions, fmt.Sprintf("e.document_vectors @@ plainto_tsquery($%d)", nArgs))
|
if e.store.kind == database.DBKindSqlite {
|
||||||
e.args = append(e.args, query)
|
query = toSqliteFtsQuery(query)
|
||||||
|
e.useSqliteFts = true
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("fts MATCH $%d", nArgs))
|
||||||
|
e.args = append(e.args, query)
|
||||||
|
|
||||||
// 0.0000001 = 0.1 / (seconds_in_a_day)
|
e.WithSorting("bm25(fts)", "DESC")
|
||||||
e.WithSorting(
|
} else {
|
||||||
fmt.Sprintf("ts_rank(document_vectors, plainto_tsquery($%d)) - extract (epoch from now() - published_at)::float * 0.0000001", nArgs),
|
e.conditions = append(e.conditions, fmt.Sprintf("e.document_vectors @@ plainto_tsquery($%d)", nArgs))
|
||||||
"DESC",
|
e.args = append(e.args, query)
|
||||||
)
|
|
||||||
|
// 0.0000001 = 0.1 / (seconds_in_a_day)
|
||||||
|
e.WithSorting(
|
||||||
|
fmt.Sprintf("ts_rank(document_vectors, plainto_tsquery($%d)) - extract (epoch from now() - published_at)::float * 0.0000001", nArgs),
|
||||||
|
"DESC",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@ -160,9 +171,24 @@ func (e *EntryQueryBuilder) WithStatuses(statuses []string) *EntryQueryBuilder {
|
||||||
// WithTags filter by a list of entry tags.
|
// WithTags filter by a list of entry tags.
|
||||||
func (e *EntryQueryBuilder) WithTags(tags []string) *EntryQueryBuilder {
|
func (e *EntryQueryBuilder) WithTags(tags []string) *EntryQueryBuilder {
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
for _, cat := range tags {
|
if e.store.kind == database.DBKindSqlite {
|
||||||
e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
|
for _, tag := range tags {
|
||||||
e.args = append(e.args, cat)
|
n := len(e.args) + 1
|
||||||
|
cond := fmt.Sprintf(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM json_each(COALESCE(e.tags, '[]')) AS jt
|
||||||
|
WHERE lower(jt.value) = lower($%d)
|
||||||
|
)
|
||||||
|
`, n)
|
||||||
|
e.conditions = append(e.conditions, cond)
|
||||||
|
e.args = append(e.args, tag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, cat := range tags {
|
||||||
|
e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
|
||||||
|
e.args = append(e.args, cat)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
|
@ -307,7 +333,16 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||||
icons i ON i.id=fi.icon_id
|
icons i ON i.id=fi.icon_id
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
users u ON u.id=e.user_id
|
users u ON u.id=e.user_id
|
||||||
WHERE ` + e.buildCondition() + " " + e.buildSorting()
|
`
|
||||||
|
|
||||||
|
if e.useSqliteFts {
|
||||||
|
query += `
|
||||||
|
LEFT JOIN
|
||||||
|
entries_fts fts ON fts.rowid = e.id
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " WHERE " + e.buildCondition() + " " + e.buildSorting()
|
||||||
|
|
||||||
rows, err := e.store.db.Query(query, e.args...)
|
rows, err := e.store.db.Query(query, e.args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -364,7 +399,6 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||||
&externalIconID,
|
&externalIconID,
|
||||||
&tz,
|
&tz,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("store: unable to fetch entry row: %v", err)
|
return nil, fmt.Errorf("store: unable to fetch entry row: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -480,3 +514,16 @@ func NewAnonymousQueryBuilder(store *Storage) *EntryQueryBuilder {
|
||||||
store: store,
|
store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toSqliteFtsQuery(q string) string {
|
||||||
|
q = strings.TrimSpace(q)
|
||||||
|
if q == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
terms := strings.Fields(q)
|
||||||
|
for i, t := range terms {
|
||||||
|
t = strings.ReplaceAll(t, `"`, `""`)
|
||||||
|
terms[i] = `"` + t + `"`
|
||||||
|
}
|
||||||
|
return strings.Join(terms, " AND ")
|
||||||
|
}
|
||||||
|
|
|
@ -7,16 +7,19 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Storage handles all operations related to the database.
|
// Storage handles all operations related to the database.
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
db *sql.DB
|
kind database.DBKind
|
||||||
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStorage returns a new Storage.
|
// NewStorage returns a new Storage.
|
||||||
func NewStorage(db *sql.DB) *Storage {
|
func NewStorage(kind database.DBKind, db *sql.DB) *Storage {
|
||||||
return &Storage{db}
|
return &Storage{kind, db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseVersion returns the version of the database which is in use.
|
// DatabaseVersion returns the version of the database which is in use.
|
||||||
|
|
|
@ -226,9 +226,9 @@ Minimum number of database connections\&.
|
||||||
Default is 1\&.
|
Default is 1\&.
|
||||||
.TP
|
.TP
|
||||||
.B DATABASE_URL
|
.B DATABASE_URL
|
||||||
Postgresql connection parameters\&.
|
Database connection parameters\&.
|
||||||
.br
|
.br
|
||||||
Default is "user=postgres password=postgres dbname=miniflux2 sslmode=disable"\&.
|
Default is "postgres://postgres:postgres/postgres?sslmode=disable"\&.
|
||||||
.TP
|
.TP
|
||||||
.B DATABASE_URL_FILE
|
.B DATABASE_URL_FILE
|
||||||
Path to a secret key exposed as a file, it should contain $DATABASE_URL value\&.
|
Path to a secret key exposed as a file, it should contain $DATABASE_URL value\&.
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Miniflux
|
Description=Miniflux
|
||||||
Documentation=man:miniflux(1) https://miniflux.app/docs/index.html
|
Documentation=man:miniflux(1) https://miniflux.app/docs/index.html
|
||||||
After=network.target postgresql.service
|
After=network.target postgresql.service cockroachdb.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/miniflux
|
ExecStart=/usr/bin/miniflux
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue