continuous-integration/drone Build is failing
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"linkport/internal/backup"
|
|
"linkport/internal/security"
|
|
"time"
|
|
)
|
|
|
|
type WebDAVSyncSettings struct {
|
|
backup.WebDAVConfig
|
|
Automatic bool `json:"automatic"`
|
|
IntervalMinutes int `json:"intervalMinutes"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func (c WebDAVSyncSettings) Validate() error {
|
|
if err := c.WebDAVConfig.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if c.IntervalMinutes < 1 || c.IntervalMinutes > 10080 {
|
|
return errors.New("sync interval must be between 1 and 10080 minutes")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type WebDAVSyncState struct {
|
|
DeviceID string `json:"deviceId"`
|
|
LastAttempt string `json:"lastAttempt"`
|
|
LastSuccess string `json:"lastSuccess"`
|
|
LastEndpoint string `json:"lastEndpoint"`
|
|
LastKey string `json:"lastKey"`
|
|
LastError string `json:"lastError"`
|
|
BaseHash string `json:"-"`
|
|
Conflict bool `json:"conflict"`
|
|
LocalHash string `json:"localHash"`
|
|
RemoteHash string `json:"remoteHash"`
|
|
Action string `json:"action"`
|
|
Running bool `json:"running"`
|
|
}
|
|
|
|
func (s *Store) initWebDAVSync() error {
|
|
_, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS webdav_sync_settings(id INTEGER PRIMARY KEY CHECK(id=1),config BLOB NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS webdav_sync_state(
|
|
id INTEGER PRIMARY KEY CHECK(id=1), device_id TEXT NOT NULL,
|
|
last_attempt TEXT NOT NULL DEFAULT '', last_success TEXT NOT NULL DEFAULT '',
|
|
last_endpoint TEXT NOT NULL DEFAULT '', last_key TEXT NOT NULL DEFAULT '', last_error TEXT NOT NULL DEFAULT '',
|
|
base_hash TEXT NOT NULL DEFAULT '', conflict INTEGER NOT NULL DEFAULT 0, local_hash TEXT NOT NULL DEFAULT '', remote_hash TEXT NOT NULL DEFAULT '', action TEXT NOT NULL DEFAULT '',
|
|
lease_until INTEGER NOT NULL DEFAULT 0, lease_id TEXT NOT NULL DEFAULT '');`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.db.Exec("INSERT INTO webdav_sync_state(id,device_id) VALUES(1,?) ON CONFLICT(id) DO NOTHING", security.Token())
|
|
return err
|
|
}
|
|
|
|
func (s *Store) WebDAVSyncSettings() (WebDAVSyncSettings, error) {
|
|
c := WebDAVSyncSettings{WebDAVConfig: backup.WebDAVConfig{Prefix: "LinkPort/"}, IntervalMinutes: 5}
|
|
var raw []byte
|
|
err := s.db.QueryRow("SELECT config FROM webdav_sync_settings WHERE id=1").Scan(&raw)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return c, nil
|
|
}
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
raw, err = s.vault.Open(raw, "webdav-sync-settings")
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
err = json.Unmarshal(raw, &c)
|
|
return c, err
|
|
}
|
|
|
|
func (s *Store) SaveWebDAVSyncSettings(c WebDAVSyncSettings) error {
|
|
if c.Prefix == "" {
|
|
c.Prefix = "LinkPort/"
|
|
}
|
|
if err := c.Validate(); err != nil {
|
|
return err
|
|
}
|
|
old, err := s.WebDAVSyncSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changed := old.Endpoint != c.Endpoint || old.Prefix != c.Prefix || old.Username != c.Username
|
|
c.UpdatedAt = time.Now().UTC()
|
|
|
|
raw, err := json.Marshal(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err = s.vault.Seal(raw, "webdav-sync-settings")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var leaseUntil int64
|
|
if err = tx.QueryRow("SELECT lease_until FROM webdav_sync_state WHERE id=1").Scan(&leaseUntil); err != nil {
|
|
return err
|
|
}
|
|
if leaseUntil > time.Now().Unix() {
|
|
return errors.New("wait for the current sync before changing settings")
|
|
}
|
|
_, err = tx.Exec("INSERT INTO webdav_sync_settings(id,config) VALUES(1,?) ON CONFLICT(id) DO UPDATE SET config=excluded.config", raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed {
|
|
if _, err = tx.Exec("UPDATE webdav_sync_state SET base_hash='',conflict=0,local_hash='',remote_hash='',action='',last_error='',last_success='',last_attempt='',last_endpoint='',last_key='' WHERE id=1"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *Store) WebDAVSyncState() (WebDAVSyncState, error) {
|
|
var state WebDAVSyncState
|
|
var leaseUntil int64
|
|
err := s.db.QueryRow("SELECT device_id,last_attempt,last_success,last_endpoint,last_key,last_error,lease_until,base_hash,conflict,local_hash,remote_hash,action FROM webdav_sync_state WHERE id=1").Scan(
|
|
&state.DeviceID, &state.LastAttempt, &state.LastSuccess, &state.LastEndpoint, &state.LastKey, &state.LastError, &leaseUntil, &state.BaseHash, &state.Conflict, &state.LocalHash, &state.RemoteHash, &state.Action)
|
|
state.Running = leaseUntil > time.Now().Unix()
|
|
return state, err
|
|
}
|
|
|
|
// ClaimWebDAVSync also excludes another process using the same configuration DB.
|
|
// An abandoned lease expires after five minutes; uploads have a 30 second limit.
|
|
func (s *Store) ClaimWebDAVSync(now time.Time, expectedLastAttempt string) (string, bool, error) {
|
|
lease := security.Token()
|
|
res, err := s.db.Exec("UPDATE webdav_sync_state SET lease_id=?,lease_until=?,last_attempt=?,last_error='' WHERE id=1 AND lease_until<=? AND last_attempt=?", lease, now.Add(5*time.Minute).Unix(), now.UTC().Format(time.RFC3339Nano), now.Unix(), expectedLastAttempt)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
n, err := res.RowsAffected()
|
|
return lease, n == 1, err
|
|
}
|
|
|
|
func (s *Store) FinishWebDAVSync(lease, endpoint, key, action, message string) error {
|
|
var res sql.Result
|
|
var err error
|
|
if message == "" {
|
|
res, err = s.db.Exec("UPDATE webdav_sync_state SET lease_until=0,lease_id='',last_error='',last_success=?,last_endpoint=?,last_key=?,action=CASE WHEN ?='backed_up' THEN ? ELSE action END WHERE id=1 AND lease_id=?", time.Now().UTC().Format(time.RFC3339Nano), endpoint, key, action, action, lease)
|
|
} else {
|
|
res, err = s.db.Exec("UPDATE webdav_sync_state SET lease_until=0,lease_id='',last_error=? WHERE id=1 AND lease_id=?", message, lease)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err := res.RowsAffected()
|
|
if err == nil && n != 1 {
|
|
return errors.New("sync lease expired")
|
|
}
|
|
return err
|
|
}
|