338 lines
8.5 KiB
Go
338 lines
8.5 KiB
Go
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<?", []any{a.now().UTC().AddDate(0, 0, -8).Format("2006-01-02")}}, {"DELETE FROM downloads WHERE created<?", []any{now - 30*86400}}, {"DELETE FROM parcels WHERE state IN('revoked','expired','exhausted') AND created<? AND NOT EXISTS(SELECT 1 FROM files WHERE parcel=parcels.id AND status!='deleted')", []any{now - 30*86400}},
|
|
} {
|
|
if _, e = a.db.Exec(q.sql, q.args...); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
if e = a.trimAudit(); e != nil {
|
|
return e
|
|
}
|
|
entries, e := fs.ReadDir(a.root.FS(), "quarantine")
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for _, d := range entries {
|
|
st, e := d.Info()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if now-st.ModTime().Unix() > 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
|
|
})
|
|
}
|