Skip to content

Commit 6f34516

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 6f34516

File tree

7 files changed

+82
-33
lines changed

7 files changed

+82
-33
lines changed

cmd/gonic/gonic.go

Lines changed: 21 additions & 8 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"
@@ -66,7 +68,12 @@ func main() {
6668

6769
confPlaylistsPath := flag.String("playlists-path", "", "path to your list of new or existing m3u playlists that gonic can manage")
6870

69-
confDBPath := flag.String("db-path", "gonic.db", "path to database (optional)")
71+
confSqlitePath := flag.String("db-path", "gonic.db", "path to database (optional, default: gonic.db)")
72+
confPostgresHost := flag.String("postgres-host", "", "name of the PostgreSQL gonicServer (optional)")
73+
confPostgresPort := flag.Int("postgres-port", 5432, "port to use for PostgreSQL connection (optional, default: 5432)")
74+
confPostgresName := flag.String("postgres-db", "gonic", "name of the PostgreSQL database (optional, default: gonic)")
75+
confPostgresUser := flag.String("postgres-user", "gonic", "name of the PostgreSQL user (optional, default: gonic)")
76+
confPostgresSslModel := flag.String("postgres-ssl-mode", "verify-full", "the ssl mode used for connecting to the PostreSQL instance (optional, default: verify-full)")
7077

7178
confScanIntervalMins := flag.Uint("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
7279
confScanAtStart := flag.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
@@ -136,15 +143,20 @@ func main() {
136143
log.Fatalf("couldn't create covers cache path: %v\n", err)
137144
}
138145

139-
dbc, err := db.New(*confDBPath, db.DefaultOptions())
146+
var dbc *db.DB
147+
if len(*confPostgresHost) > 0 {
148+
dbc, err = db.NewPostgres(*confPostgresHost, *confPostgresPort, *confPostgresName, *confPostgresUser, os.Getenv("GONIC_POSTGRES_PW"), *confPostgresSslModel)
149+
} else {
150+
dbc, err = db.NewSqlite3(*confSqlitePath, db.DefaultOptions())
151+
}
140152
if err != nil {
141153
log.Fatalf("error opening database: %v\n", err)
142154
}
143155
defer dbc.Close()
144156

145157
err = dbc.Migrate(db.MigrationContext{
146158
Production: true,
147-
DBPath: *confDBPath,
159+
DBPath: *confSqlitePath,
148160
OriginalMusicPath: confMusicPaths[0].path,
149161
PlaylistsPath: *confPlaylistsPath,
150162
PodcastsPath: *confPodcastPath,
@@ -225,17 +237,18 @@ func main() {
225237
jukebx = jukebox.New()
226238
}
227239

228-
sessKey, err := dbc.GetSetting("session_key")
240+
encSessKey, err := dbc.GetSetting("session_key")
229241
if err != nil {
230242
log.Panicf("error getting session key: %v\n", err)
231243
}
232-
if sessKey == "" {
233-
sessKey = string(securecookie.GenerateRandomKey(32))
234-
if err := dbc.SetSetting("session_key", sessKey); err != nil {
244+
sessKey, err := base64.StdEncoding.DecodeString(encSessKey)
245+
if err != nil || len(sessKey) == 0 {
246+
sessKey = securecookie.GenerateRandomKey(32)
247+
if err := dbc.SetSetting("session_key", base64.StdEncoding.EncodeToString(sessKey)); err != nil {
235248
log.Panicf("error setting session key: %v\n", err)
236249
}
237250
}
238-
sessDB := gormstore.New(dbc.DB, []byte(sessKey))
251+
sessDB := gormstore.New(dbc.DB, []byte(encSessKey))
239252
sessDB.SessionOpts.HttpOnly = true
240253
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
241254

db/db.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type DB struct {
4242
*gorm.DB
4343
}
4444

45-
func New(path string, options url.Values) (*DB, error) {
45+
func NewSqlite3(path string, options url.Values) (*DB, error) {
4646
// https://github.com/mattn/go-sqlite3#connection-string
4747
url := url.URL{
4848
Scheme: "file",
@@ -53,13 +53,26 @@ func New(path string, options url.Values) (*DB, error) {
5353
if err != nil {
5454
return nil, fmt.Errorf("with gorm: %w", err)
5555
}
56+
return newDB(db)
57+
}
58+
59+
func NewPostgres(host string, port int, databaseName string, username string, password string, sslmode string) (*DB, error) {
60+
pathAndArgs := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s", host, port, username, databaseName, password, sslmode)
61+
db, err := gorm.Open("postgres", pathAndArgs)
62+
if err != nil {
63+
return nil, fmt.Errorf("with gorm: %w", err)
64+
}
65+
return newDB(db)
66+
}
67+
68+
func newDB(db *gorm.DB) (*DB, error) {
5669
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
5770
db.DB().SetMaxOpenConns(1)
5871
return &DB{DB: db}, nil
5972
}
6073

6174
func NewMock() (*DB, error) {
62-
return New(":memory:", mockOptions())
75+
return NewSqlite3(":memory:", mockOptions())
6376
}
6477

6578
func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []int) error {
@@ -72,10 +85,11 @@ func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []in
7285
rows = append(rows, "(?, ?)")
7386
values = append(values, left, c)
7487
}
75-
q := fmt.Sprintf("INSERT OR IGNORE INTO %q (%s) VALUES %s",
88+
q := fmt.Sprintf("INSERT INTO %q (%s) VALUES %s ON CONFLICT (%s) DO NOTHING",
7689
table,
7790
strings.Join(head, ", "),
7891
strings.Join(rows, ", "),
92+
strings.Join(head, ", "),
7993
)
8094
return db.Exec(q, values...).Error
8195
}
@@ -612,8 +626,8 @@ func join[T fmt.Stringer](in []T, sep string) string {
612626
return strings.Join(strs, sep)
613627
}
614628

615-
func Dump(ctx context.Context, db *gorm.DB, to string) error {
616-
dest, err := New(to, url.Values{})
629+
func DumpToSqlite3(ctx context.Context, db *gorm.DB, to string) error {
630+
dest, err := NewSqlite3(to, url.Values{})
617631
if err != nil {
618632
return fmt.Errorf("create dest db: %w", err)
619633
}

db/migrations.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,18 @@ func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
179179

180180
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
181181
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)
182+
if tx.Dialect().GetName() == "sqlite3" {
183+
tx.Select("1").
184+
Table("sqlite_master").
185+
Where("type = ?", "index").
186+
Where("name = ?", "idx_user_id_client").
187+
Count(&hasIDX)
188+
} else if tx.Dialect().GetName() == "postgres" {
189+
tx.Select("1").
190+
Table("pg_indexes").
191+
Where("indexname = ?", "idx_user_id_client").
192+
Count(&hasIDX)
193+
}
188194
if hasIDX == 1 {
189195
// index already exists
190196
return nil
@@ -461,9 +467,15 @@ func migratePlaylistsQueuesToFullID(tx *gorm.DB, _ MigrationContext) error {
461467
if err := step.Error; err != nil {
462468
return fmt.Errorf("step migrate play_queues to full id: %w", err)
463469
}
464-
step = tx.Exec(`
470+
if tx.Dialect().GetName() == "postgres" {
471+
step = tx.Exec(`
472+
UPDATE play_queues SET newcurrent=('tr-' || current)::varchar[200];
473+
`)
474+
} else {
475+
step = tx.Exec(`
465476
UPDATE play_queues SET newcurrent=('tr-' || CAST(current AS varchar(10)));
466477
`)
478+
}
467479
if err := step.Error; err != nil {
468480
return fmt.Errorf("step migrate play_queues to full id: %w", err)
469481
}
@@ -590,7 +602,7 @@ func migrateAlbumArtistsMany2Many(tx *gorm.DB, _ MigrationContext) error {
590602
return fmt.Errorf("step insert from albums: %w", err)
591603
}
592604

593-
step = tx.Exec(`DROP INDEX idx_albums_tag_artist_id`)
605+
step = tx.Exec(`DROP INDEX IF EXISTS idx_albums_tag_artist_id`)
594606
if err := step.Error; err != nil {
595607
return fmt.Errorf("step drop index: %w", err)
596608
}
@@ -730,10 +742,10 @@ func migratePlaylistsPaths(tx *gorm.DB, ctx MigrationContext) error {
730742
}
731743

732744
func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error {
733-
if !ctx.Production {
745+
if ctx.Production {
734746
return nil
735747
}
736-
return Dump(context.Background(), tx, fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix()))
748+
return DumpToSqlite3(context.Background(), tx, fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix()))
737749
}
738750

739751
func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error {
@@ -770,12 +782,22 @@ func migrateArtistAppearances(tx *gorm.DB, _ MigrationContext) error {
770782
return fmt.Errorf("step transfer album artists: %w", err)
771783
}
772784

773-
step = tx.Exec(`
785+
if tx.Dialect().GetName() == "sqlite3" {
786+
step = tx.Exec(`
774787
INSERT OR IGNORE INTO artist_appearances (artist_id, album_id)
775788
SELECT track_artists.artist_id, tracks.album_id
776789
FROM track_artists
777790
JOIN tracks ON tracks.id=track_artists.track_id
778791
`)
792+
} else {
793+
step = tx.Exec(`
794+
INSERT INTO artist_appearances (artist_id, album_id)
795+
SELECT track_artists.artist_id, tracks.album_id
796+
FROM track_artists
797+
JOIN tracks ON tracks.id=track_artists.track_id
798+
ON CONFLICT DO NOTHING
799+
`)
800+
}
779801
if err := step.Error; err != nil {
780802
return fmt.Errorf("step transfer album artists: %w", err)
781803
}
@@ -795,7 +817,7 @@ func migrateTemporaryDisplayAlbumArtist(tx *gorm.DB, _ MigrationContext) error {
795817
return tx.Exec(`
796818
UPDATE albums
797819
SET tag_album_artist=(
798-
SELECT group_concat(artists.name, ', ')
820+
SELECT string_agg(artists.name, ', ')
799821
FROM artists
800822
JOIN album_artists ON album_artists.artist_id=artists.id AND album_artists.album_id=albums.id
801823
GROUP BY album_artists.album_id

mockfs/mockfs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (m *MockFS) DumpDB(suffix ...string) {
309309
p = append(p, suffix...)
310310

311311
destPath := filepath.Join(os.TempDir(), strings.Join(p, "-"))
312-
if err := db.Dump(context.Background(), m.db.DB, destPath); err != nil {
312+
if err := db.DumpToSqlite3(context.Background(), m.db.DB, destPath); err != nil {
313313
m.t.Fatalf("dumping db: %v", err)
314314
}
315315

server/ctrlsubsonic/handlers_by_folder.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
3131
}
3232
var folders []*db.Album
3333
c.dbc.
34-
Select("*, count(sub.id) child_count").
34+
Select("albums.*, count(sub.id) child_count").
3535
Preload("AlbumStar", "user_id=?", user.ID).
3636
Preload("AlbumRating", "user_id=?", user.ID).
3737
Joins("LEFT JOIN albums sub ON albums.id=sub.parent_id").
3838
Where("albums.parent_id IN ?", rootQ.SubQuery()).
3939
Group("albums.id").
40-
Order("albums.right_path COLLATE NOCASE").
40+
Order("albums.right_path").
4141
Find(&folders)
4242
// [a-z#] -> 27
4343
indexMap := make(map[string]*spec.Index, 27)
@@ -80,7 +80,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
8080
Where("parent_id=?", id.Value).
8181
Preload("AlbumStar", "user_id=?", user.ID).
8282
Preload("AlbumRating", "user_id=?", user.ID).
83-
Order("albums.right_path COLLATE NOCASE").
83+
Order("albums.right_path").
8484
Find(&childFolders)
8585
for _, ch := range childFolders {
8686
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(ch))

server/ctrlsubsonic/handlers_by_tags.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
2626
user := r.Context().Value(CtxUser).(*db.User)
2727
var artists []*db.Artist
2828
q := c.dbc.
29-
Select("*, count(album_artists.album_id) album_count").
29+
Select("artists.*, count(album_artists.album_id) album_count").
3030
Joins("JOIN album_artists ON album_artists.artist_id=artists.id").
3131
Preload("ArtistStar", "user_id=?", user.ID).
3232
Preload("ArtistRating", "user_id=?", user.ID).
3333
Preload("Info").
3434
Group("artists.id").
35-
Order("artists.name COLLATE NOCASE")
35+
Order("artists.name")
3636
if m := getMusicFolder(c.musicPaths, params); m != "" {
3737
q = q.
3838
Joins("JOIN albums ON albums.id=album_artists.album_id").
@@ -230,7 +230,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
230230
// search artists
231231
var artists []*db.Artist
232232
q := c.dbc.
233-
Select("*, count(albums.id) album_count").
233+
Select("artists.*, count(albums.id) album_count").
234234
Group("artists.id")
235235
for _, s := range queries {
236236
q = q.Where(`name LIKE ? OR name_u_dec LIKE ?`, s, s)

server/ctrlsubsonic/handlers_raw.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ func streamGetTransodePreference(dbc *db.DB, userID int, client string) (*db.Tra
256256
var pref db.TranscodePreference
257257
err := dbc.
258258
Where("user_id=?", userID).
259-
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
259+
Where("client IN (?)", []string{"*", client}).
260260
Order("client DESC"). // ensure "*" is last if it's there
261261
First(&pref).
262262
Error

0 commit comments

Comments
 (0)