mirror of
https://github.com/miniflux/v2.git
synced 2025-09-15 18:57:04 +00:00
Merge fd9072d187
into 87e65f800e
This commit is contained in:
commit
f43708732f
12 changed files with 1666 additions and 121 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" }
|
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
|
||||||
|
}
|
43
flake.nix
Normal file
43
flake.nix
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"miniflux.app/v2/internal/crypto"
|
"miniflux.app/v2/internal/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var schemaVersion = len(migrations)
|
var cockroachSchemaVersion = len(cockroachMigrations)
|
||||||
|
|
||||||
// Order is important. Add new migrations at the end of the list.
|
// Order is important. Add new migrations at the end of the list.
|
||||||
var migrations = [...]func(tx *sql.Tx) error{
|
var cockroachMigrations = []Migration{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
CREATE TABLE schema_version (
|
CREATE TABLE schema_version (
|
||||||
|
@ -123,13 +123,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
return nil
|
||||||
CREATE EXTENSION IF NOT EXISTS hstore;
|
|
||||||
ALTER TABLE users ADD COLUMN extra hstore;
|
|
||||||
CREATE INDEX users_extra_idx ON users using gin(extra);
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
|
@ -263,11 +257,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
return nil
|
||||||
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
|
@ -281,7 +271,13 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
|
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
|
||||||
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || title || ' ' || coalesce(content, '') for 1000000));
|
||||||
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
|
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -292,16 +288,6 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `
|
|
||||||
UPDATE
|
|
||||||
entries
|
|
||||||
SET
|
|
||||||
document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
|
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -325,6 +311,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
|
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
UPDATE entries SET changed_at = published_at;
|
UPDATE entries SET changed_at = published_at;
|
||||||
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
|
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
|
||||||
`
|
`
|
||||||
|
@ -350,19 +342,31 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE entries ADD COLUMN share_code text not null default '';
|
ALTER TABLE entries ADD COLUMN share_code text not null default '';
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
|
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
|
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, url);`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
|
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
|
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -430,6 +434,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
|
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
UPDATE entries SET created_at = published_at;
|
UPDATE entries SET created_at = published_at;
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -442,62 +452,9 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
ADD column google_id text not null default '',
|
ADD column google_id text not null default '',
|
||||||
ADD column openid_connect_id text not null default ''
|
ADD column openid_connect_id text not null default ''
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(`
|
|
||||||
DECLARE my_cursor CURSOR FOR
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
COALESCE(extra->'custom_css', '') as custom_css,
|
|
||||||
COALESCE(extra->'google_id', '') as google_id,
|
|
||||||
COALESCE(extra->'oidc_id', '') as oidc_id
|
|
||||||
FROM users
|
|
||||||
FOR UPDATE
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Exec("CLOSE my_cursor")
|
|
||||||
|
|
||||||
for {
|
|
||||||
var (
|
|
||||||
userID int64
|
|
||||||
customStylesheet string
|
|
||||||
googleID string
|
|
||||||
oidcID string
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := tx.Exec(
|
|
||||||
`UPDATE
|
|
||||||
users
|
|
||||||
SET
|
|
||||||
stylesheet=$2,
|
|
||||||
google_id=$3,
|
|
||||||
openid_connect_id=$4
|
|
||||||
WHERE
|
|
||||||
id=$1
|
|
||||||
`,
|
|
||||||
userID, customStylesheet, googleID, oidcID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
if _, err = tx.Exec(`ALTER TABLE users DROP COLUMN extra;`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
|
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
|
||||||
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
|
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
|
||||||
|
@ -506,7 +463,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url) WHERE length(url) < 2000;
|
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
|
||||||
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||||
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||||
`)
|
`)
|
||||||
|
@ -531,6 +488,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
|
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -566,6 +529,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
|
CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
|
ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -659,10 +628,21 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE users RENAME double_tap TO gesture_nav;
|
ALTER TABLE users ADD COLUMN gesture_nav text DEFAULT 'tap' NOT NULL;
|
||||||
ALTER TABLE users
|
`
|
||||||
ALTER COLUMN gesture_nav SET DATA TYPE text using case when gesture_nav = true then 'tap' when gesture_nav = false then 'none' end,
|
_, err = tx.Exec(sql)
|
||||||
ALTER COLUMN gesture_nav SET default 'tap';
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
UPDATE users SET gesture_nav = CASE WHEN double_tap THEN 'tap' ELSE 'none' END;
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
|
ALTER TABLE users DROP COLUMN double_tap;
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
|
@ -710,7 +690,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unique index
|
// Create unique index
|
||||||
_, err = tx.Exec(`CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx ON enclosures(user_id, entry_id, md5(url))`)
|
_, err = tx.Exec(`CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx ON enclosures(user_id, entry_id, url);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1045,6 +1025,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
sql := `
|
sql := `
|
||||||
ALTER TABLE icons ADD COLUMN external_id text default '';
|
ALTER TABLE icons ADD COLUMN external_id text default '';
|
||||||
|
`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `
|
||||||
CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
|
CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
|
||||||
`
|
`
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
|
@ -1079,7 +1065,6 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
UPDATE icons SET external_id = $1 WHERE id = $2
|
UPDATE icons SET external_id = $1 WHERE id = $2
|
||||||
`,
|
`,
|
||||||
crypto.GenerateRandomStringHex(20), id)
|
crypto.GenerateRandomStringHex(20), id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1160,7 +1145,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||||
},
|
},
|
||||||
// This migration replaces deprecated timezones by their equivalent on Debian Trixie.
|
// This migration replaces deprecated timezones by their equivalent on Debian Trixie.
|
||||||
func(tx *sql.Tx) (err error) {
|
func(tx *sql.Tx) (err error) {
|
||||||
var deprecatedTimeZoneMap = map[string]string{
|
deprecatedTimeZoneMap := map[string]string{
|
||||||
// Africa
|
// Africa
|
||||||
"Africa/Asmera": "Africa/Asmara",
|
"Africa/Asmera": "Africa/Asmara",
|
||||||
|
|
|
@ -7,13 +7,60 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DBKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DBKindPostgres DBKind = iota
|
||||||
|
DBKindCockroach
|
||||||
|
)
|
||||||
|
|
||||||
|
var dbKindProto = map[DBKind]string{
|
||||||
|
DBKindPostgres: "postgresql",
|
||||||
|
DBKindCockroach: "cockroachdb",
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbKindDriver = map[DBKind]string{
|
||||||
|
DBKindPostgres: "postgres",
|
||||||
|
DBKindCockroach: "postgres",
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbKindSchemaVersion = map[DBKind]int{
|
||||||
|
DBKindPostgres: postgresSchemaVersion,
|
||||||
|
DBKindCockroach: cockroachSchemaVersion,
|
||||||
|
}
|
||||||
|
|
||||||
// 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),
|
||||||
|
@ -51,7 +98,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 +108,18 @@ 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]
|
||||||
|
|
||||||
|
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
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"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 +71,21 @@ 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 := `
|
setweightPart := "setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
setweightPart = "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')
|
document_vectors = %s
|
||||||
WHERE
|
WHERE
|
||||||
id=$6 AND user_id=$7
|
id=$6 AND user_id=$7
|
||||||
`
|
`, setweightPart)
|
||||||
|
|
||||||
if _, err := s.db.Exec(
|
if _, err := s.db.Exec(
|
||||||
query,
|
query,
|
||||||
|
@ -100,7 +105,11 @@ 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 := `
|
setweightPart := "setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B')"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
setweightPart = "to_tsvector(substring($11 || ' ' || $11 || ' ' || coalesce($12, '') for 1000000))"
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
INSERT INTO entries
|
INSERT INTO entries
|
||||||
(
|
(
|
||||||
title,
|
title,
|
||||||
|
@ -130,12 +139,12 @@ 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'),
|
%s,
|
||||||
$13
|
$13
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
id, status, created_at, changed_at
|
id, status, created_at, changed_at
|
||||||
`
|
`, setweightPart)
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
|
@ -178,7 +187,11 @@ 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 := `
|
setweightPart := "setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B')"
|
||||||
|
if s.kind == database.DBKindCockroach {
|
||||||
|
setweightPart = "to_tsvector(substring($7 || ' ' || $7 || ' ' || coalesce($8, '') for 1000000))"
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
UPDATE
|
UPDATE
|
||||||
entries
|
entries
|
||||||
SET
|
SET
|
||||||
|
@ -188,13 +201,13 @@ 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'),
|
document_vectors = %s,
|
||||||
tags=$12
|
tags=$12
|
||||||
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
|
||||||
`
|
`, setweightPart)
|
||||||
err := tx.QueryRow(
|
err := tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue