package core import ( "time" "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/jobs" "github.com/doug-martin/goqu/v9" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) func (f *FileSystem) Delete(r Resource) (Resource, error) { var res Resource err := f.runInTx(func(f txFileSystem) error { var err error res, err = f.Delete(r) return err }) return res, err } func (f txFileSystem) Delete(r Resource) (Resource, error) { if !r.parentID.Valid { return Resource{}, ErrInsufficientPermissions } if r.deleted.Valid { return Resource{}, ErrResourceDeleted } parent, err := f.ResourceByID(r.parentID.Bytes) if err == nil { err = parent.checkPermission(f.user, PermissionWrite) } if err != nil { return Resource{}, err } if err := softDelete(f.db, r.id); err != nil { return Resource{}, err } if err := updateResourceModified(f.db, r.parentID.Bytes); err != nil { return Resource{}, err } r.deleted = pgtype.Timestamp{ Valid: true, Time: time.Now(), } return r, nil } func softDelete(db db.TxHandler, id uuid.UUID) error { n, q := selectResourceTree(id, false) r := goqu.T("resources") // Set modified and deleted query, params, _ := q. From(r). Where(r.Col("id").Eq(pg.From(n).Select("id"))). Update(). Set( goqu.Record{ "modified": goqu.L("NOW()"), "deleted": goqu.L("NOW()"), }). ToSQL() if _, err := db.Exec(query, params...); err != nil { return err } // Add to trash query, params, _ = pg.Insert(goqu.T("trash")).Cols("id").Vals(goqu.Vals{id}).ToSQL() _, err := db.Exec(query, params...) return err } func softDeleteChildren(db db.TxHandler, id, parent uuid.UUID) error { n, s := selectResourceTree(id, false) r := goqu.T("resources") // Mark deleted q, params, _ := s. From(r). Where(r.Col("id").Eq(pg.From(n).Select("id"))). Where(r.Col("id").Neq(id)). Update(). Set( goqu.Record{ "modified": goqu.L("NOW()"), "deleted": goqu.L("NOW()"), }). ToSQL() if _, err := db.Exec(q, params...); err != nil { return err } // Add children to trash insert := pg. Insert(goqu.T("trash")). Cols("id"). FromQuery(pg. From("resources"). Select("id"). Where(goqu.C("parent").Eq(id))) q, args, _ := insert.ToSQL() if _, err := db.Exec(q, args...); err != nil { return err } return updateResourceModified(db, parent) } func (f *FileSystem) DeleteForever(r Resource) error { return f.runInTx(func(f txFileSystem) error { return f.DeleteForever(r) }) } func (f txFileSystem) DeleteForever(r Resource) error { if !r.parentID.Valid { return ErrInsufficientPermissions } parent, err := f.ResourceByID(r.parentID.Bytes) if err == nil { err = parent.checkPermission(f.user, PermissionWrite) } if err != nil { return err } // Select all descendants, including deleted resources n, q := selectResourceTree(r.id, true) if err := updateResourceModified(f.db, parent.id); err != nil { return err // deleteAllVersions needs to be called last, as it will enqueue the delete jobs } else if err := hardDeleteAllVersions(f.db, q, n); err != nil { return err } return nil } func collectDeletedVersions(rows pgx.Rows) ([]jobs.DeleteContentsArgs, error) { defer rows.Close() var result []jobs.DeleteContentsArgs var v jobs.DeleteContentsArgs var storage pgtype.Text var id uuid.UUID for rows.Next() { err := rows.Scan(&id, &storage) if err != nil { return nil, err } if id != uuid.Nil { v.Name = id.String() result = append(result, v) } } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (f *FileSystem) RestoreDeleted(r Resource, parentPathOrUUID string, name string, autoRename bool) (res Resource, err error) { err = f.runInTx(func(f txFileSystem) error { var err error res, err = f.RestoreDeleted(r, parentPathOrUUID, name, autoRename) return err }) return res, err } // RestoreDeleted restores a previously deleted resources // Checks: // - Parent must not be deleted // - Parent must have write permission // - No name conflict with exiting resource func (f txFileSystem) RestoreDeleted(r Resource, parentPathOrUUID string, name string, autoRename bool) (res Resource, err error) { // Locate parent var parent Resource if parentPathOrUUID == "" { if r.parentID.Valid { parent, err = f.ResourceByID(r.parentID.Bytes) } else { err = ErrResourceNotFound } } else { parent, err = f.ResourceByPathWithRoot(parentPathOrUUID) } if err == ErrResourceNotFound { err = ErrParentNotFound } if err != nil { return } // Make sure parent is not deleted and has write permissions if parent.deleted.Valid { err = ErrParentDeleted return } if err = parent.checkPermission(f.user, PermissionWrite); err != nil { return } if name == "" { name = r.name } if name, err = detectNameConflict(f.db, parent.id, name, autoRename); err != nil { return } q, args, _ := pg.Delete(goqu.T("trash")).Where(goqu.C("id").Eq(r.id)).ToSQL() if _, err = f.db.Exec(q, args...); err != nil { return } if parent.id != r.parentID.Bytes || r.name != name { if err = updateResourceNameParent(f.db, r.id, name, pgtype.UUID{Bytes: parent.id, Valid: true}); err != nil { return } else { r.name = name r.parentID = pgtype.UUID{Bytes: parent.id, Valid: true} r.visibleParent = r.parentID } } n, s := selectResourceTree(r.id, false) tR := goqu.T("resources") query, params, _ := s. From(r). Where(tR.Col("id").Eq(pg.From(n).Select("id"))). Update().Set( goqu.Record{ "modified": goqu.L("NOW()"), "deleted": nil, }).ToSQL() if _, err = f.db.Exec(query, params...); err != nil { return } if err = recomputePermissions(f.db, r.id); err != nil { return } r.deleted = pgtype.Timestamp{} res = r return }