Skip to content

Commit 72ee24d

Browse files
committed
Adding support for PostgreSQL as database
This adds support for a second database backend: PostgreSQL (in addition to sqlite3). This allows externailzing the database used by gonic.
1 parent 93ce039 commit 72ee24d

File tree

12 files changed

+156
-139
lines changed

12 files changed

+156
-139
lines changed

cmd/gonic/gonic.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33

44
import (
55
"context"
6+
"encoding/base64"
67
"errors"
78
"expvar"
89
"flag"
@@ -26,6 +27,7 @@ import (
2627

2728
"github.com/google/shlex"
2829
"github.com/gorilla/securecookie"
30+
_ "github.com/jinzhu/gorm/dialects/postgres"
2931
_ "github.com/jinzhu/gorm/dialects/sqlite"
3032
"github.com/sentriz/gormstore"
3133
"golang.org/x/sync/errgroup"
@@ -65,8 +67,7 @@ func main() {
6567
flag.Var(&confMusicPaths, "music-path", "path to music")
6668

6769
confPlaylistsPath := flag.String("playlists-path", "", "path to your list of new or existing m3u playlists that gonic can manage")
68-
69-
confDBPath := flag.String("db-path", "gonic.db", "path to database (optional)")
70+
confDBURI := flag.String("db-uri", "", "db URI")
7071

7172
confScanIntervalMins := flag.Uint("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
7273
confScanAtStart := flag.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
@@ -92,6 +93,7 @@ func main() {
9293
confExpvar := flag.Bool("expvar", false, "enable the /debug/vars endpoint (optional)")
9394

9495
deprecatedConfGenreSplit := flag.String("genre-split", "", "(deprecated, see multi-value settings)")
96+
deprecatedConfDBPath := flag.String("db-path", "gonic.db", "(deprecated, see db-uri)")
9597

9698
flag.Parse()
9799
flagconf.ParseEnv()
@@ -136,15 +138,18 @@ func main() {
136138
log.Fatalf("couldn't create covers cache path: %v\n", err)
137139
}
138140

139-
dbc, err := db.New(*confDBPath, db.DefaultOptions())
141+
if *confDBURI == "" {
142+
*confDBURI = "sqlite3://" + *deprecatedConfDBPath
143+
}
144+
145+
dbc, err := db.New(*confDBURI)
140146
if err != nil {
141147
log.Fatalf("error opening database: %v\n", err)
142148
}
143149
defer dbc.Close()
144150

145151
err = dbc.Migrate(db.MigrationContext{
146152
Production: true,
147-
DBPath: *confDBPath,
148153
OriginalMusicPath: confMusicPaths[0].path,
149154
PlaylistsPath: *confPlaylistsPath,
150155
PodcastsPath: *confPodcastPath,
@@ -225,17 +230,18 @@ func main() {
225230
jukebx = jukebox.New()
226231
}
227232

228-
sessKey, err := dbc.GetSetting("session_key")
233+
encSessKey, err := dbc.GetSetting("session_key")
229234
if err != nil {
230235
log.Panicf("error getting session key: %v\n", err)
231236
}
232-
if sessKey == "" {
233-
sessKey = string(securecookie.GenerateRandomKey(32))
234-
if err := dbc.SetSetting("session_key", sessKey); err != nil {
237+
sessKey, err := base64.StdEncoding.DecodeString(encSessKey)
238+
if err != nil || len(sessKey) == 0 {
239+
sessKey = securecookie.GenerateRandomKey(32)
240+
if err := dbc.SetSetting("session_key", base64.StdEncoding.EncodeToString(sessKey)); err != nil {
235241
log.Panicf("error setting session key: %v\n", err)
236242
}
237243
}
238-
sessDB := gormstore.New(dbc.DB, []byte(sessKey))
244+
sessDB := gormstore.New(dbc.DB, []byte(encSessKey))
239245
sessDB.SessionOpts.HttpOnly = true
240246
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
241247

db/db.go

Lines changed: 32 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package db
22

33
import (
4-
"context"
54
"errors"
65
"fmt"
76
"log"
@@ -13,55 +12,55 @@ import (
1312
"time"
1413

1514
"github.com/jinzhu/gorm"
16-
"github.com/mattn/go-sqlite3"
15+
_ "github.com/jinzhu/gorm/dialects/postgres"
16+
_ "github.com/jinzhu/gorm/dialects/sqlite"
1717

1818
// TODO: remove this dep
1919
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
2020
)
2121

22-
func DefaultOptions() url.Values {
23-
return url.Values{
24-
// with this, multiple connections share a single data and schema cache.
25-
// see https://www.sqlite.org/sharedcache.html
26-
"cache": {"shared"},
27-
// with this, the db sleeps for a little while when locked. can prevent
28-
// a SQLITE_BUSY. see https://www.sqlite.org/c3ref/busy_timeout.html
29-
"_busy_timeout": {"30000"},
30-
"_journal_mode": {"WAL"},
31-
"_foreign_keys": {"true"},
32-
}
22+
type DB struct {
23+
*gorm.DB
3324
}
3425

35-
func mockOptions() url.Values {
36-
return url.Values{
37-
"_foreign_keys": {"true"},
26+
func New(uri string) (*DB, error) {
27+
if uri == "" {
28+
return nil, fmt.Errorf("empty db uri")
3829
}
39-
}
4030

41-
type DB struct {
42-
*gorm.DB
43-
}
31+
url, err := url.Parse(uri)
32+
if err != nil {
33+
return nil, fmt.Errorf("parse uri: %w", err)
34+
}
4435

45-
func New(path string, options url.Values) (*DB, error) {
46-
// https://github.com/mattn/go-sqlite3#connection-string
47-
url := url.URL{
48-
Scheme: "file",
49-
Opaque: path,
36+
gormURL := strings.TrimPrefix(url.String(), url.Scheme+"://")
37+
38+
//nolint:goconst
39+
switch url.Scheme {
40+
case "sqlite3":
41+
q := url.Query()
42+
q.Set("cache", "shared")
43+
q.Set("_busy_timeout", "30000")
44+
q.Set("_journal_mode", "WAL")
45+
q.Set("_foreign_keys", "true")
46+
url.RawQuery = q.Encode()
47+
case "postgres":
48+
// the postgres driver expects the schema prefix to be on the URL
49+
gormURL = url.String()
50+
default:
51+
return nil, fmt.Errorf("unknown db scheme")
5052
}
51-
url.RawQuery = options.Encode()
52-
db, err := gorm.Open("sqlite3", url.String())
53+
54+
db, err := gorm.Open(url.Scheme, gormURL)
5355
if err != nil {
5456
return nil, fmt.Errorf("with gorm: %w", err)
5557
}
58+
5659
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
5760
db.DB().SetMaxOpenConns(1)
5861
return &DB{DB: db}, nil
5962
}
6063

61-
func NewMock() (*DB, error) {
62-
return New(":memory:", mockOptions())
63-
}
64-
6564
func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error {
6665
if len(col) == 0 {
6766
return nil
@@ -72,10 +71,11 @@ func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []in
7271
rows = append(rows, "(?, ?)")
7372
values = append(values, left, c)
7473
}
75-
q := fmt.Sprintf("INSERT OR IGNORE INTO %q (%s) VALUES %s",
74+
q := fmt.Sprintf("INSERT INTO %q (%s) VALUES %s ON CONFLICT (%s) DO NOTHING",
7675
table,
7776
strings.Join(head, ", "),
7877
strings.Join(rows, ", "),
78+
strings.Join(head, ", "),
7979
)
8080
return db.Exec(q, values...).Error
8181
}
@@ -611,45 +611,3 @@ func join[T fmt.Stringer](in []T, sep string) string {
611611
}
612612
return strings.Join(strs, sep)
613613
}
614-
615-
func Dump(ctx context.Context, db *gorm.DB, to string) error {
616-
dest, err := New(to, url.Values{})
617-
if err != nil {
618-
return fmt.Errorf("create dest db: %w", err)
619-
}
620-
defer dest.Close()
621-
622-
connSrc, err := db.DB().Conn(ctx)
623-
if err != nil {
624-
return fmt.Errorf("getting src raw conn: %w", err)
625-
}
626-
defer connSrc.Close()
627-
628-
connDest, err := dest.DB.DB().Conn(ctx)
629-
if err != nil {
630-
return fmt.Errorf("getting dest raw conn: %w", err)
631-
}
632-
defer connDest.Close()
633-
634-
err = connDest.Raw(func(connDest interface{}) error {
635-
return connSrc.Raw(func(connSrc interface{}) error {
636-
connDestq := connDest.(*sqlite3.SQLiteConn)
637-
connSrcq := connSrc.(*sqlite3.SQLiteConn)
638-
bk, err := connDestq.Backup("main", connSrcq, "main")
639-
if err != nil {
640-
return fmt.Errorf("create backup db: %w", err)
641-
}
642-
for done, _ := bk.Step(-1); !done; { //nolint: revive
643-
}
644-
if err := bk.Finish(); err != nil {
645-
return fmt.Errorf("finishing dump: %w", err)
646-
}
647-
return nil
648-
})
649-
})
650-
if err != nil {
651-
return fmt.Errorf("backing up: %w", err)
652-
}
653-
654-
return nil
655-
}

db/db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestGetSetting(t *testing.T) {
2222
key := SettingKey(randKey())
2323
value := "howdy"
2424

25-
testDB, err := NewMock()
25+
testDB, err := New("sqlite3://:memory:")
2626
if err != nil {
2727
t.Fatalf("error creating db: %v", err)
2828
}

db/migrations.go

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package db
33

44
import (
5-
"context"
65
"errors"
76
"fmt"
87
"log"
@@ -20,7 +19,6 @@ import (
2019

2120
type MigrationContext struct {
2221
Production bool
23-
DBPath string
2422
OriginalMusicPath string
2523
PlaylistsPath string
2624
PodcastsPath string
@@ -59,7 +57,6 @@ func (db *DB) Migrate(ctx MigrationContext) error {
5957
construct(ctx, "202206101425", migrateUser),
6058
construct(ctx, "202207251148", migrateStarRating),
6159
construct(ctx, "202211111057", migratePlaylistsQueuesToFullID),
62-
constructNoTx(ctx, "202212272312", backupDBPre016),
6360
construct(ctx, "202304221528", migratePlaylistsToM3U),
6461
construct(ctx, "202305301718", migratePlayCountToLength),
6562
construct(ctx, "202307281628", migrateAlbumArtistsMany2Many),
@@ -106,14 +103,14 @@ func constructNoTx(ctx MigrationContext, id string, f func(*gorm.DB, MigrationCo
106103
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
107104
return tx.AutoMigrate(
108105
Genre{},
106+
Artist{},
107+
Album{},
108+
Track{},
109109
TrackGenre{},
110110
AlbumGenre{},
111-
Track{},
112-
Artist{},
113111
User{},
114112
Setting{},
115113
Play{},
116-
Album{},
117114
PlayQueue{},
118115
).
119116
Error
@@ -179,12 +176,18 @@ func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
179176

180177
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
181178
var hasIDX int
182-
tx.
183-
Select("1").
184-
Table("sqlite_master").
185-
Where("type = ?", "index").
186-
Where("name = ?", "idx_user_id_client").
187-
Count(&hasIDX)
179+
if tx.Dialect().GetName() == "sqlite3" {
180+
tx.Select("1").
181+
Table("sqlite_master").
182+
Where("type = ?", "index").
183+
Where("name = ?", "idx_user_id_client").
184+
Count(&hasIDX)
185+
} else if tx.Dialect().GetName() == "postgres" {
186+
tx.Select("1").
187+
Table("pg_indexes").
188+
Where("indexname = ?", "idx_user_id_client").
189+
Count(&hasIDX)
190+
}
188191
if hasIDX == 1 {
189192
// index already exists
190193
return nil
@@ -461,9 +464,15 @@ func migratePlaylistsQueuesToFullID(tx *gorm.DB, _ MigrationContext) error {
461464
if err := step.Error; err != nil {
462465
return fmt.Errorf("step migrate play_queues to full id: %w", err)
463466
}
464-
step = tx.Exec(`
467+
if tx.Dialect().GetName() == "postgres" {
468+
step = tx.Exec(`
469+
UPDATE play_queues SET newcurrent=('tr-' || current)::varchar[200];
470+
`)
471+
} else {
472+
step = tx.Exec(`
465473
UPDATE play_queues SET newcurrent=('tr-' || CAST(current AS varchar(10)));
466474
`)
475+
}
467476
if err := step.Error; err != nil {
468477
return fmt.Errorf("step migrate play_queues to full id: %w", err)
469478
}
@@ -590,7 +599,7 @@ func migrateAlbumArtistsMany2Many(tx *gorm.DB, _ MigrationContext) error {
590599
return fmt.Errorf("step insert from albums: %w", err)
591600
}
592601

593-
step = tx.Exec(`DROP INDEX idx_albums_tag_artist_id`)
602+
step = tx.Exec(`DROP INDEX IF EXISTS idx_albums_tag_artist_id`)
594603
if err := step.Error; err != nil {
595604
return fmt.Errorf("step drop index: %w", err)
596605
}
@@ -729,13 +738,6 @@ func migratePlaylistsPaths(tx *gorm.DB, ctx MigrationContext) error {
729738
return nil
730739
}
731740

732-
func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error {
733-
if !ctx.Production {
734-
return nil
735-
}
736-
return Dump(context.Background(), tx, fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix()))
737-
}
738-
739741
func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error {
740742
return tx.AutoMigrate(Album{}).Error
741743
}
@@ -770,12 +772,22 @@ func migrateArtistAppearances(tx *gorm.DB, _ MigrationContext) error {
770772
return fmt.Errorf("step transfer album artists: %w", err)
771773
}
772774

773-
step = tx.Exec(`
775+
if tx.Dialect().GetName() == "sqlite3" {
776+
step = tx.Exec(`
774777
INSERT OR IGNORE INTO artist_appearances (artist_id, album_id)
775778
SELECT track_artists.artist_id, tracks.album_id
776779
FROM track_artists
777780
JOIN tracks ON tracks.id=track_artists.track_id
778781
`)
782+
} else {
783+
step = tx.Exec(`
784+
INSERT INTO artist_appearances (artist_id, album_id)
785+
SELECT track_artists.artist_id, tracks.album_id
786+
FROM track_artists
787+
JOIN tracks ON tracks.id=track_artists.track_id
788+
ON CONFLICT DO NOTHING
789+
`)
790+
}
779791
if err := step.Error; err != nil {
780792
return fmt.Errorf("step transfer album artists: %w", err)
781793
}
@@ -795,7 +807,7 @@ func migrateTemporaryDisplayAlbumArtist(tx *gorm.DB, _ MigrationContext) error {
795807
return tx.Exec(`
796808
UPDATE albums
797809
SET tag_album_artist=(
798-
SELECT group_concat(artists.name, ', ')
810+
SELECT string_agg(artists.name, ', ')
799811
FROM artists
800812
JOIN album_artists ON album_artists.artist_id=artists.id AND album_artists.album_id=albums.id
801813
GROUP BY album_artists.album_id

0 commit comments

Comments
 (0)