mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-05 19:21:23 -06:00
183 lines
5.2 KiB
Go
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
|
|
}
|