Files
phylum/server/internal/core/trash.go
2025-06-22 14:06:33 +05:30

183 lines
5.2 KiB
Go

package core
import (
"context"
"encoding/base64"
"encoding/binary"
"fmt"
"time"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/jobs"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/sirupsen/logrus"
)
func (f *FileSystem) TrashList(cursor string, n uint) ([]Resource, string, error) {
t := goqu.T("trash")
r := goqu.T("resources").As("r")
p := goqu.T("resources").As("p")
v := goqu.T("resource_versions").As("v")
l := goqu.T("publinks").As("l")
q := pg.From(t).
Join(r, goqu.On(t.Col("id").Eq(r.Col("id")))).
Join(p, goqu.On(r.Col("parent").Eq(p.Col("id")))).
Select(
r.All(),
pg.Select(goqu.L(versionsQuery)).From(v).Where(v.Col("resource_id").Eq(r.Col("id"))),
pg.Select(goqu.L(publinksQuery)).From(l).Where(l.Col("root").Eq(r.Col("id"))),
// TODO: Always select p.id when fullAccess
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?::INT]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", f.user.ID)),
pg.Select(goqu.L("COALESCE(p.permissions, '{}'::JSONB)")),
)
if f.user.Permissions&PermissionFilesAll == 0 {
q = q.Where(goqu.L("r.permissions[?::INT]::INTEGER <> 0", f.user.ID))
}
if cursor != "" {
if d, err := base64.StdEncoding.DecodeString(cursor); err != nil {
return nil, "", err
} else if len(d) != 24 {
return nil, "", fmt.Errorf("illegal cursor. Length %d not expected", len(d))
} else {
t := int64(binary.LittleEndian.Uint64(d[16:]))
lastID, _ := uuid.FromBytes(d[:16])
lastTimestamp := time.Unix(t/1e9, t%1e9).UTC()
q = q.Where(
goqu.Or(
goqu.I("deleted").Lt(goqu.V(lastTimestamp)),
goqu.And(
goqu.I("deleted").Eq(goqu.V(lastTimestamp)),
goqu.I("id").Lt(goqu.V(lastID)),
)))
}
}
query, params, _ := q.
Order(goqu.C("deleted").Desc()).
OrderAppend(r.Col("id").Desc()).
Limit(n).
ToSQL()
if rows, err := f.db.Query(query, params...); err != nil {
return nil, "", err
} else if res, err := pgx.CollectRows(rows, scanFullResource); err != nil {
return nil, "", err
} else {
cursor := ""
if uint(len(res)) == n {
last := res[len(res)-1]
c := make([]byte, 24)
b, _ := last.id.MarshalBinary()
copy(c, b)
binary.LittleEndian.PutUint64(c[16:], uint64(last.deleted.Time.UnixNano()))
cursor = base64.StdEncoding.EncodeToString(c)
}
return res, cursor, nil
}
}
func TrashCompact(ctx context.Context, duration time.Duration) {
t := time.Now().Add(-duration)
logrus.Info(fmt.Sprintf("Removing files deleted before %s", t.Format(time.RFC1123)))
f := openOmniscient(db.Get(ctx))
if err := f.hardDeleteOldResources(t); err != nil {
logrus.Error(err)
}
}
func (f *FileSystem) TrashSummary() (int, int, error) {
v := goqu.T("resource_versions").As("v")
n, q := f.selectTrash(time.Time{})
q = q.LeftJoin(v, goqu.On(v.Col("resource_id").Eq(n.Col("id")))).
Select(
goqu.COALESCE(goqu.SUM(v.Col("size")), 0),
goqu.COUNT(goqu.L("DISTINCT(?)", n.Col("id"))))
query, args, _ := q.ToSQL()
row := f.db.QueryRow(query, args...)
var size int
var items int
err := row.Scan(&size, &items)
return items, size, err
}
func (f *FileSystem) TrashEmpty() error {
n, q := f.selectTrash(time.Time{})
return f.db.RunInTx(func(db db.TxHandler) error {
return hardDeleteAllVersions(db, q, n)
})
}
func (f *FileSystem) selectTrash(time time.Time) (exp.AliasedExpression, *goqu.SelectDataset) {
r := goqu.T("resources").As("r")
n := goqu.T("nodes").As("n")
t := goqu.T("trash").As("t")
base := pg.
From(r).
Select(r.Col("id"), r.Col("parent"), r.Col("deleted")).
Join(t, goqu.On(t.Col("id").Eq(r.Col("id"))))
if f.user.Permissions&PermissionFilesAll == 0 {
base = base.Where(goqu.L("r.permissions[?]::INTEGER <> 0", f.user.ID))
}
if !time.IsZero() {
base = base.Where(r.Col("deleted").Lt(goqu.V(time.UTC())))
}
rec := pg.
From(r).
Select(r.Col("id"), r.Col("parent"), r.Col("deleted")).
Join(n, goqu.On(r.Col("parent").Eq(n.Col("id")))).
// Some children may be independently trashed (at different times). Don't select those
Where(goqu.L("? IS NOT DISTINCT FROM ?", r.Col("deleted"), n.Col("deleted")))
q := pg.From(n).WithRecursive("nodes(id, parent, deleted)", base.UnionAll(rec))
return n, q
}
func (f *FileSystem) hardDeleteOldResources(t time.Time) error {
n, q := f.selectTrash(t)
return f.db.RunInTx(func(db db.TxHandler) error {
return hardDeleteAllVersions(db, q, n)
})
}
func hardDeleteAllVersions(db db.TxHandler, q *goqu.SelectDataset, n interface {
exp.Expression
Col(interface{}) exp.IdentifierExpression
}) error {
v := goqu.T("resource_versions").As("v")
query, params, _ := q.
Join(v, goqu.On(n.Col("id").Eq(v.Col("resource_id")))).
Where(v.Col("deleted").IsNotNull()).
Select(v.Col("id"), v.Col("storage")).
ToSQL()
var versions []jobs.DeleteContentsArgs
if rows, err := db.Query(query, params...); err != nil {
return err
} else if versions, err = collectDeletedVersions(rows); err != nil {
return err
}
r := goqu.T("resources")
query, args, _ := q.
From(r).
Where(r.Col("id").Eq(pg.From(n).Select("id"))).
Delete().ToSQL()
if _, err := db.Exec(query, args...); err != nil {
return err
} else {
jobs.DeleteContents(db, versions)
}
return nil
}