679 lines
18 KiB
Go
679 lines
18 KiB
Go
package app
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"errors"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
"unicode"
|
||
|
||
"filippo.io/age"
|
||
"golang.org/x/sys/unix"
|
||
)
|
||
|
||
type ManifestFile struct {
|
||
Name string `json:"name"`
|
||
Size int64 `json:"size"`
|
||
}
|
||
type Manifest struct {
|
||
Files []ManifestFile `json:"files"`
|
||
Days int `json:"days"`
|
||
Downloads int `json:"downloads"`
|
||
Password string `json:"password"`
|
||
Renew bool `json:"renew"`
|
||
}
|
||
|
||
func reserveSize(n int64, encrypted bool) int64 {
|
||
if encrypted {
|
||
n += 1024 + ((n/65536)+1)*16
|
||
}
|
||
return ((n+4095)/4096)*4096 + 4096
|
||
}
|
||
func cleanName(s string) string {
|
||
s = strings.ReplaceAll(s, "\\", "/")
|
||
s = filepath.Base(s)
|
||
s = strings.Map(func(r rune) rune {
|
||
if unicode.IsControl(r) {
|
||
return -1
|
||
}
|
||
return r
|
||
}, s)
|
||
return strings.TrimSpace(s)
|
||
}
|
||
func objectPath(id string) string { return "objects/" + id[:2] + "/" + id }
|
||
func (a *App) capacity() (used, reserved, metadata int64, err error) {
|
||
err = a.db.QueryRow("SELECT COALESCE(sum(stored_bytes),0) FROM objects").Scan(&used)
|
||
if err != nil {
|
||
return
|
||
}
|
||
err = a.db.QueryRow("SELECT COALESCE(sum(reserved),0) FROM files WHERE object_id IS NULL AND reserved>0 AND status!='deleted'").Scan(&reserved)
|
||
if err != nil {
|
||
return
|
||
}
|
||
for _, p := range []string{"db/bluebird.db", "db/bluebird.db-wal", "db/bluebird.db-shm"} {
|
||
s, e := a.root.Stat(p)
|
||
if e == nil {
|
||
metadata += allocated(s)
|
||
} else if !errors.Is(e, os.ErrNotExist) {
|
||
err = e
|
||
return
|
||
}
|
||
}
|
||
metadata += a.extraDisk
|
||
return
|
||
}
|
||
func allocated(s os.FileInfo) int64 {
|
||
if st, ok := s.Sys().(*syscall.Stat_t); ok {
|
||
return max(st.Blocks*512, s.Size())
|
||
}
|
||
if st, ok := s.Sys().(*unix.Stat_t); ok {
|
||
return max(st.Blocks*512, s.Size())
|
||
}
|
||
return ((s.Size() + 4095) / 4096) * 4096
|
||
}
|
||
func (a *App) createUpload(w http.ResponseWriter, r *http.Request) {
|
||
if !a.allow(r, "upload", 20) {
|
||
fail(w, 429, "上传请求过于频繁")
|
||
return
|
||
}
|
||
var m Manifest
|
||
if !decode(w, r, &m) {
|
||
return
|
||
}
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
p, e := a.policy()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if !p.AnonymousUpload {
|
||
fail(w, 403, "管理员已关闭上传")
|
||
return
|
||
}
|
||
if m.Days == 0 {
|
||
m.Days = p.DefaultDays
|
||
}
|
||
if m.Downloads == 0 {
|
||
m.Downloads = p.DefaultDownloads
|
||
}
|
||
if m.Days < 1 || m.Days > p.MaxInitialDays || m.Downloads < 1 || m.Downloads > p.MaxDownloads || len(m.Files) < 1 || len(m.Files) > p.MaxFiles {
|
||
fail(w, 400, "文件数量、期限或下载次数超出策略")
|
||
return
|
||
}
|
||
if p.RequirePassword && m.Password == "" {
|
||
fail(w, 400, "当前策略要求取件密码")
|
||
return
|
||
}
|
||
hash := ""
|
||
if m.Password != "" {
|
||
if len(m.Password) < 8 || len(m.Password) > 128 {
|
||
fail(w, 400, "取件密码需为 8–128 字节")
|
||
return
|
||
}
|
||
select {
|
||
case a.passwordSlots <- struct{}{}:
|
||
default:
|
||
fail(w, 429, "密码验证繁忙")
|
||
return
|
||
}
|
||
hash, e = hashPassword(m.Password)
|
||
<-a.passwordSlots
|
||
if e != nil {
|
||
fail(w, 400, e.Error())
|
||
return
|
||
}
|
||
}
|
||
var total, reservation int64
|
||
for i, f := range m.Files {
|
||
m.Files[i].Name = cleanName(f.Name)
|
||
if m.Files[i].Name == "" || m.Files[i].Name == "." || m.Files[i].Name == ".." || len(m.Files[i].Name) > 240 || f.Size < 0 || f.Size > p.MaxFileBytes || total > p.MaxBatchBytes-f.Size {
|
||
fail(w, 400, "文件名、文件大小或批次总大小无效")
|
||
return
|
||
}
|
||
total += f.Size
|
||
reservation += reserveSize(f.Size, a.cfg.Encryption)
|
||
}
|
||
if total > p.MaxBatchBytes {
|
||
fail(w, 400, "批次总大小超出限制")
|
||
return
|
||
}
|
||
var count int
|
||
if e = a.db.QueryRow("SELECT count(*) FROM files").Scan(&count); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if count+len(m.Files) > a.cfg.MaxRecords {
|
||
fail(w, 507, "文件记录已满")
|
||
return
|
||
}
|
||
used, reserved, meta, e := a.capacity()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
var disk unix.Statfs_t
|
||
if e = unix.Statfs(a.cfg.DataDir, &disk); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
free := int64(disk.Bavail) * int64(disk.Bsize)
|
||
limits, e := a.runtimeSettings()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if used+reserved+reservation > limits.FileBudget || used+reserved+reservation+meta > limits.DataBudget || free-reserved-reservation < limits.MinFreeBytes || (disk.Files > 0 && disk.Ffree < uint64(len(m.Files)+32)) {
|
||
fail(w, 507, "可用存储不足,暂时无法接收文件")
|
||
return
|
||
}
|
||
visitor, e := a.visitor(w, r)
|
||
if e != nil {
|
||
fail(w, 429, "匿名会话暂不可用")
|
||
return
|
||
}
|
||
ip := a.remoteIP(r)
|
||
day := a.now().UTC().Format("2006-01-02")
|
||
subjects := []string{"ip:" + a.keyed(ip), "visitor:" + visitor}
|
||
tx, e := a.db.Begin()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
defer tx.Rollback()
|
||
for i, subject := range subjects {
|
||
if _, e = tx.Exec("INSERT OR IGNORE INTO usage VALUES(?,?,0,0)", subject, day); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
var b int64
|
||
var n int
|
||
if e = tx.QueryRow("SELECT bytes,uploads FROM usage WHERE subject=? AND day=?", subject, day).Scan(&b, &n); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
limit := p.IPDailyUploads
|
||
if i == 1 {
|
||
limit = p.SessionDailyUploads
|
||
}
|
||
if n >= limit || (i == 0 && b > p.IPDailyBytes-total) {
|
||
fail(w, 429, "今日上传次数或流量额度已用完")
|
||
return
|
||
}
|
||
if _, e = tx.Exec("UPDATE usage SET bytes=bytes+?,uploads=uploads+1 WHERE subject=? AND day=?", total, subject, day); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
}
|
||
id, manage := randomID(), randomToken()
|
||
now := a.now().Unix()
|
||
renewal := 0
|
||
if m.Renew {
|
||
renewal = p.RenewalDays
|
||
}
|
||
_, e = tx.Exec("INSERT INTO parcels(id,visitor,ip,state,created,password_hash,manage_hash,days,downloads,renewal_days,renewal_mode,lifetime_days,upload_expires,usage_day) VALUES(?,?,?,'uploading',?,?,?,?,?,?,?,?,?,?)", id, visitor, ip, now, hash, tokenHash(manage), m.Days, m.Downloads, renewal, p.RenewalMode, p.MaxLifetimeDays, now+3600, day)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
out := []map[string]any{}
|
||
for _, f := range m.Files {
|
||
fid := randomID()
|
||
if _, e = tx.Exec("INSERT INTO files(id,parcel,name,size,status,reserved,remaining,charged) VALUES(?,?,?,?,'pending',?,?,?)", fid, id, f.Name, f.Size, reserveSize(f.Size, a.cfg.Encryption), m.Downloads, f.Size); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
out = append(out, map[string]any{"id": fid, "name": f.Name, "size": f.Size})
|
||
}
|
||
if e = tx.Commit(); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
a.audit("upload.create", id, ip)
|
||
respond(w, 201, map[string]any{"id": id, "manage_token": manage, "files": out})
|
||
}
|
||
func (a *App) owner(r *http.Request, id string) bool {
|
||
var owner, manage string
|
||
if e := a.db.QueryRow("SELECT visitor,manage_hash FROM parcels WHERE id=?", id).Scan(&owner, &manage); e != nil {
|
||
return false
|
||
}
|
||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||
if token != "" && tokenHash(token) == manage {
|
||
return true
|
||
}
|
||
v := a.currentVisitor(r)
|
||
return v != "" && v == owner
|
||
}
|
||
func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) {
|
||
id, fid := r.PathValue("parcel"), r.PathValue("file")
|
||
ip := a.remoteIP(r)
|
||
if !a.allow(r, "file-transfer", 120) {
|
||
fail(w, 429, "文件传输请求过于频繁")
|
||
return
|
||
}
|
||
if !a.acquire("upload", ip) {
|
||
fail(w, 429, "上传并发已满,请稍后重试")
|
||
return
|
||
}
|
||
defer a.release("upload", ip)
|
||
a.deadlines(w, r)
|
||
a.mu.Lock()
|
||
if !a.owner(r, id) {
|
||
a.mu.Unlock()
|
||
fail(w, 404, "上传会话不存在")
|
||
return
|
||
}
|
||
var state, status string
|
||
var expected, reserved, expiry int64
|
||
e := a.db.QueryRow("SELECT p.state,f.status,f.size,f.reserved,p.upload_expires FROM files f JOIN parcels p ON p.id=f.parcel WHERE f.id=? AND p.id=?", fid, id).Scan(&state, &status, &expected, &reserved, &expiry)
|
||
if e != nil || state != "uploading" || expiry <= a.now().Unix() {
|
||
a.mu.Unlock()
|
||
fail(w, 409, "上传会话已失效")
|
||
return
|
||
}
|
||
if status == "ready" {
|
||
a.mu.Unlock()
|
||
respond(w, 200, map[string]bool{"ready": true})
|
||
return
|
||
}
|
||
if a.activeFiles[fid] {
|
||
a.mu.Unlock()
|
||
fail(w, 409, "该文件正在上传")
|
||
return
|
||
}
|
||
var attempts int
|
||
var sessionIP, visitor, day string
|
||
if e = a.db.QueryRow("SELECT f.attempts,p.ip,p.visitor,p.usage_day FROM files f JOIN parcels p ON p.id=f.parcel WHERE f.id=?", fid).Scan(&attempts, &sessionIP, &visitor, &day); e != nil {
|
||
a.mu.Unlock()
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
policy, pe := a.policy()
|
||
if pe != nil {
|
||
a.mu.Unlock()
|
||
a.serverError(w, pe)
|
||
return
|
||
}
|
||
if !policy.AnonymousUpload {
|
||
a.mu.Unlock()
|
||
fail(w, 403, "上传已关闭")
|
||
return
|
||
}
|
||
tx, e := a.db.Begin()
|
||
if e != nil {
|
||
a.mu.Unlock()
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if attempts > 0 {
|
||
var charged int64
|
||
e = tx.QueryRow("SELECT bytes FROM usage WHERE subject=? AND day=?", "ip:"+a.keyed(sessionIP), day).Scan(&charged)
|
||
if e == nil && charged > policy.IPDailyBytes-expected {
|
||
tx.Rollback()
|
||
a.mu.Unlock()
|
||
fail(w, 429, "重试上传超过日流量限制")
|
||
return
|
||
}
|
||
if e == nil {
|
||
for _, subject := range []string{"ip:" + a.keyed(sessionIP), "visitor:" + visitor} {
|
||
_, e = tx.Exec("UPDATE usage SET bytes=bytes+? WHERE subject=? AND day=?", expected, subject, day)
|
||
if e != nil {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if e == nil {
|
||
_, e = tx.Exec("UPDATE files SET charged=charged+? WHERE id=?", expected, fid)
|
||
}
|
||
}
|
||
if e == nil {
|
||
_, e = tx.Exec("UPDATE files SET attempts=attempts+1 WHERE id=?", fid)
|
||
}
|
||
if e == nil {
|
||
e = tx.Commit()
|
||
} else {
|
||
tx.Rollback()
|
||
}
|
||
if e != nil {
|
||
a.mu.Unlock()
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
a.activeFiles[fid] = true
|
||
a.mu.Unlock()
|
||
defer func() { a.mu.Lock(); delete(a.activeFiles, fid); a.mu.Unlock() }()
|
||
tmp := "tmp/" + fid
|
||
_ = a.root.Remove(tmp)
|
||
f, e := a.root.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
defer a.root.Remove(tmp)
|
||
var dst io.Writer = f
|
||
var enc io.WriteCloser
|
||
if a.cfg.Encryption {
|
||
enc, e = age.Encrypt(f, a.identity.Recipient())
|
||
if e != nil {
|
||
f.Close()
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
dst = enc
|
||
}
|
||
h := sha256.New()
|
||
r.Body = http.MaxBytesReader(w, r.Body, expected)
|
||
totalDeadline := time.Now().Add(time.Duration(a.cfg.MaxTransferSeconds) * time.Second)
|
||
if sessionEnd := time.Unix(expiry, 0); sessionEnd.Before(totalDeadline) {
|
||
totalDeadline = sessionEnd
|
||
}
|
||
src := &idleReader{reader: r.Body, controller: http.NewResponseController(w), total: totalDeadline, idle: time.Duration(a.cfg.TransferIdleSeconds) * time.Second}
|
||
received, e := io.CopyBuffer(io.MultiWriter(dst, h), src, make([]byte, 128<<10))
|
||
if e == nil && received != expected {
|
||
e = errors.New("实际大小与声明不一致")
|
||
}
|
||
if e == nil && enc != nil {
|
||
e = enc.Close()
|
||
}
|
||
if e == nil {
|
||
e = f.Sync()
|
||
}
|
||
st, se := f.Stat()
|
||
ce := f.Close()
|
||
if e == nil {
|
||
e = ce
|
||
}
|
||
if e == nil {
|
||
e = se
|
||
}
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
if _, ue := a.db.Exec("UPDATE files SET received=received+? WHERE id=?", received, fid); ue != nil {
|
||
a.serverError(w, ue)
|
||
return
|
||
}
|
||
if e != nil {
|
||
fail(w, 400, "文件接收失败或大小与声明不一致,请重试")
|
||
return
|
||
}
|
||
stored := allocated(st)
|
||
if stored > reserved {
|
||
fail(w, 507, "文件实际落盘开销超出预留")
|
||
return
|
||
}
|
||
if e = a.db.QueryRow("SELECT state FROM parcels WHERE id=?", id).Scan(&state); e != nil || state != "uploading" || expiry <= a.now().Unix() {
|
||
fail(w, 409, "上传已取消")
|
||
return
|
||
}
|
||
keyID := "plain"
|
||
if a.cfg.Encryption {
|
||
keyID = tokenHash(a.identity.Recipient().String())
|
||
}
|
||
digest := a.keyed(hex.EncodeToString(h.Sum(nil)) + "/" + keyID)
|
||
if !a.cfg.Deduplication {
|
||
digest += "/" + randomID()
|
||
}
|
||
var oid string
|
||
e = a.db.QueryRow("SELECT id FROM objects WHERE digest=? AND size=?", digest, expected).Scan(&oid)
|
||
if e != nil && !errors.Is(e, sql.ErrNoRows) {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
newObject := oid == ""
|
||
committed := false
|
||
defer func() {
|
||
if newObject && oid != "" && !committed {
|
||
if de := a.root.Remove(objectPath(oid)); de != nil && !errors.Is(de, os.ErrNotExist) {
|
||
a.extraDisk += stored
|
||
a.log.Error("orphan removal failed", "error", de)
|
||
}
|
||
}
|
||
}()
|
||
if newObject {
|
||
oid = randomID()
|
||
path := objectPath(oid)
|
||
if e = a.root.MkdirAll(filepath.Dir(path), 0700); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if e = a.root.Rename(tmp, path); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if e = syncDir(a.root, filepath.Dir(path)); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if e = syncDir(a.root, "tmp"); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
} else {
|
||
if _, e = a.root.Stat(objectPath(oid)); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if e = a.root.Remove(tmp); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
}
|
||
tx, e = a.db.Begin()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
defer tx.Rollback()
|
||
if newObject {
|
||
_, e = tx.Exec("INSERT INTO objects VALUES(?,?,?,?,?,?,?)", oid, digest, expected, stored, a.cfg.Encryption, keyID, a.now().Unix())
|
||
if e != nil {
|
||
_ = a.root.Remove(objectPath(oid))
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
}
|
||
var attached sql.Result
|
||
attached, e = tx.Exec("UPDATE files SET object_id=?,status='ready',reserved=0 WHERE id=? AND status='pending'", oid, fid)
|
||
if e == nil {
|
||
n, _ := attached.RowsAffected()
|
||
if n != 1 {
|
||
e = errors.New("文件已被管理员删除")
|
||
}
|
||
}
|
||
if e == nil {
|
||
e = tx.Commit()
|
||
}
|
||
if e != nil {
|
||
if newObject {
|
||
_ = a.root.Remove(objectPath(oid))
|
||
}
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
committed = true
|
||
respond(w, 200, map[string]bool{"ready": true})
|
||
}
|
||
func (a *App) publish(w http.ResponseWriter, r *http.Request) {
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
id := r.PathValue("parcel")
|
||
if !a.owner(r, id) {
|
||
fail(w, 404, "包裹不存在")
|
||
return
|
||
}
|
||
var state string
|
||
var days, lifetime int
|
||
var uploadExpiry int64
|
||
e := a.db.QueryRow("SELECT state,days,lifetime_days,upload_expires FROM parcels WHERE id=?", id).Scan(&state, &days, &lifetime, &uploadExpiry)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if state == "ready" {
|
||
respond(w, 200, map[string]any{"published": true, "message": "已发布;如丢失取件码,请重新生成"})
|
||
return
|
||
}
|
||
policy, pe := a.policy()
|
||
if pe != nil {
|
||
a.serverError(w, pe)
|
||
return
|
||
}
|
||
if !policy.AnonymousUpload {
|
||
fail(w, 403, "上传已关闭")
|
||
return
|
||
}
|
||
if state != "uploading" || uploadExpiry <= a.now().Unix() {
|
||
fail(w, 409, "上传会话已失效")
|
||
return
|
||
}
|
||
var pending int
|
||
e = a.db.QueryRow("SELECT count(*) FROM files WHERE parcel=? AND status!='ready'", id).Scan(&pending)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if pending > 0 {
|
||
fail(w, 409, "尚有文件未上传完成")
|
||
return
|
||
}
|
||
now := a.now().Unix()
|
||
expires := now + int64(days)*86400
|
||
hard := now + int64(lifetime)*86400
|
||
code := pickupCode()
|
||
tx, e := a.db.Begin()
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
defer tx.Rollback()
|
||
_, e = tx.Exec("UPDATE parcels SET state='ready',created=?,expires=?,hard_expires=?,code_hash=? WHERE id=?", now, expires, hard, a.keyed(code), id)
|
||
if e == nil {
|
||
_, e = tx.Exec("UPDATE files SET expires=?,hard_expires=? WHERE parcel=?", expires, hard, id)
|
||
}
|
||
if e == nil {
|
||
e = tx.Commit()
|
||
}
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
if e = a.settleUsage(id); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
a.audit("parcel.publish", id, a.remoteIP(r))
|
||
respond(w, 200, a.shareResult(r, code, expires))
|
||
}
|
||
func (a *App) shareResult(r *http.Request, code string, expires int64) map[string]any {
|
||
links := []map[string]string{}
|
||
for _, base := range a.cfg.accessOrigins() {
|
||
label := "公网HTTP"
|
||
if strings.HasPrefix(base, "https://") {
|
||
label = "公网HTTPS"
|
||
}
|
||
u, _ := url.Parse(base)
|
||
ip := net.ParseIP(u.Hostname())
|
||
if ip != nil && (ip.IsPrivate() || ip.IsLoopback()) {
|
||
label = "内网入口"
|
||
}
|
||
links = append(links, map[string]string{"label": label + " · " + base, "url": base + "/#/pickup/" + code})
|
||
}
|
||
return map[string]any{"code": code, "url": a.accessFor(r).base + "/#/pickup/" + code, "urls": links, "expires": expires}
|
||
}
|
||
func (a *App) regenerate(w http.ResponseWriter, r *http.Request) {
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
id := r.PathValue("parcel")
|
||
if !a.owner(r, id) {
|
||
fail(w, 404, "包裹不存在")
|
||
return
|
||
}
|
||
var expiry int64
|
||
if e := a.db.QueryRow("SELECT expires FROM parcels WHERE id=? AND state='ready' AND EXISTS(SELECT 1 FROM files WHERE parcel=parcels.id AND status='ready' AND remaining>0 AND expires>?)", id, a.now().Unix()).Scan(&expiry); e != nil || expiry <= a.now().Unix() {
|
||
fail(w, 409, "包裹不可取件")
|
||
return
|
||
}
|
||
code := pickupCode()
|
||
if _, e := a.db.Exec("UPDATE parcels SET code_hash=?,version=version+1 WHERE id=?", a.keyed(code), id); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
respond(w, 200, a.shareResult(r, code, expiry))
|
||
}
|
||
func (a *App) revoke(w http.ResponseWriter, r *http.Request) {
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
id := r.PathValue("parcel")
|
||
if !a.owner(r, id) {
|
||
fail(w, 404, "包裹不存在")
|
||
return
|
||
}
|
||
if _, e := a.db.Exec("UPDATE parcels SET state='revoked',version=version+1 WHERE id=?", id); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
a.audit("parcel.revoke", id, a.remoteIP(r))
|
||
respond(w, 200, map[string]bool{"revoked": true})
|
||
}
|
||
func (a *App) uploadStatus(w http.ResponseWriter, r *http.Request) {
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
id := r.PathValue("parcel")
|
||
if !a.owner(r, id) {
|
||
fail(w, 404, "包裹不存在")
|
||
return
|
||
}
|
||
var state string
|
||
var created, expires int64
|
||
if e := a.db.QueryRow("SELECT state,created,expires FROM parcels WHERE id=?", id).Scan(&state, &created, &expires); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
files, e := a.fileList(id)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
respond(w, 200, map[string]any{"id": id, "state": state, "created": created, "expires": expires, "files": files})
|
||
}
|
||
func (a *App) mine(w http.ResponseWriter, r *http.Request) {
|
||
a.mu.Lock()
|
||
defer a.mu.Unlock()
|
||
v := a.currentVisitor(r)
|
||
if v == "" {
|
||
respond(w, 200, []any{})
|
||
return
|
||
}
|
||
rows, e := a.db.Query("SELECT p.id,p.state,p.created,p.expires,(SELECT count(*) FROM files WHERE parcel=p.id),(SELECT COALESCE(sum(size),0) FROM files WHERE parcel=p.id) FROM parcels p WHERE visitor=? ORDER BY created DESC LIMIT 100", v)
|
||
if e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
defer rows.Close()
|
||
out := []map[string]any{}
|
||
for rows.Next() {
|
||
var id, state string
|
||
var created, expires, size int64
|
||
var n int
|
||
if e = rows.Scan(&id, &state, &created, &expires, &n, &size); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
out = append(out, map[string]any{"id": id, "state": state, "created": created, "expires": expires, "count": n, "size": size})
|
||
}
|
||
if e = rows.Err(); e != nil {
|
||
a.serverError(w, e)
|
||
return
|
||
}
|
||
respond(w, 200, out)
|
||
}
|