Skip to content

Commit 0031a94

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 16e6046 commit 0031a94

File tree

9 files changed

+73
-31
lines changed

9 files changed

+73
-31
lines changed

cmd/gonic/gonic.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"github.com/google/shlex"
18+
_ "github.com/jinzhu/gorm/dialects/postgres"
1819
_ "github.com/jinzhu/gorm/dialects/sqlite"
1920
"github.com/oklog/run"
2021
"github.com/peterbourgon/ff"
@@ -38,7 +39,12 @@ func main() {
3839
confTLSKey := set.String("tls-key", "", "path to TLS private key (optional)")
3940
confPodcastPath := set.String("podcast-path", "", "path to podcasts")
4041
confCachePath := set.String("cache-path", "", "path to cache")
41-
confDBPath := set.String("db-path", "gonic.db", "path to database (optional)")
42+
confSqlitePath := set.String("db-path", "gonic.db", "path to database (optional, default: gonic.db)")
43+
confPostgresHost := set.String("postgres-host", "", "name of the PostgreSQL gonicServer (optional)")
44+
confPostgresPort := set.Int("postgres-port", 5432, "port to use for PostgreSQL connection (optional, default: 5432)")
45+
confPostgresName := set.String("postgres-db", "gonic", "name of the PostgreSQL database (optional, default: gonic)")
46+
confPostgresUser := set.String("postgres-user", "gonic", "name of the PostgreSQL user (optional, default: gonic)")
47+
confPostgresSslModel := set.String("postgres-ssl-mode", "verify-full", "the ssl mode used for connecting to the PostreSQL instance (optional, default: verify-full)")
4248
confScanIntervalMins := set.Int("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
4349
confScanAtStart := set.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
4450
confScanWatcher := set.Bool("scan-watcher-enabled", false, "whether to watch file system for new music and rescan (optional)")
@@ -104,7 +110,13 @@ func main() {
104110
}
105111
}
106112

107-
dbc, err := db.New(*confDBPath, db.DefaultOptions())
113+
var dbc *db.DB
114+
var err error
115+
if len(*confPostgresHost) > 0 {
116+
dbc, err = db.NewPostgres(*confPostgresHost, *confPostgresPort, *confPostgresName, *confPostgresUser, os.Getenv("GONIC_POSTGRES_PW"), *confPostgresSslModel)
117+
} else {
118+
dbc, err = db.NewSqlite3(*confSqlitePath, db.DefaultOptions())
119+
}
108120
if err != nil {
109121
log.Fatalf("error opening database: %v\n", err)
110122
}

db/db.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type DB struct {
3434
*gorm.DB
3535
}
3636

37-
func New(path string, options url.Values) (*DB, error) {
37+
func NewSqlite3(path string, options url.Values) (*DB, error) {
3838
// https://github.com/mattn/go-sqlite3#connection-string
3939
url := url.URL{
4040
Scheme: "file",
@@ -45,13 +45,26 @@ func New(path string, options url.Values) (*DB, error) {
4545
if err != nil {
4646
return nil, fmt.Errorf("with gorm: %w", err)
4747
}
48+
return newDB(db)
49+
}
50+
51+
func NewPostgres(host string, port int, databaseName string, username string, password string, sslmode string) (*DB, error) {
52+
pathAndArgs := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s", host, port, username, databaseName, password, sslmode)
53+
db, err := gorm.Open("postgres", pathAndArgs)
54+
if err != nil {
55+
return nil, fmt.Errorf("with gorm: %w", err)
56+
}
57+
return newDB(db)
58+
}
59+
60+
func newDB(db *gorm.DB) (*DB, error) {
4861
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
4962
db.DB().SetMaxOpenConns(1)
5063
return &DB{DB: db}, nil
5164
}
5265

5366
func NewMock() (*DB, error) {
54-
return New(":memory:", mockOptions())
67+
return NewSqlite3(":memory:", mockOptions())
5568
}
5669

5770
func (db *DB) GetSetting(key string) (string, error) {
@@ -80,10 +93,11 @@ func (db *DB) InsertBulkLeftMany(table string, head []string, left int, col []in
8093
rows = append(rows, "(?, ?)")
8194
values = append(values, left, c)
8295
}
83-
q := fmt.Sprintf("INSERT OR IGNORE INTO %q (%s) VALUES %s",
96+
q := fmt.Sprintf("INSERT INTO %q (%s) VALUES %s ON CONFLICT (%s) DO NOTHING",
8497
table,
8598
strings.Join(head, ", "),
8699
strings.Join(rows, ", "),
100+
strings.Join(head, ", "),
87101
)
88102
return db.Exec(q, values...).Error
89103
}

db/migrations.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContex
7474
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
7575
return tx.AutoMigrate(
7676
Genre{},
77+
Artist{},
78+
Album{},
79+
Track{},
7780
TrackGenre{},
7881
AlbumGenre{},
79-
Track{},
80-
Artist{},
8182
User{},
8283
Setting{},
8384
Play{},
84-
Album{},
8585
Playlist{},
8686
PlayQueue{},
8787
).
@@ -145,12 +145,18 @@ func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
145145

146146
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
147147
var hasIDX int
148-
tx.
149-
Select("1").
150-
Table("sqlite_master").
151-
Where("type = ?", "index").
152-
Where("name = ?", "idx_user_id_client").
153-
Count(&hasIDX)
148+
if tx.Dialect().GetName() == "sqlite3" {
149+
tx.Select("1").
150+
Table("sqlite_master").
151+
Where("type = ?", "index").
152+
Where("name = ?", "idx_user_id_client").
153+
Count(&hasIDX)
154+
} else if tx.Dialect().GetName() == "postgres" {
155+
tx.Select("1").
156+
Table("pg_indexes").
157+
Where("indexname = ?", "idx_user_id_client").
158+
Count(&hasIDX)
159+
}
154160
if hasIDX == 1 {
155161
// index already exists
156162
return nil
@@ -420,9 +426,15 @@ func migratePlaylistsQueuesToFullID(tx *gorm.DB, _ MigrationContext) error {
420426
if err := step.Error; err != nil {
421427
return fmt.Errorf("step migrate play_queues to full id: %w", err)
422428
}
423-
step = tx.Exec(`
429+
if tx.Dialect().GetName() == "postgres" {
430+
step = tx.Exec(`
431+
UPDATE play_queues SET newcurrent=('tr-' || current)::varchar[200];
432+
`)
433+
} else {
434+
step = tx.Exec(`
424435
UPDATE play_queues SET newcurrent=('tr-' || CAST(current AS varchar(10)));
425436
`)
437+
}
426438
if err := step.Error; err != nil {
427439
return fmt.Errorf("step migrate play_queues to full id: %w", err)
428440
}

mockfs/mockfs.go

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

291291
destPath := filepath.Join(os.TempDir(), strings.Join(p, "-"))
292-
dest, err := db.New(destPath, url.Values{})
292+
dest, err := db.NewSqlite3(destPath, url.Values{})
293293
if err != nil {
294294
m.t.Fatalf("create dest db: %v", err)
295295
}

scanner/scanner_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ func TestMultiFolderWithSharedArtist(t *testing.T) {
397397

398398
sq := func(db *gorm.DB) *gorm.DB {
399399
return db.
400-
Select("*, count(sub.id) child_count, sum(sub.length) duration").
400+
Select("albums.*, count(sub.id) child_count, sum(sub.length) duration").
401401
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
402402
Group("albums.id")
403403
}

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.DB.
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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
2424
user := r.Context().Value(CtxUser).(*db.User)
2525
var artists []*db.Artist
2626
q := c.DB.
27-
Select("*, count(sub.id) album_count").
27+
Select("artists.*, count(sub.id) album_count").
2828
Joins("LEFT JOIN albums sub ON artists.id=sub.tag_artist_id").
2929
Preload("ArtistStar", "user_id=?", user.ID).
3030
Preload("ArtistRating", "user_id=?", user.ID).
3131
Group("artists.id").
32-
Order("artists.name COLLATE NOCASE")
32+
Order("artists.name")
3333
if m := getMusicFolder(c.MusicPaths, params); m != "" {
3434
q = q.Where("sub.root_dir=?", m)
3535
}
@@ -68,7 +68,7 @@ func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
6868
c.DB.
6969
Preload("Albums", func(db *gorm.DB) *gorm.DB {
7070
return db.
71-
Select("*, count(sub.id) child_count, sum(sub.length) duration").
71+
Select("albums.*, count(sub.id) child_count, sum(sub.length) duration").
7272
Joins("LEFT JOIN tracks sub ON albums.id=sub.album_id").
7373
Preload("AlbumStar", "user_id=?", user.ID).
7474
Preload("AlbumRating", "user_id=?", user.ID).
@@ -99,6 +99,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
9999
err = c.DB.
100100
Select("albums.*, count(tracks.id) child_count, sum(tracks.length) duration").
101101
Joins("LEFT JOIN tracks ON tracks.album_id=albums.id").
102+
Group("albums.id").
102103
Preload("TagArtist").
103104
Preload("Genres").
104105
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
@@ -163,14 +164,14 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
163164
case "frequent":
164165
user := r.Context().Value(CtxUser).(*db.User)
165166
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", user.ID)
166-
q = q.Order("plays.count DESC")
167+
q = q.Order("SUM(plays.count) DESC")
167168
case "newest":
168169
q = q.Order("created_at DESC")
169170
case "random":
170171
q = q.Order(gorm.Expr("random()"))
171172
case "recent":
172173
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?", user.ID)
173-
q = q.Order("plays.time DESC")
174+
q = q.Order("MAX(plays.time) DESC")
174175
case "starred":
175176
q = q.Joins("JOIN album_stars ON albums.id=album_stars.album_id AND album_stars.user_id=?", user.ID)
176177
q = q.Order("tag_title")
@@ -218,7 +219,7 @@ func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
218219
// search artists
219220
var artists []*db.Artist
220221
q := c.DB.
221-
Select("*, count(albums.id) album_count").
222+
Select("artists.*, count(albums.id) album_count").
222223
Group("artists.id").
223224
Where("name LIKE ? OR name_u_dec LIKE ?", query, query).
224225
Joins("JOIN albums ON albums.tag_artist_id=artists.id").

server/ctrlsubsonic/handlers_raw.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func streamGetTransPref(dbc *db.DB, userID int, client string) (*db.TranscodePre
3030
var pref db.TranscodePreference
3131
err := dbc.
3232
Where("user_id=?", userID).
33-
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
33+
Where("client IN (?)", []string{"*", client}).
3434
Order("client DESC"). // ensure "*" is last if it's there
3535
First(&pref).
3636
Error

server/server.go

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

33
import (
4+
"encoding/base64"
45
"fmt"
56
"log"
67
"net/http"
@@ -67,17 +68,19 @@ func New(opts Options) (*Server, error) {
6768
r.Use(base.WithCORS)
6869
r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
6970

70-
sessKey, err := opts.DB.GetSetting("session_key")
71+
encSessKey, err := opts.DB.GetSetting("session_key")
7172
if err != nil {
7273
return nil, fmt.Errorf("get session key: %w", err)
7374
}
74-
if sessKey == "" {
75-
if err := opts.DB.SetSetting("session_key", string(securecookie.GenerateRandomKey(32))); err != nil {
75+
sessKey, err := base64.StdEncoding.DecodeString(encSessKey)
76+
if err != nil || len(sessKey) == 0 {
77+
sessKey = securecookie.GenerateRandomKey(32)
78+
if err := opts.DB.SetSetting("session_key", base64.StdEncoding.EncodeToString(sessKey)); err != nil {
7679
return nil, fmt.Errorf("set session key: %w", err)
7780
}
7881
}
7982

80-
sessDB := gormstore.New(opts.DB.DB, []byte(sessKey))
83+
sessDB := gormstore.New(opts.DB.DB, sessKey)
8184
sessDB.SessionOpts.HttpOnly = true
8285
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
8386

0 commit comments

Comments
 (0)