e3bb8b19dd
Also improves migrations and updates the db schema docs. Signed-off-by: Michael Mayer <michael@photoprism.app>
234 lines
6.5 KiB
Go
234 lines
6.5 KiB
Go
package entity
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/ulule/deepcopier"
|
|
|
|
"github.com/photoprism/photoprism/internal/form"
|
|
"github.com/photoprism/photoprism/internal/remote"
|
|
"github.com/photoprism/photoprism/internal/remote/webdav"
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
)
|
|
|
|
const (
|
|
SyncStatusRefresh = "refresh"
|
|
SyncStatusDownload = "download"
|
|
SyncStatusUpload = "upload"
|
|
SyncStatusSynced = "synced"
|
|
)
|
|
|
|
type Services []Service
|
|
|
|
// Service represents a remote service, e.g. for uploading, downloading or syncing media files.
|
|
//
|
|
// Field Descriptions:
|
|
// - AccTimeout configures the timeout for requests, options: "", high, medium, low, none.
|
|
// - AccErrors holds the number of connection errors since the last reset.
|
|
// - AccShare enables manual upload, see SharePath, ShareSize, and ShareExpires.
|
|
// - AccSync enables automatic file synchronization, see SyncDownload and SyncUpload.
|
|
// - RetryLimit specifies the number of retry attempts, a negative value disables the limit.
|
|
type Service struct {
|
|
ID uint `gorm:"primary_key"`
|
|
AccName string `gorm:"type:VARCHAR(160);"`
|
|
AccOwner string `gorm:"type:VARCHAR(160);"`
|
|
AccURL string `gorm:"type:VARCHAR(255);"`
|
|
AccType string `gorm:"type:VARBINARY(255);"`
|
|
AccKey string `gorm:"type:VARBINARY(255);"`
|
|
AccUser string `gorm:"type:VARBINARY(255);"`
|
|
AccPass string `gorm:"type:VARBINARY(255);"`
|
|
AccTimeout string `gorm:"type:VARBINARY(16);"`
|
|
AccError string `gorm:"type:VARBINARY(512);"`
|
|
AccErrors int
|
|
AccShare bool
|
|
AccSync bool
|
|
RetryLimit int
|
|
SharePath string `gorm:"type:VARBINARY(1024);"`
|
|
ShareSize string `gorm:"type:VARBINARY(16);"`
|
|
ShareExpires int
|
|
SyncPath string `gorm:"type:VARBINARY(1024);"`
|
|
SyncStatus string `gorm:"type:VARBINARY(16);"`
|
|
SyncInterval int
|
|
SyncDate sql.NullTime `deepcopier:"skip"`
|
|
SyncUpload bool
|
|
SyncDownload bool
|
|
SyncFilenames bool
|
|
SyncRaw bool
|
|
CreatedAt time.Time `deepcopier:"skip"`
|
|
UpdatedAt time.Time `deepcopier:"skip"`
|
|
DeletedAt *time.Time `deepcopier:"skip" sql:"index"`
|
|
}
|
|
|
|
// TableName returns the entity table name.
|
|
func (Service) TableName() string {
|
|
return "services"
|
|
}
|
|
|
|
// AddService adds a new service account to the database.
|
|
func AddService(form form.Service) (model *Service, err error) {
|
|
model = &Service{
|
|
ShareSize: "",
|
|
ShareExpires: 0,
|
|
RetryLimit: 3,
|
|
AccTimeout: string(webdav.TimeoutDefault),
|
|
SyncStatus: SyncStatusRefresh,
|
|
}
|
|
|
|
err = model.SaveForm(form)
|
|
|
|
return model, err
|
|
}
|
|
|
|
// LogError updates the service error count and message.
|
|
func (m *Service) LogError(err error) error {
|
|
if err == nil {
|
|
return m.ResetErrors(true, true)
|
|
}
|
|
|
|
// Update error message and increase count.
|
|
m.AccError = err.Error()
|
|
m.AccErrors++
|
|
|
|
// Disable sharing when retry limit is reached.
|
|
if m.RetryLimit > 0 && m.AccErrors > m.RetryLimit {
|
|
m.AccShare = false
|
|
}
|
|
|
|
// Update fields in database.
|
|
return m.Updates(Service{AccError: m.AccError, AccErrors: m.AccErrors, AccShare: m.AccShare})
|
|
}
|
|
|
|
// ResetErrors resets the service and related file error messages and counters.
|
|
func (m *Service) ResetErrors(share, sync bool) error {
|
|
if !share && !sync || Db().NewRecord(m) {
|
|
return nil
|
|
}
|
|
|
|
if m.ID == 0 {
|
|
return fmt.Errorf("invalid id")
|
|
}
|
|
|
|
if share {
|
|
if err := Db().Model(FileShare{}).Where("service_id = ?", m.ID).Updates(Values{"error": "", "errors": 0}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if sync {
|
|
if err := Db().Model(FileSync{}).Where("service_id = ?", m.ID).Updates(Values{"error": "", "errors": 0}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
m.AccError = ""
|
|
m.AccErrors = 0
|
|
|
|
return m.Updates(Values{"acc_error": m.AccError, "acc_errors": m.AccErrors})
|
|
}
|
|
|
|
// SaveForm saves the entity using form data and stores it in the database.
|
|
func (m *Service) SaveForm(form form.Service) error {
|
|
db := Db()
|
|
|
|
// Copy model values from form.
|
|
if err := deepcopier.Copy(m).From(form); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Support for other remote services in addition to WebDAV.
|
|
if m.AccType != remote.ServiceWebDAV {
|
|
m.AccShare = false // Disable manual upload.
|
|
m.AccSync = false // Disable background sync.
|
|
}
|
|
|
|
// Prevent two-way sync, see https://github.com/photoprism/photoprism/issues/1785
|
|
if m.SyncUpload && m.SyncDownload {
|
|
m.SyncUpload = false
|
|
}
|
|
|
|
// Set default manual upload folder if empty.
|
|
if m.SharePath == "" {
|
|
m.SharePath = "/"
|
|
}
|
|
|
|
// Set default background sync folder if empty.
|
|
if m.SyncPath == "" {
|
|
m.SyncPath = "/"
|
|
}
|
|
|
|
// Number of remote request retry attempts.
|
|
if m.RetryLimit < -1 {
|
|
m.RetryLimit = -1 // Disabled.
|
|
} else if m.RetryLimit > 999 {
|
|
m.RetryLimit = 999 // 999 retries max.
|
|
}
|
|
|
|
// Refresh after performing changes.
|
|
if m.AccSync && m.SyncStatus == SyncStatusSynced {
|
|
m.SyncStatus = SyncStatusRefresh
|
|
}
|
|
|
|
// Reset error counters if account already exists.
|
|
if !Db().NewRecord(m) {
|
|
Log("service", "reset errors", m.ResetErrors(m.AccShare, m.AccSync))
|
|
}
|
|
|
|
// Ensure account name and owner are not too long.
|
|
m.AccName = txt.Clip(m.AccName, txt.ClipName)
|
|
m.AccOwner = txt.Clip(m.AccOwner, txt.ClipName)
|
|
|
|
// Save changes.
|
|
return db.Save(m).Error
|
|
}
|
|
|
|
// Delete deletes the entity from the database.
|
|
func (m *Service) Delete() error {
|
|
return Db().Delete(m).Error
|
|
}
|
|
|
|
// Directories returns a list of directories or albums in an account.
|
|
func (m *Service) Directories() (result fs.FileInfos, err error) {
|
|
if m.AccType == remote.ServiceWebDAV {
|
|
client := webdav.New(m.AccURL, m.AccUser, m.AccPass, webdav.Timeout(m.AccTimeout))
|
|
result, err = client.Directories("/", true, 0)
|
|
}
|
|
|
|
// Sort directory list.
|
|
sort.Sort(result)
|
|
|
|
// Update error count and message.
|
|
if err := m.LogError(err); err != nil {
|
|
log.Warnf("account: %s", err)
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
// Updates multiple columns in the database.
|
|
func (m *Service) Updates(values interface{}) error {
|
|
return UnscopedDb().Model(m).UpdateColumns(values).Error
|
|
}
|
|
|
|
// Update a column in the database.
|
|
func (m *Service) Update(attr string, value interface{}) error {
|
|
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
|
}
|
|
|
|
// Save updates the record in the database or inserts a new record if it does not already exist.
|
|
func (m *Service) Save() error {
|
|
return Db().Save(m).Error
|
|
}
|
|
|
|
// Create inserts a new row to the database.
|
|
func (m *Service) Create() error {
|
|
return Db().Create(m).Error
|
|
}
|
|
|
|
// ShareOriginals tests if the unmodified originals should be shared.
|
|
func (m *Service) ShareOriginals() bool {
|
|
return m.ShareSize == ""
|
|
}
|