Skip to content

Commit

Permalink
api: asset: delete & restore (#2092)
Browse files Browse the repository at this point in the history
* api: asset: delete & restore

* flag asset as deleted on completion

* address comments
  • Loading branch information
gioelecerati authored Apr 18, 2024
1 parent 7c92e5d commit d5d6e65
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 3 deletions.
80 changes: 78 additions & 2 deletions packages/api/src/controllers/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ import { CliArgs } from "../parse-cli";
import mung from "express-mung";
import { getClips } from "./clip";

// 7 Days
const DELETE_ASSET_DELAY = 7 * 24 * 60 * 60 * 1000;

const app = Router();

export function catalystPipelineStrategy(req: Request) {
Expand Down Expand Up @@ -629,8 +632,18 @@ const fieldsMap = {
} as const;

app.get("/", authorizer({}), async (req, res) => {
let { limit, cursor, all, allUsers, order, filters, count, cid, ...otherQs } =
toStringValues(req.query);
let {
limit,
cursor,
all,
allUsers,
order,
filters,
count,
cid,
deleting,
...otherQs
} = toStringValues(req.query);
const fieldFilters = _(otherQs)
.pick("playbackId", "sourceUrl", "phase")
.map((v, k) => ({ id: k, value: decodeURIComponent(v) }))
Expand Down Expand Up @@ -661,6 +674,16 @@ app.get("/", authorizer({}), async (req, res) => {
query.push(
sql`coalesce(asset.data->>'projectId', '') = ${req.project?.id || ""}`
);
if (req.user.admin && deleting) {
const deletionThreshold = new Date(
Date.now() - DELETE_ASSET_DELAY
).toISOString();

query.push(sql`asset.data->'status'->>'phase' = 'deleting'`);
query.push(
sql`asset.data->>'deletedAt' IS NOT NULL AND asset.data->>'deletedAt' < ${deletionThreshold}`
);
}

let output: WithID<Asset>[];
let newCursor: string;
Expand Down Expand Up @@ -1100,11 +1123,64 @@ app.delete("/:id", authorizer({}), async (req, res) => {
if (!req.user.admin && req.user.id !== asset.userId) {
throw new ForbiddenError(`users may only delete their own assets`);
}

if (asset.status.phase === "deleting" || asset.deleted) {
throw new BadRequestError(`asset is already deleted`);
}

await req.taskScheduler.deleteAsset(asset);
res.status(204);
res.end();
});

app.post("/:id/restore", authorizer({}), async (req, res) => {
const { id } = req.params;
const asset = await db.asset.get(id);

if (!asset) {
throw new NotFoundError(`asset not found`);
}

if (!req.user.admin && req.user.id !== asset.userId) {
throw new ForbiddenError(`users may only restore their own assets`);
}

if (!asset.deleted) {
throw new BadRequestError(`asset is not deleted`);
}

if (asset.status?.phase !== "deleting") {
throw new BadRequestError(`asset is not in a restorable state`);
}

await req.taskScheduler.restoreAsset(asset);
res.status(204);
res.end();
});

app.patch("/:id/deleted", authorizer({ anyAdmin: true }), async (req, res) => {
const { id } = req.params;
const asset = await db.asset.get(id);

if (!asset) {
throw new NotFoundError(`asset not found`);
}

if (!(asset.status.phase === "deleting")) {
throw new BadRequestError(`asset is not in a deleting phase`);
}

await db.asset.update(asset.id, {
status: {
phase: "deleted",
updatedAt: Date.now(),
},
});

res.status(204);
res.end();
});

app.delete("/", authorizer({ anyAdmin: true }), async (req, res) => {
if (req.query.userId && req.query.userId !== req.user.id && !req.user.admin) {
throw new ForbiddenError(`users may only delete their own assets`);
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/schema/api-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ components:
- processing
- ready
- failed
- deleting
- deleted
updatedAt:
type: number
description:
Expand Down
36 changes: 35 additions & 1 deletion packages/api/src/task/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,44 @@ export class TaskScheduler {
if (typeof asset === "string") {
asset = await db.asset.get(asset);
}

let phase = asset.status?.phase;
// Prevent bump of updatedAt if phase isn't getting updated
let updatedAt = asset.status?.updatedAt;

if (phase === "ready") {
// If the asset is ready, we need to schedule deletion
phase = "deleting";
updatedAt = Date.now();
}

await this.updateAsset(asset, {
deleted: true,
deletedAt: Date.now(),
status: asset.status, // prevent updatedAt from being bumped
status: {
phase: phase,
updatedAt: updatedAt,
},
});
return true;
} catch (e) {
return false;
}
}

async restoreAsset(asset: string | Asset) {
try {
if (typeof asset === "string") {
asset = await db.asset.get(asset);
}

await this.updateAsset(asset, {
deleted: false,
deletedAt: null,
status: {
phase: "ready",
updatedAt: Date.now(),
},
});
return true;
} catch (e) {
Expand Down

0 comments on commit d5d6e65

Please sign in to comment.