diff --git a/server/internal/core/resource_copy_move.go b/server/internal/core/resource_copy_move.go index f8c90c90..223c367d 100644 --- a/server/internal/core/resource_copy_move.go +++ b/server/internal/core/resource_copy_move.go @@ -60,10 +60,14 @@ func (f filesystem) Move(r Resource, target string, conflictResolution ResourceB var deleted = false return res, deleted, f.runInTx(func(f filesystem) error { if conflictResolution == ResourceBindConflictResolutionOverwrite || conflictResolution == ResourceBindConflictResolutionDelete { - if _, err := f.softDeleteChild(destParent, destName); err == nil { - deleted = true - } else if !errors.Is(err, ErrResourceNotFound) { + if id, _, err := f.childResourceIDByName(r.ID(), destName); err != nil { + if !errors.Is(err, ErrResourceNotFound) { + return err + } + } else if err := softDelete(f.db, id); err != nil { return err + } else { + deleted = true } } newParentID := pgtype.UUID{ diff --git a/server/internal/core/resource_create.go b/server/internal/core/resource_create.go index 597f8ed8..7737f048 100644 --- a/server/internal/core/resource_create.go +++ b/server/internal/core/resource_create.go @@ -156,8 +156,9 @@ func (f filesystem) createResource( case ResourceBindConflictResolutionError: err = ErrResourceNameConflict case ResourceBindConflictResolutionEnsure: - res, err = f.childResourceByName(parent, name) - if err == nil && res.dir != dir { + var rDir bool + _, rDir, err = f.childResourceIDByName(parent, name) + if err == nil && rDir != dir { err = ErrResourceNameConflict } case ResourceBindConflictResolutionRename: @@ -187,15 +188,21 @@ func (f filesystem) createResource( } } case ResourceBindConflictResolutionOverwrite: - res, err = f.childResourceByName(parent, name) + var rID uuid.UUID + var rDir bool + rID, rDir, err = f.childResourceIDByName(parent, name) if err == nil { deleted = true - if res.dir == dir { + if rDir == dir { if dir { - err = f.softDeleteChildren(res.id, parent) + err = f.softDeleteChildren(rID, parent) + } + if err == nil { + // Repurpose existing resource + res, err = f.ResourceByID(rID) } } else { - err = f.deleteRecursive(res.id, parent, true) + err = softDelete(f.db, res.id) if err == nil { res, created, _, err = f.createResource( id, @@ -209,10 +216,11 @@ func (f filesystem) createResource( } } case ResourceBindConflictResolutionDelete: - res, err = f.childResourceByName(parent, name) + var rID uuid.UUID + rID, _, err = f.childResourceIDByName(parent, name) if err == nil { deleted = true - err = f.deleteRecursive(res.id, parent, true) + err = softDelete(f.db, rID) if err == nil { res, created, _, err = f.createResource( id, diff --git a/server/internal/core/resource_delete.go b/server/internal/core/resource_delete.go index 3409a1ab..96fc31e1 100644 --- a/server/internal/core/resource_delete.go +++ b/server/internal/core/resource_delete.go @@ -1,11 +1,11 @@ package core import ( - "errors" "fmt" "path" "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" @@ -13,31 +13,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (f filesystem) softDeleteChild(r Resource, name string) (Resource, error) { - if !r.hasPermission(PermissionWrite) { - return Resource{}, ErrInsufficientPermissions - } - if r.deleted.Valid { - return Resource{}, ErrResourceDeleted - } - - c, err := f.childResourceByName(r.ID(), name) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - err = ErrResourceNotFound - } - return Resource{}, err - } - if err := f.deleteRecursive(c.ID(), r.ID(), true); err != nil { - return Resource{}, err - } - r.deleted = pgtype.Timestamp{ - Valid: true, - Time: time.Now(), - } - return r, nil -} - func (f filesystem) DeleteRecursive(r Resource, softDelete bool) (Resource, error) { if !r.parentID.Valid { return Resource{}, ErrInsufficientPermissions @@ -100,14 +75,14 @@ func (f filesystem) RestoreDeleted(r Resource, parentPathOrUUID string, name str if name == "" { name = r.name } - _, err = f.childResourceByName(p.id, name) + _, _, err = f.childResourceIDByName(p.id, name) if autoRename && err == nil { ext := path.Ext(name) basename := name[:len(name)-len(ext)] counter := 1 for { name = fmt.Sprintf("%s (%d)%s", basename, counter, ext) - if _, err = f.childResourceByName(p.id, name); err == nil { + if _, _, err = f.childResourceIDByName(p.id, name); err == nil { counter++ } else { break @@ -177,13 +152,82 @@ func (f filesystem) deleteRecursive(id, parent uuid.UUID, softDelete bool) error return err } -func (f filesystem) softDeleteChildren(id, parent uuid.UUID) error { - err := f.runInTx(func(f filesystem) error { - var err error - if _, _, err = f.markDeleted(id, true, true); err != nil { +func softDelete(d db.Handler, id uuid.UUID) error { + return d.RunInTx(func(db db.Handler) error { + // Select all descendants, not including deleted ones + _, _, q := selectResourceTree(id, false, false, false) + + // Set modified and deleted + query, params, _ := q. + 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 (f filesystem) hardDelete(id, parent uuid.UUID) error { + return f.runInTx(func(f filesystem) error { + // Select all descendants, including deleted resources + v := goqu.T("resource_versions").As("v") + r, _, q := selectResourceTree(id, false, true, false) + + // Delete resources and versions from db, returning version ids + query, params, _ := q. + LeftJoin(v, goqu.On(r.Col("id").Eq(v.Col("resource_id")))). + Delete(). + Returning(v.Col("id")). + ToSQL() + + if rows, err := f.db.Query(query, params...); err != nil { + return err + } else if ids, err := pgx.CollectRows(rows, scanID); err != nil { + return err + } else if err := f.updateResourceModified(parent); err != nil { + return err + } else { + jobs.DeleteAllVersionContents(ids) + } + return nil + }) +} + +func scanID(row pgx.CollectableRow) (uuid.UUID, error) { + var id uuid.UUID + err := row.Scan(&id) + return id, err +} + +func (f filesystem) softDeleteChildren(id, parent uuid.UUID) error { + err := f.runInTx(func(f filesystem) error { + // Select non-deleted descendents excluding the tree root (id) + _, _, s := selectResourceTree(id, true, false, false) + + // Mark deleted + q, params, _ := s.Update(). + Set( + goqu.Record{ + "modified": goqu.L("NOW()"), + "deleted": goqu.L("NOW()"), + }). + ToSQL() + if _, err := f.db.Exec(q, params...); err != nil { + return err + } + + // Add children to trash insert := pg. Insert(goqu.T("trash")). Cols("id"). diff --git a/server/internal/core/resource_locate.go b/server/internal/core/resource_locate.go index ff121283..95a75b50 100644 --- a/server/internal/core/resource_locate.go +++ b/server/internal/core/resource_locate.go @@ -120,18 +120,20 @@ func (f filesystem) targetNameParentByPathWithRoot(path string, src Resource) (s return name, parent, nil } -func (f filesystem) childResourceByName(parentID uuid.UUID, name string) (Resource, error) { - const query = fullResourceQuery + "WHERE r.parent = @parent::UUID AND r.name = @name::TEXT AND r.deleted IS NULL" +func (f filesystem) childResourceIDByName(parentID uuid.UUID, name string) (uuid.UUID, bool, error) { + const query = "SELECT id, dir FROM resources WHERE parent = @parent::UUID AND name = @name::TEXT AND deleted IS NULL" args := pgx.NamedArgs{ - "user_id": f.userID, - "parent": parentID, - "name": name, + "parent": parentID, + "name": name, } - if rows, err := f.db.Query(query, args); err != nil { - return Resource{}, err - } else { - return f.collectFullResource(rows) + row := f.db.QueryRow(query, args) + var id uuid.UUID + var dir bool + err := row.Scan(&id, &dir) + if Is(err, pgx.ErrNoRows) { + err = ErrResourceNotFound } + return id, dir, err } func parseUUIDPrefix(path string) (pgtype.UUID, string, error) {