v0.6.0: strengthen backup integrity
pipeline / test (push) Has been cancelled
pipeline / build (push) Has been cancelled

This commit is contained in:
Ein Anderssono
2026-06-15 00:34:32 +02:00
parent 0a905758cc
commit 05188e5451
13 changed files with 840 additions and 97 deletions
+44
View File
@@ -0,0 +1,44 @@
package manifest
import "testing"
func TestNewEntryPath(t *testing.T) {
e := newEntry("id1", "file.jpg", 123, "local")
if e.ID != "id1" || e.Filename != "file.jpg" || e.Path != "file.jpg" || e.Size != 123 || e.Cloud != "local" || e.Exported == 0 {
t.Fatalf("unexpected entry: %+v", e)
}
e = NewEntry("id2", "file2.jpg", "Album/file2.jpg", 456, "cloud")
if e.Path != "Album/file2.jpg" || e.Filename != "file2.jpg" || e.Size != 456 || e.Cloud != "cloud" {
t.Fatalf("unexpected entry with path: %+v", e)
}
}
func TestAddEntryDefaultsPath(t *testing.T) {
dir := t.TempDir()
jm := LoadJSONL(dir)
if err := jm.OpenAppend(); err != nil {
t.Fatal(err)
}
jm.AddEntry(Entry{ID: "x1", Filename: "file.jpg", Size: 3, Cloud: "local"})
jm.Close()
if got := LoadJSONL(dir).Entries()["x1"].Path; got != "file.jpg" {
t.Fatalf("jsonl path = %q", got)
}
sdir := t.TempDir()
sm, err := LoadSQLite(sdir)
if err != nil {
t.Fatal(err)
}
if err := sm.OpenAppend(); err != nil {
t.Fatal(err)
}
sm.AddEntry(Entry{ID: "x1", Filename: "file.jpg", Size: 3, Cloud: "local"})
if _, err := sm.db.Exec(`UPDATE downloads SET path = '' WHERE id = 'x1'`); err != nil {
t.Fatal(err)
}
if got := sm.Entries()["x1"].Path; got != "file.jpg" {
t.Fatalf("sqlite path = %q", got)
}
sm.Close()
}
+16 -3
View File
@@ -40,6 +40,7 @@ func LoadJSONL(dir string) *jsonlManifest {
type entryWithID struct {
ID string `json:"id"`
Filename string `json:"filename"`
Path string `json:"path,omitempty"`
Size int64 `json:"size"`
Cloud string `json:"cloud"`
Exported int64 `json:"exported"`
@@ -51,8 +52,13 @@ func LoadJSONL(dir string) *jsonlManifest {
}
var raw entryWithID
if json.Unmarshal([]byte(line), &raw) == nil && raw.ID != "" {
if raw.Path == "" {
raw.Path = raw.Filename
}
m.entries[raw.ID] = Entry{
ID: raw.ID,
Filename: raw.Filename,
Path: raw.Path,
Size: raw.Size,
Cloud: raw.Cloud,
Exported: raw.Exported,
@@ -70,18 +76,25 @@ func (m *jsonlManifest) Has(id string) bool {
}
func (m *jsonlManifest) Add(id string, filename string, size int64, cloud string) {
m.AddEntry(newEntry(id, filename, size, cloud))
}
func (m *jsonlManifest) AddEntry(entry Entry) {
m.mu.Lock()
defer m.mu.Unlock()
entry := newEntry(id, filename, size, cloud)
m.entries[id] = entry
if entry.Path == "" {
entry.Path = entry.Filename
}
m.entries[entry.ID] = entry
if m.file != nil {
data, _ := json.Marshal(struct {
ID string `json:"id"`
Filename string `json:"filename"`
Path string `json:"path,omitempty"`
Size int64 `json:"size"`
Cloud string `json:"cloud"`
Exported int64 `json:"exported"`
}{ID: id, Filename: entry.Filename, Size: entry.Size, Cloud: entry.Cloud, Exported: entry.Exported})
}{ID: entry.ID, Filename: entry.Filename, Path: entry.Path, Size: entry.Size, Cloud: entry.Cloud, Exported: entry.Exported})
m.file.Write(data)
m.file.Write([]byte("\n"))
}
+11
View File
@@ -5,6 +5,7 @@ import "time"
type Entry struct {
ID string
Filename string
Path string
Size int64
Cloud string
Exported int64
@@ -13,6 +14,7 @@ type Entry struct {
type Manifest interface {
Has(id string) bool
Add(id string, filename string, size int64, cloud string)
AddEntry(entry Entry)
Save() error
Close()
OpenAppend() error
@@ -26,8 +28,17 @@ func newEntry(id, filename string, size int64, cloud string) Entry {
return Entry{
ID: id,
Filename: filename,
Path: filename,
Size: size,
Cloud: cloud,
Exported: time.Now().Unix(),
}
}
func NewEntry(id, filename, path string, size int64, cloud string) Entry {
e := newEntry(id, filename, size, cloud)
if path != "" {
e.Path = path
}
return e
}
+4 -2
View File
@@ -49,7 +49,8 @@ func ConvertFromJSONL(dir string) (Manifest, error) {
}
for id, e := range src.Entries() {
dst.Add(id, e.Filename, e.Size, e.Cloud)
e.ID = id
dst.AddEntry(e)
}
os.Remove(JSONLPath(dir))
@@ -69,7 +70,8 @@ func ConvertFromSQLite(dir string) (Manifest, error) {
}
for id, e := range src.Entries() {
dst.Add(id, e.Filename, e.Size, e.Cloud)
e.ID = id
dst.AddEntry(e)
}
if err := dst.Save(); err != nil {
return nil, fmt.Errorf("save jsonl: %w", err)
+16 -5
View File
@@ -61,6 +61,7 @@ func (m *sqliteManifest) OpenAppend() error {
_, err = execFn(`CREATE TABLE IF NOT EXISTS downloads (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL DEFAULT '',
path TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
cloud TEXT NOT NULL DEFAULT '',
exported INTEGER NOT NULL DEFAULT 0
@@ -69,6 +70,7 @@ func (m *sqliteManifest) OpenAppend() error {
db.Close()
return fmt.Errorf("create table: %w", err)
}
_, _ = execFn(`ALTER TABLE downloads ADD COLUMN path TEXT NOT NULL DEFAULT ''`)
_, err = execFn(`CREATE INDEX IF NOT EXISTS idx_downloads_id ON downloads(id)`)
if err != nil {
db.Close()
@@ -91,12 +93,18 @@ func (m *sqliteManifest) Has(id string) bool {
}
func (m *sqliteManifest) Add(id string, filename string, size int64, cloud string) {
m.AddEntry(newEntry(id, filename, size, cloud))
}
func (m *sqliteManifest) AddEntry(entry Entry) {
if m.db == nil {
return
}
entry := newEntry(id, filename, size, cloud)
m.db.Exec(`INSERT OR REPLACE INTO downloads (id, filename, size, cloud, exported) VALUES (?, ?, ?, ?, ?)`,
id, entry.Filename, entry.Size, entry.Cloud, entry.Exported)
if entry.Path == "" {
entry.Path = entry.Filename
}
m.db.Exec(`INSERT OR REPLACE INTO downloads (id, filename, path, size, cloud, exported) VALUES (?, ?, ?, ?, ?, ?)`,
entry.ID, entry.Filename, entry.Path, entry.Size, entry.Cloud, entry.Exported)
}
func (m *sqliteManifest) Save() error {
@@ -119,14 +127,17 @@ func (m *sqliteManifest) Entries() map[string]Entry {
return nil
}
out := make(map[string]Entry)
rows, err := m.db.Query(`SELECT id, filename, size, cloud, exported FROM downloads`)
rows, err := m.db.Query(`SELECT id, filename, path, size, cloud, exported FROM downloads`)
if err != nil {
return out
}
defer rows.Close()
for rows.Next() {
var e Entry
if err := rows.Scan(&e.ID, &e.Filename, &e.Size, &e.Cloud, &e.Exported); err == nil {
if err := rows.Scan(&e.ID, &e.Filename, &e.Path, &e.Size, &e.Cloud, &e.Exported); err == nil {
if e.Path == "" {
e.Path = e.Filename
}
out[e.ID] = e
}
}
+2 -2
View File
@@ -249,10 +249,10 @@ func TestSQLiteCreateIndexError(t *testing.T) {
return nil, err
}
m.execFunc = func(query string, args ...any) (sql.Result, error) {
callCount++
if callCount == 2 {
if strings.Contains(query, "CREATE INDEX") {
return nil, fmt.Errorf("injected CREATE INDEX error")
}
callCount++
return db.Exec(query, args...)
}
return db, nil