package app import ( "encoding/hex" "errors" "io/fs" "os" "strings" ) func (a *App) recover() error { // No other process owns this data directory. Partial sessions are never published. // An interrupted attempt may have transferred bytes not yet recorded; charge its full allocation conservatively. if _, e := a.db.Exec("UPDATE files SET received=max(received,charged) WHERE status='pending' AND attempts>0 AND parcel IN(SELECT id FROM parcels WHERE state='uploading')"); e != nil { return e } if _, e := a.db.Exec("UPDATE parcels SET state='revoked',version=version+1 WHERE state='uploading'"); e != nil { return e } entries, e := fs.ReadDir(a.root.FS(), "tmp") if e != nil { return e } for _, entry := range entries { if entry.Type()&os.ModeSymlink != 0 { return errors.New("临时目录存在符号链接") } if e = a.root.RemoveAll("tmp/" + entry.Name()); e != nil { return e } } rows, e := a.db.Query("SELECT id FROM objects") if e != nil { return e } known := map[string]bool{} for rows.Next() { var id string if e = rows.Scan(&id); e != nil { rows.Close() return e } known[id] = true } e = rows.Err() rows.Close() if e != nil { return e } for id := range known { if len(id) != 32 { return errors.New("非法对象标识") } if _, e := hex.DecodeString(id); e != nil { return errors.New("非法对象标识") } st, e := a.root.Lstat(objectPath(id)) if e != nil || !st.Mode().IsRegular() { if e != nil && !errors.Is(e, os.ErrNotExist) { return e } if _, e = a.db.Exec("UPDATE files SET status='missing' WHERE object_id=?", id); e != nil { return e } } } e = fs.WalkDir(a.root.FS(), "objects", func(path string, d fs.DirEntry, e error) error { if e != nil { return e } if d.Type()&os.ModeSymlink != 0 { return errors.New("对象目录存在符号链接") } if d.IsDir() { return nil } if !known[d.Name()] { return a.root.Rename(path, "quarantine/"+randomID()) } return nil }) if e != nil { return e } return a.Cleanup() } func (a *App) settleUsage(parcel string) error { var ip, visitor, day string if e := a.db.QueryRow("SELECT ip,visitor,usage_day FROM parcels WHERE id=?", parcel).Scan(&ip, &visitor, &day); e != nil { return e } var refund int64 if e := a.db.QueryRow("SELECT COALESCE(sum(max(charged-received,0)),0) FROM files WHERE parcel=?", parcel).Scan(&refund); e != nil { return e } tx, e := a.db.Begin() if e != nil { return e } defer tx.Rollback() for _, s := range []string{"ip:" + a.keyed(ip), "visitor:" + visitor} { if _, e = tx.Exec("UPDATE usage SET bytes=max(0,bytes-?) WHERE subject=? AND day=?", refund, s, day); e != nil { return e } } if _, e = tx.Exec("UPDATE files SET charged=received WHERE parcel=?", parcel); e != nil { return e } return tx.Commit() } func (a *App) Cleanup() error { a.mu.Lock() defer a.mu.Unlock() now := a.now().Unix() rows, e := a.db.Query("SELECT id,state,upload_expires FROM parcels WHERE state='uploading' OR state IN('revoked','expired','exhausted')") if e != nil { return e } type parcel struct { id, state string expiry int64 } parcels := []parcel{} for rows.Next() { var p parcel if e = rows.Scan(&p.id, &p.state, &p.expiry); e != nil { rows.Close() return e } parcels = append(parcels, p) } e = rows.Err() rows.Close() if e != nil { return e } for _, p := range parcels { if p.state == "uploading" && p.expiry > now { continue } rows, e := a.db.Query("SELECT id FROM files WHERE parcel=?", p.id) if e != nil { return e } active := false for rows.Next() { var id string if e = rows.Scan(&id); e != nil { rows.Close() return e } if a.activeFiles[id] { active = true } } e = rows.Err() rows.Close() if e != nil { return e } if active { continue } if p.state == "uploading" { if _, e = a.db.Exec("UPDATE parcels SET state='expired',version=version+1 WHERE id=?", p.id); e != nil { return e } } if e = a.settleUsage(p.id); e != nil { return e } if _, e = a.db.Exec("UPDATE files SET status='deleting' WHERE parcel=?", p.id); e != nil { return e } } rows, e = a.db.Query("SELECT f.id,COALESCE(f.object_id,''),f.status,f.expires,f.remaining,p.state FROM files f JOIN parcels p ON p.id=f.parcel WHERE f.status!='deleted'") if e != nil { return e } type file struct { id, object, status, state string expiry int64 remaining int } files := []file{} for rows.Next() { var f file if e = rows.Scan(&f.id, &f.object, &f.status, &f.expiry, &f.remaining, &f.state); e != nil { rows.Close() return e } files = append(files, f) } e = rows.Err() rows.Close() if e != nil { return e } for _, f := range files { if a.activeFiles[f.id] || a.activeObjects[f.object] > 0 { continue } remove := f.status == "deleting" || f.status == "missing" || (f.state == "ready" && (f.remaining == 0 || f.expiry <= now)) if !remove { continue } if e = a.root.Remove("tmp/" + f.id); e != nil && !errors.Is(e, os.ErrNotExist) { return e } if _, e = a.db.Exec("UPDATE files SET status='deleted',reserved=0,object_id=NULL WHERE id=?", f.id); e != nil { return e } } // Objects remain charged until unlink succeeds. Sharing records never unlink each other's payloads. rows, e = a.db.Query("SELECT id FROM objects WHERE NOT EXISTS(SELECT 1 FROM files WHERE object_id=objects.id)") if e != nil { return e } ids := []string{} for rows.Next() { var id string if e = rows.Scan(&id); e != nil { rows.Close() return e } ids = append(ids, id) } e = rows.Err() rows.Close() if e != nil { return e } for _, id := range ids { if a.activeObjects[id] > 0 { continue } if e = a.root.Remove(objectPath(id)); e != nil && !errors.Is(e, os.ErrNotExist) { return e } if _, e = a.db.Exec("DELETE FROM objects WHERE id=?", id); e != nil { return e } } if _, e = a.db.Exec("UPDATE parcels SET state='exhausted',version=version+1 WHERE state='ready' AND NOT EXISTS(SELECT 1 FROM files WHERE parcel=parcels.id AND status='ready' AND remaining>0)"); e != nil { return e } for _, q := range []struct { sql string args []any }{ {"DELETE FROM visitors WHERE expires<=?", []any{now}}, {"DELETE FROM grants WHERE expires<=?", []any{now}}, {"DELETE FROM admin_sessions WHERE expires<=?", []any{now}}, {"DELETE FROM usage WHERE day 86400 { if e = a.root.Remove("quarantine/" + d.Name()); e != nil { return e } } } if e = a.scanOrphans(); e != nil { return e } a.extraDisk = 0 e = fs.WalkDir(a.root.FS(), "quarantine", func(path string, d fs.DirEntry, e error) error { if e != nil { return e } st, e := d.Info() if e == nil { a.extraDisk += allocated(st) } return e }) if e != nil { return e } _, e = a.db.Exec("PRAGMA wal_checkpoint(PASSIVE)") return e } func (a *App) InvalidateRestored() error { _, e := a.db.Exec("UPDATE parcels SET state='restored',code_hash=NULL,manage_hash=lower(hex(randomblob(32))),version=version+1; DELETE FROM grants; DELETE FROM admin_sessions; DELETE FROM visitors;") return e } func safeRelative(s string) bool { return s != "" && !strings.HasPrefix(s, "/") && !strings.Contains(s, "\\") && fs.ValidPath(s) } func (a *App) scanOrphans() error { rows, e := a.db.Query("SELECT id FROM objects") if e != nil { return e } known := map[string]bool{} for rows.Next() { var id string if e = rows.Scan(&id); e != nil { rows.Close() return e } known[id] = true } e = rows.Err() rows.Close() if e != nil { return e } return fs.WalkDir(a.root.FS(), "objects", func(path string, d fs.DirEntry, e error) error { if e != nil { return e } if d.Type()&os.ModeSymlink != 0 { return errors.New("对象目录存在符号链接") } if d.IsDir() { return nil } if !known[d.Name()] { return a.root.Rename(path, "quarantine/"+randomID()) } return nil }) }