diff --git a/conf/configuration.go b/conf/configuration.go index 82a28f743..e885a01f7 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -11,6 +11,7 @@ type sonic struct { Port string `default:"4533"` MusicFolder string `default:"./music"` DbPath string `default:"./data/cloudsonic.db"` + LogLevel string `default:"info"` IgnoredArticles string `default:"The El La Los Las Le Les Os As O A"` IndexGroups string `default:"A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)"` @@ -21,10 +22,8 @@ type sonic struct { PlsIgnoreFolders bool `default:"true"` PlsIgnoredPatterns string `default:"^iCloud;\\~"` - // DevFlags - LogLevel string `default:"info"` + // DevFlags. These are used to enable/disable debugging and incomplete features DevDisableAuthentication bool `default:"false"` - DevDisableFileCheck bool `default:"false"` DevDisableBanner bool `default:"false"` DevInitialPassword string `default:""` } diff --git a/engine/browser.go b/engine/browser.go index 962e56490..abd22bb72 100644 --- a/engine/browser.go +++ b/engine/browser.go @@ -14,13 +14,13 @@ import ( ) type Browser interface { - MediaFolders() (model.MediaFolders, error) - Indexes(ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) + MediaFolders(ctx context.Context) (model.MediaFolders, error) + Indexes(ctx context.Context, ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) Directory(ctx context.Context, id string) (*DirectoryInfo, error) Artist(ctx context.Context, id string) (*DirectoryInfo, error) Album(ctx context.Context, id string) (*DirectoryInfo, error) GetSong(ctx context.Context, id string) (*Entry, error) - GetGenres() (model.Genres, error) + GetGenres(ctx context.Context) (model.Genres, error) } func NewBrowser(ds model.DataStore) Browser { @@ -31,11 +31,11 @@ type browser struct { ds model.DataStore } -func (b *browser) MediaFolders() (model.MediaFolders, error) { +func (b *browser) MediaFolders(ctx context.Context) (model.MediaFolders, error) { return b.ds.MediaFolder().GetAll() } -func (b *browser) Indexes(ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) { +func (b *browser) Indexes(ctx context.Context, ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) { l, err := b.ds.Property().DefaultGet(model.PropLastScan, "-1") ms, _ := strconv.ParseInt(l, 10, 64) lastModified := utils.ToTime(ms) @@ -136,7 +136,7 @@ func (b *browser) GetSong(ctx context.Context, id string) (*Entry, error) { return &entry, nil } -func (b *browser) GetGenres() (model.Genres, error) { +func (b *browser) GetGenres(ctx context.Context) (model.Genres, error) { genres, err := b.ds.Genre().GetAll() for i, g := range genres { if strings.TrimSpace(g.Name) == "" { diff --git a/engine/browser_test.go b/engine/browser_test.go index 237579098..968f08b93 100644 --- a/engine/browser_test.go +++ b/engine/browser_test.go @@ -1,6 +1,7 @@ package engine import ( + "context" "errors" "github.com/cloudsonic/sonic-server/model" @@ -24,7 +25,7 @@ var _ = Describe("Browser", func() { }) It("returns sorted data", func() { - Expect(b.GetGenres()).To(Equal(model.Genres{ + Expect(b.GetGenres(context.TODO())).To(Equal(model.Genres{ {Name: "", SongCount: 13, AlbumCount: 13}, {Name: "Electronic", SongCount: 4000, AlbumCount: 40}, {Name: "Rock", SongCount: 1000, AlbumCount: 100}, @@ -33,7 +34,7 @@ var _ = Describe("Browser", func() { It("bubbles up errors", func() { repo.err = errors.New("generic error") - _, err := b.GetGenres() + _, err := b.GetGenres(context.TODO()) Expect(err).ToNot(BeNil()) }) }) diff --git a/engine/cover.go b/engine/cover.go index 41250cb95..8f46c0868 100644 --- a/engine/cover.go +++ b/engine/cover.go @@ -2,6 +2,7 @@ package engine import ( "bytes" + "context" "errors" "image" _ "image/gif" @@ -17,7 +18,7 @@ import ( ) type Cover interface { - Get(id string, size int, out io.Writer) error + Get(ctx context.Context, id string, size int, out io.Writer) error } type cover struct { @@ -49,7 +50,7 @@ func (c *cover) getCoverPath(id string) (string, error) { return "", model.ErrNotFound } -func (c *cover) Get(id string, size int, out io.Writer) error { +func (c *cover) Get(ctx context.Context, id string, size int, out io.Writer) error { path, err := c.getCoverPath(id) if err != nil && err != model.ErrNotFound { return err diff --git a/engine/cover_test.go b/engine/cover_test.go index 2a1d39dbd..8f068fc54 100644 --- a/engine/cover_test.go +++ b/engine/cover_test.go @@ -2,6 +2,7 @@ package engine_test import ( "bytes" + "context" "image" "testing" @@ -25,7 +26,7 @@ func TestCover(t *testing.T) { Convey("Subject: GetCoverArt Endpoint", t, func() { Convey("When id is not found", func() { mockMediaFileRepo.SetData(`[]`, 1) - err := cover.Get("1", 0, out) + err := cover.Get(context.TODO(), "1", 0, out) Convey("Then return default cover", func() { So(err, ShouldBeNil) @@ -34,7 +35,7 @@ func TestCover(t *testing.T) { }) Convey("When id is found", func() { mockMediaFileRepo.SetData(`[{"ID":"2","HasCoverArt":true,"Path":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) - err := cover.Get("2", 0, out) + err := cover.Get(context.TODO(), "2", 0, out) Convey("Then it should return the cover from the file", func() { So(err, ShouldBeNil) @@ -44,7 +45,7 @@ func TestCover(t *testing.T) { Convey("When there is an error accessing the database", func() { mockMediaFileRepo.SetData(`[{"ID":"2","HasCoverArt":true,"Path":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) mockMediaFileRepo.SetError(true) - err := cover.Get("2", 0, out) + err := cover.Get(context.TODO(), "2", 0, out) Convey("Then error should not be nil", func() { So(err, ShouldNotBeNil) @@ -52,7 +53,7 @@ func TestCover(t *testing.T) { }) Convey("When id is found but file is not present", func() { mockMediaFileRepo.SetData(`[{"ID":"2","HasCoverArt":true,"Path":"tests/fixtures/NOT_FOUND.mp3"}]`, 1) - err := cover.Get("2", 0, out) + err := cover.Get(context.TODO(), "2", 0, out) Convey("Then it should return DatNotFound error", func() { So(err, ShouldEqual, model.ErrNotFound) @@ -60,7 +61,7 @@ func TestCover(t *testing.T) { }) Convey("When specifying a size", func() { mockMediaFileRepo.SetData(`[{"ID":"2","HasCoverArt":true,"Path":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) - err := cover.Get("2", 100, out) + err := cover.Get(context.TODO(), "2", 100, out) Convey("Then image returned should be 100x100", func() { So(err, ShouldBeNil) @@ -73,7 +74,7 @@ func TestCover(t *testing.T) { }) Convey("When id is for an album", func() { mockAlbumRepo.SetData(`[{"ID":"1","CoverArtPath":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) - err := cover.Get("al-1", 0, out) + err := cover.Get(context.TODO(), "al-1", 0, out) Convey("Then it should return the cover for the album", func() { So(err, ShouldBeNil) diff --git a/engine/playlists.go b/engine/playlists.go index 14b2ec157..a5bcb33d2 100644 --- a/engine/playlists.go +++ b/engine/playlists.go @@ -9,7 +9,7 @@ import ( ) type Playlists interface { - GetAll() (model.Playlists, error) + GetAll(ctx context.Context) (model.Playlists, error) Get(ctx context.Context, id string) (*PlaylistInfo, error) Create(ctx context.Context, playlistId, name string, ids []string) error Delete(ctx context.Context, playlistId string) error @@ -103,7 +103,7 @@ func (p *playlists) Update(ctx context.Context, playlistId string, name *string, return p.ds.Playlist().Put(pls) } -func (p *playlists) GetAll() (model.Playlists, error) { +func (p *playlists) GetAll(ctx context.Context) (model.Playlists, error) { return p.ds.Playlist().GetAll(model.QueryOptions{}) } diff --git a/engine/users.go b/engine/users.go index 092630f07..2bb5fba80 100644 --- a/engine/users.go +++ b/engine/users.go @@ -1,6 +1,7 @@ package engine import ( + "context" "crypto/md5" "encoding/hex" "fmt" @@ -11,7 +12,7 @@ import ( ) type Users interface { - Authenticate(username, password, token, salt string) (*model.User, error) + Authenticate(ctx context.Context, username, password, token, salt string) (*model.User, error) } func NewUsers(ds model.DataStore) Users { @@ -22,7 +23,7 @@ type users struct { ds model.DataStore } -func (u *users) Authenticate(username, pass, token, salt string) (*model.User, error) { +func (u *users) Authenticate(ctx context.Context, username, pass, token, salt string) (*model.User, error) { user, err := u.ds.User().FindByUsername(username) if err == model.ErrNotFound { return nil, model.ErrInvalidAuth @@ -51,7 +52,7 @@ func (u *users) Authenticate(username, pass, token, salt string) (*model.User, e go func() { err := u.ds.User().UpdateLastAccessAt(user.ID) if err != nil { - log.Error("Could not update user's lastAccessAt", "user", user.UserName) + log.Error(ctx, "Could not update user's lastAccessAt", "user", user.UserName) } }() return user, nil diff --git a/engine/users_test.go b/engine/users_test.go index 4386d0a0e..53a9a3e7b 100644 --- a/engine/users_test.go +++ b/engine/users_test.go @@ -1,6 +1,8 @@ package engine import ( + "context" + "github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/persistence" . "github.com/onsi/ginkgo" @@ -17,20 +19,20 @@ var _ = Describe("Users", func() { Context("Plaintext password", func() { It("authenticates with plaintext password ", func() { - usr, err := users.Authenticate("admin", "wordpass", "", "") + usr, err := users.Authenticate(context.TODO(), "admin", "wordpass", "", "") Expect(err).NotTo(HaveOccurred()) Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) }) It("fails authentication with wrong password", func() { - _, err := users.Authenticate("admin", "INVALID", "", "") + _, err := users.Authenticate(context.TODO(), "admin", "INVALID", "", "") Expect(err).To(MatchError(model.ErrInvalidAuth)) }) }) Context("Encoded password", func() { It("authenticates with simple encoded password ", func() { - usr, err := users.Authenticate("admin", "enc:776f726470617373", "", "") + usr, err := users.Authenticate(context.TODO(), "admin", "enc:776f726470617373", "", "") Expect(err).NotTo(HaveOccurred()) Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) }) @@ -38,13 +40,13 @@ var _ = Describe("Users", func() { Context("Token based authentication", func() { It("authenticates with token based authentication", func() { - usr, err := users.Authenticate("admin", "", "23b342970e25c7928831c3317edd0b67", "retnlmjetrymazgkt") + usr, err := users.Authenticate(context.TODO(), "admin", "", "23b342970e25c7928831c3317edd0b67", "retnlmjetrymazgkt") Expect(err).NotTo(HaveOccurred()) Expect(usr).To(Equal(&model.User{UserName: "admin", Password: "wordpass"})) }) It("fails if salt is missing", func() { - _, err := users.Authenticate("admin", "", "23b342970e25c7928831c3317edd0b67", "") + _, err := users.Authenticate(context.TODO(), "admin", "", "23b342970e25c7928831c3317edd0b67", "") Expect(err).To(MatchError(model.ErrInvalidAuth)) }) }) diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index a191c5329..9d6237f9f 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -22,7 +22,7 @@ func NewBrowsingController(browser engine.Browser) *BrowsingController { } func (c *BrowsingController) GetMusicFolders(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - mediaFolderList, _ := c.browser.MediaFolders() + mediaFolderList, _ := c.browser.MediaFolders(r.Context()) folders := make([]responses.MusicFolder, len(mediaFolderList)) for i, f := range mediaFolderList { folders[i].Id = f.ID @@ -34,7 +34,7 @@ func (c *BrowsingController) GetMusicFolders(w http.ResponseWriter, r *http.Requ } func (c *BrowsingController) getArtistIndex(r *http.Request, ifModifiedSince time.Time) (*responses.Indexes, error) { - indexes, lastModified, err := c.browser.Indexes(ifModifiedSince) + indexes, lastModified, err := c.browser.Indexes(r.Context(), ifModifiedSince) if err != nil { log.Error(r, "Error retrieving Indexes", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error") @@ -152,7 +152,7 @@ func (c *BrowsingController) GetSong(w http.ResponseWriter, r *http.Request) (*r } func (c *BrowsingController) GetGenres(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - genres, err := c.browser.GetGenres() + genres, err := c.browser.GetGenres(r.Context()) if err != nil { log.Error(r, err) return nil, NewError(responses.ErrorGeneric, "Internal Error") diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index 45796674b..611adf9d0 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -39,7 +39,7 @@ func (c *MediaRetrievalController) GetCoverArt(w http.ResponseWriter, r *http.Re } size := ParamInt(r, "size", 0) - err = c.cover.Get(id, size, w) + err = c.cover.Get(r.Context(), id, size, w) switch { case err == model.ErrNotFound: diff --git a/server/subsonic/media_retrieval_test.go b/server/subsonic/media_retrieval_test.go index fad68437b..e4e0e36bf 100644 --- a/server/subsonic/media_retrieval_test.go +++ b/server/subsonic/media_retrieval_test.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "errors" "io" "net/http/httptest" @@ -17,7 +18,7 @@ type fakeCover struct { recvSize int } -func (c *fakeCover) Get(id string, size int, out io.Writer) error { +func (c *fakeCover) Get(ctx context.Context, id string, size int, out io.Writer) error { if c.err != nil { return c.err } diff --git a/server/subsonic/middlewares.go b/server/subsonic/middlewares.go index 82fc55f78..0f5f0ac16 100644 --- a/server/subsonic/middlewares.go +++ b/server/subsonic/middlewares.go @@ -50,7 +50,7 @@ func authenticate(users engine.Users) func(next http.Handler) http.Handler { token := ParamString(r, "t") salt := ParamString(r, "s") - usr, err := users.Authenticate(username, pass, token, salt) + usr, err := users.Authenticate(r.Context(), username, pass, token, salt) if err == model.ErrInvalidAuth { log.Warn(r, "Invalid login", "username", username, err) } else if err != nil { diff --git a/server/subsonic/middlewares_test.go b/server/subsonic/middlewares_test.go index 9c628a312..89ca9ee8c 100644 --- a/server/subsonic/middlewares_test.go +++ b/server/subsonic/middlewares_test.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "net/http" "net/http/httptest" "strings" @@ -113,7 +114,7 @@ type mockUsers struct { username, password, token, salt string } -func (m *mockUsers) Authenticate(username, password, token, salt string) (*model.User, error) { +func (m *mockUsers) Authenticate(ctx context.Context, username, password, token, salt string) (*model.User, error) { m.username = username m.password = password m.token = token diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index 39f050c80..7bd1477ea 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -20,7 +20,7 @@ func NewPlaylistsController(pls engine.Playlists) *PlaylistsController { } func (c *PlaylistsController) GetPlaylists(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { - allPls, err := c.pls.GetAll() + allPls, err := c.pls.GetAll(r.Context()) if err != nil { log.Error(r, err) return nil, NewError(responses.ErrorGeneric, "Internal error")