mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-11 11:57:12 +02:00
The plugin manager clears last_error on every startup with an UPDATE that takes the SQLite write lock even when no row matches. On slow storage the startup scan often holds the lock at that moment, so the reset waited out the busy timeout and logged "database is locked", even with no plugins installed. ClearErrors now checks for errors with a read first and only writes when there is something to clear.
153 lines
4.3 KiB
Go
153 lines
4.3 KiB
Go
package persistence
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
. "github.com/Masterminds/squirrel"
|
|
"github.com/deluan/rest"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/pocketbase/dbx"
|
|
)
|
|
|
|
type pluginRepository struct {
|
|
sqlRepository
|
|
}
|
|
|
|
func NewPluginRepository(ctx context.Context, db dbx.Builder) model.PluginRepository {
|
|
r := &pluginRepository{}
|
|
r.ctx = ctx
|
|
r.db = db
|
|
r.registerModel(&model.Plugin{}, map[string]filterFunc{
|
|
"id": idFilter("plugin"),
|
|
"enabled": booleanFilter,
|
|
})
|
|
return r
|
|
}
|
|
|
|
func (r *pluginRepository) isPermitted() bool {
|
|
user := loggedUser(r.ctx)
|
|
return user.IsAdmin
|
|
}
|
|
|
|
func (r *pluginRepository) ClearErrors() error {
|
|
if !r.isPermitted() {
|
|
return rest.ErrPermissionDenied
|
|
}
|
|
// An UPDATE takes the write lock even when nothing matches, so only run it when there is an error to clear
|
|
var hasErrors bool
|
|
if err := r.db.NewQuery("SELECT EXISTS (SELECT 1 FROM plugin WHERE last_error != '')").Row(&hasErrors); err != nil {
|
|
return err
|
|
}
|
|
if !hasErrors {
|
|
return nil
|
|
}
|
|
_, err := r.db.NewQuery("UPDATE plugin SET last_error = '' WHERE last_error != ''").Execute()
|
|
return err
|
|
}
|
|
|
|
func (r *pluginRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
|
if !r.isPermitted() {
|
|
return 0, rest.ErrPermissionDenied
|
|
}
|
|
sql := r.newSelect()
|
|
return r.count(sql, options...)
|
|
}
|
|
|
|
func (r *pluginRepository) Delete(id string) error {
|
|
if !r.isPermitted() {
|
|
return rest.ErrPermissionDenied
|
|
}
|
|
return r.delete(Eq{"id": id})
|
|
}
|
|
|
|
func (r *pluginRepository) Get(id string) (*model.Plugin, error) {
|
|
if !r.isPermitted() {
|
|
return nil, rest.ErrPermissionDenied
|
|
}
|
|
sel := r.newSelect().Where(Eq{"id": id}).Columns("*")
|
|
res := model.Plugin{}
|
|
err := r.queryOne(sel, &res)
|
|
return &res, err
|
|
}
|
|
|
|
func (r *pluginRepository) GetAll(options ...model.QueryOptions) (model.Plugins, error) {
|
|
if !r.isPermitted() {
|
|
return nil, rest.ErrPermissionDenied
|
|
}
|
|
sel := r.newSelect(options...).Columns("*")
|
|
res := model.Plugins{}
|
|
err := r.queryAll(sel, &res)
|
|
return res, err
|
|
}
|
|
|
|
func (r *pluginRepository) Put(plugin *model.Plugin) error {
|
|
if !r.isPermitted() {
|
|
return rest.ErrPermissionDenied
|
|
}
|
|
|
|
plugin.UpdatedAt = time.Now()
|
|
|
|
if plugin.ID == "" {
|
|
return errors.New("plugin ID cannot be empty")
|
|
}
|
|
|
|
// Upsert using INSERT ... ON CONFLICT for atomic operation
|
|
_, err := r.db.NewQuery(`
|
|
INSERT INTO plugin (id, path, manifest, config, users, all_users, libraries, all_libraries, allow_write_access, enabled, last_error, sha256, created_at, updated_at)
|
|
VALUES ({:id}, {:path}, {:manifest}, {:config}, {:users}, {:all_users}, {:libraries}, {:all_libraries}, {:allow_write_access}, {:enabled}, {:last_error}, {:sha256}, {:created_at}, {:updated_at})
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
path = excluded.path,
|
|
manifest = excluded.manifest,
|
|
config = excluded.config,
|
|
users = excluded.users,
|
|
all_users = excluded.all_users,
|
|
libraries = excluded.libraries,
|
|
all_libraries = excluded.all_libraries,
|
|
allow_write_access = excluded.allow_write_access,
|
|
enabled = excluded.enabled,
|
|
last_error = excluded.last_error,
|
|
sha256 = excluded.sha256,
|
|
updated_at = excluded.updated_at
|
|
`).Bind(dbx.Params{
|
|
"id": plugin.ID,
|
|
"path": plugin.Path,
|
|
"manifest": plugin.Manifest,
|
|
"config": plugin.Config,
|
|
"users": plugin.Users,
|
|
"all_users": plugin.AllUsers,
|
|
"libraries": plugin.Libraries,
|
|
"all_libraries": plugin.AllLibraries,
|
|
"allow_write_access": plugin.AllowWriteAccess,
|
|
"enabled": plugin.Enabled,
|
|
"last_error": plugin.LastError,
|
|
"sha256": plugin.SHA256,
|
|
"created_at": time.Now(),
|
|
"updated_at": plugin.UpdatedAt,
|
|
}).Execute()
|
|
return err
|
|
}
|
|
|
|
func (r *pluginRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
|
return r.CountAll(r.parseRestOptions(r.ctx, options...))
|
|
}
|
|
|
|
func (r *pluginRepository) EntityName() string {
|
|
return "plugin"
|
|
}
|
|
|
|
func (r *pluginRepository) NewInstance() any {
|
|
return &model.Plugin{}
|
|
}
|
|
|
|
func (r *pluginRepository) Read(id string) (any, error) {
|
|
return r.Get(id)
|
|
}
|
|
|
|
func (r *pluginRepository) ReadAll(options ...rest.QueryOptions) (any, error) {
|
|
return r.GetAll(r.parseRestOptions(r.ctx, options...))
|
|
}
|
|
|
|
var _ model.PluginRepository = (*pluginRepository)(nil)
|
|
var _ rest.Repository = (*pluginRepository)(nil)
|