初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
@@ -0,0 +1,337 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user