[server][core] Restore deleted

This commit is contained in:
Abhishek Shroff
2025-03-17 11:49:33 +05:30
parent 1257bfe9e6
commit fb005e2ccd
8 changed files with 213 additions and 69 deletions

View File

@@ -23,6 +23,7 @@ func SetupCommand() *cobra.Command {
cmd.AddCommand(
setupListCommand(),
setupSummaryCommand(),
setupRestoreCommand(),
)
return cmd

View File

@@ -0,0 +1,31 @@
package trash
import (
"fmt"
"os"
"github.com/shroff/phylum/server/internal/command/common"
"github.com/spf13/cobra"
)
func setupRestoreCommand() *cobra.Command {
cmd := cobra.Command{
Use: "restore uuid... ",
Short: "Restore Deleted Resource",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
f := common.UserFileSystem(cmd)
for _, uuid := range args {
r, err := f.ResourceByPathOrUUID(uuid)
items, err := r.Restore()
if err != nil {
fmt.Println("cannot restore '" + uuid + "': " + err.Error())
os.Exit(1)
}
fmt.Printf("Items restored: %d\n", items)
}
},
}
return &cmd
}

View File

@@ -96,56 +96,6 @@ type CreateResourcesParams struct {
ContentSha256 string
}
const deleteRecursive = `-- name: DeleteRecursive :many
WITH RECURSIVE nodes(id, parent, dir) AS (
SELECT r.id, r.parent, r.dir
FROM resources r
WHERE
CASE $1::boolean
WHEN TRUE THEN r.parent = $2::uuid
ELSE r.id = $2::uuid
END
UNION ALL
SELECT r.id, r.parent, r.dir
FROM resources r JOIN nodes n on r.parent = n.id
WHERE deleted IS NULL
)
UPDATE resources
SET modified = NOW(), deleted = NOW()
WHERE id in (SELECT id FROM nodes)
RETURNING id, dir
`
type DeleteRecursiveParams struct {
PreserveRoot bool
ID uuid.UUID
}
type DeleteRecursiveRow struct {
ID uuid.UUID
Dir bool
}
func (q *Queries) DeleteRecursive(ctx context.Context, arg DeleteRecursiveParams) ([]DeleteRecursiveRow, error) {
rows, err := q.db.Query(ctx, deleteRecursive, arg.PreserveRoot, arg.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DeleteRecursiveRow
for rows.Next() {
var i DeleteRecursiveRow
if err := rows.Scan(&i.ID, &i.Dir); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const diskUsage = `-- name: DiskUsage :one
WITH RECURSIVE nodes(id, parent, content_length, dir) AS (
SELECT r.id, r.parent, r.content_length, r.dir

View File

@@ -12,6 +12,102 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const deleteRecursive = `-- name: DeleteRecursive :many
WITH RECURSIVE nodes(id, parent, dir) AS (
SELECT r.id, r.parent, r.dir
FROM resources r
WHERE
CASE $1::boolean
WHEN TRUE THEN r.parent = $2::uuid
ELSE r.id = $2::uuid
END
UNION ALL
SELECT r.id, r.parent, r.dir
FROM resources r JOIN nodes n on r.parent = n.id
WHERE deleted IS NULL
)
UPDATE resources
SET modified = NOW(), deleted = NOW()
WHERE id in (SELECT id FROM nodes)
RETURNING id, dir
`
type DeleteRecursiveParams struct {
PreserveRoot bool
ID uuid.UUID
}
type DeleteRecursiveRow struct {
ID uuid.UUID
Dir bool
}
func (q *Queries) DeleteRecursive(ctx context.Context, arg DeleteRecursiveParams) ([]DeleteRecursiveRow, error) {
rows, err := q.db.Query(ctx, deleteRecursive, arg.PreserveRoot, arg.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DeleteRecursiveRow
for rows.Next() {
var i DeleteRecursiveRow
if err := rows.Scan(&i.ID, &i.Dir); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const restoreDeleted = `-- name: RestoreDeleted :many
WITH RECURSIVE nodes(id, parent, ts) AS (
SELECT r.id, r.parent, r.deleted
FROM resources r
WHERE r.id = $1::UUID
AND CASE
WHEN $2::TEXT IS NULL THEN TRUE
ELSE permissions[$2::TEXT]::integer <> 0 END
UNION ALL
SELECT r.id, r.parent, n.ts
FROM resources r
JOIN nodes n ON r.parent = n.id
WHERE r.deleted = n.ts
)
UPDATE resources
SET modified = NOW(), deleted = NULL
FROM nodes
WHERE resources.id = nodes.id
RETURNING 1
`
type RestoreDeletedParams struct {
ResourceID uuid.UUID
Username pgtype.Text
}
func (q *Queries) RestoreDeleted(ctx context.Context, arg RestoreDeletedParams) ([]int32, error) {
rows, err := q.db.Query(ctx, restoreDeleted, arg.ResourceID, arg.Username)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int32
for rows.Next() {
var column_1 int32
if err := rows.Scan(&column_1); err != nil {
return nil, err
}
items = append(items, column_1)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const trashList = `-- name: TrashList :many
SELECT id, name, parent, dir, created, modified, deleted, content_length, content_type, content_sha256, permissions, grants FROM resources
WHERE CASE

View File

@@ -18,4 +18,5 @@ var (
ErrResourceMoveTargetSubdirectory = errors.NewError(http.StatusConflict, "move_target_subdirectory", "Cannot move a resource to its own subdirectory")
ErrResourceCopyTargetSelf = errors.NewError(http.StatusConflict, "copy_target_self", "Cannot overwrite a resource with itself")
ErrParentNotFound = errors.NewError(http.StatusConflict, "parent_not_found", "Parent not found")
ErrParentDeleted = errors.NewError(http.StatusConflict, "parent_deleted", "Parent deleted")
)

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/shroff/phylum/server/internal/core/db"
)
@@ -71,3 +72,41 @@ func (f filesystem) TrashSummary() (int, int, error) {
return int(res.Items), int(res.Size), nil
}
}
// RestoreDeleted restores a previously deleted resources
// Checks:
// - Parent must not be deleted
// - Parent must have write permission
// - No name conflict with exiting resource
func (r Resource) Restore() (int, error) {
p, err := r.f.ResourceByID(*r.parentID)
if err == ErrResourceNotFound {
err = ErrParentNotFound
}
if err != nil {
return 0, err
}
if p.deleted != nil {
return 0, ErrParentDeleted
}
if !p.hasPermission(PermissionWrite) {
return 0, ErrInsufficientPermissions
}
_, err = r.f.db.ChildResourceByName(r.f.ctx, db.ChildResourceByNameParams{Parent: p.id, Name: r.name})
if err != pgx.ErrNoRows {
if err != nil {
err = ErrResourceNameConflict
}
return 0, err
}
var username pgtype.Text
if !r.f.fullAccess {
username.String = r.f.username
username.Valid = true
}
res, err := r.f.db.RestoreDeleted(r.f.ctx, db.RestoreDeletedParams{Username: username, ResourceID: r.id})
return len(res), err
}

View File

@@ -107,25 +107,6 @@ SELECT r.*
ON n.id = r.id
WHERE n.depth >= @min_depth::INTEGER;
-- name: DeleteRecursive :many
WITH RECURSIVE nodes(id, parent, dir) AS (
SELECT r.id, r.parent, r.dir
FROM resources r
WHERE
CASE @preserve_root::boolean
WHEN TRUE THEN r.parent = @id::uuid
ELSE r.id = @id::uuid
END
UNION ALL
SELECT r.id, r.parent, r.dir
FROM resources r JOIN nodes n on r.parent = n.id
WHERE deleted IS NULL
)
UPDATE resources
SET modified = NOW(), deleted = NOW()
WHERE id in (SELECT id FROM nodes)
RETURNING id, dir;
-- name: DiskUsage :one
WITH RECURSIVE nodes(id, parent, content_length, dir) AS (
SELECT r.id, r.parent, r.content_length, r.dir

View File

@@ -1,3 +1,48 @@
-- name: DeleteRecursive :many
WITH RECURSIVE nodes(id, parent, dir) AS (
SELECT r.id, r.parent, r.dir
FROM resources r
WHERE
CASE @preserve_root::boolean
WHEN TRUE THEN r.parent = @id::uuid
ELSE r.id = @id::uuid
END
UNION ALL
SELECT r.id, r.parent, r.dir
FROM resources r JOIN nodes n on r.parent = n.id
WHERE deleted IS NULL
)
UPDATE resources
SET modified = NOW(), deleted = NOW()
WHERE id in (SELECT id FROM nodes)
RETURNING id, dir;
-- name: RestoreDeleted :many
WITH RECURSIVE nodes(id, parent, ts) AS (
SELECT r.id, r.parent, r.deleted
FROM resources r
WHERE r.id = @resource_id::UUID
AND CASE
WHEN sqlc.narg('username')::TEXT IS NULL THEN TRUE
ELSE permissions[@username::TEXT]::integer <> 0 END
UNION ALL
SELECT r.id, r.parent, n.ts
FROM resources r
JOIN nodes n ON r.parent = n.id
WHERE r.deleted = n.ts
)
UPDATE resources
SET modified = NOW(), deleted = NULL
FROM nodes
WHERE resources.id = nodes.id
RETURNING 1;
SELECT r.*
FROM nodes n
JOIN resources r
ON n.id = r.id
WHERE n.depth >= @min_depth::INTEGER;
-- name: TrashList :many
SELECT * FROM resources
WHERE CASE