Skip to content

Commit 7316b33

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 92a73b2 commit 7316b33

File tree

9 files changed

+66
-30
lines changed

9 files changed

+66
-30
lines changed

cmd/gonic/gonic.go

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

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

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

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: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ func construct(ctx MigrationContext, id string, f func(*gorm.DB, MigrationContex
7373
func migrateInitSchema(tx *gorm.DB, _ MigrationContext) error {
7474
return tx.AutoMigrate(
7575
Genre{},
76+
Artist{},
77+
Album{},
78+
Track{},
7679
TrackGenre{},
7780
AlbumGenre{},
78-
Track{},
79-
Artist{},
8081
User{},
8182
Setting{},
8283
Play{},
83-
Album{},
8484
Playlist{},
8585
PlayQueue{},
8686
).
@@ -144,12 +144,18 @@ func migrateAddGenre(tx *gorm.DB, _ MigrationContext) error {
144144

145145
func migrateUpdateTranscodePrefIDX(tx *gorm.DB, _ MigrationContext) error {
146146
var hasIDX int
147-
tx.
148-
Select("1").
149-
Table("sqlite_master").
150-
Where("type = ?", "index").
151-
Where("name = ?", "idx_user_id_client").
152-
Count(&hasIDX)
147+
if tx.Dialect().GetName() == "sqlite3" {
148+
tx.Select("1").
149+
Table("sqlite_master").
150+
Where("type = ?", "index").
151+
Where("name = ?", "idx_user_id_client").
152+
Count(&hasIDX)
153+
} else if tx.Dialect().GetName() == "postgres" {
154+
tx.Select("1").
155+
Table("pg_indexes").
156+
Where("indexname = ?", "idx_user_id_client").
157+
Count(&hasIDX)
158+
}
153159
if hasIDX == 1 {
154160
// index already exists
155161
return nil

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"
@@ -70,17 +71,19 @@ func New(opts Options) (*Server, error) {
7071
}
7172
r.Use(base.WithCORS)
7273

73-
sessKey, err := opts.DB.GetSetting("session_key")
74+
encSessKey, err := opts.DB.GetSetting("session_key")
7475
if err != nil {
7576
return nil, fmt.Errorf("get session key: %w", err)
7677
}
77-
if sessKey == "" {
78-
if err := opts.DB.SetSetting("session_key", string(securecookie.GenerateRandomKey(32))); err != nil {
78+
sessKey, err := base64.StdEncoding.DecodeString(encSessKey)
79+
if err != nil || len(sessKey) == 0 {
80+
sessKey = securecookie.GenerateRandomKey(32)
81+
if err := opts.DB.SetSetting("session_key", base64.StdEncoding.EncodeToString(sessKey)); err != nil {
7982
return nil, fmt.Errorf("set session key: %w", err)
8083
}
8184
}
8285

83-
sessDB := gormstore.New(opts.DB.DB, []byte(sessKey))
86+
sessDB := gormstore.New(opts.DB.DB, sessKey)
8487
sessDB.SessionOpts.HttpOnly = true
8588
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
8689

0 commit comments

Comments
 (0)