Files
bluebird/internal/app/maintenance.go
T

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
})
}