diff --git a/lib/ui/api.ts b/lib/ui/api.ts index 7913869..98b94a2 100644 --- a/lib/ui/api.ts +++ b/lib/ui/api.ts @@ -457,6 +457,36 @@ for (const [resource, flags] of Object.entries(resources)) { } } +router.get("/blob/files/:id", async (ctx) => { + const authorizer: Authorizer = ctx.state.authorizer; + const resource = "files"; + const id = ctx.params.id; + + const log = { + message: `Download ${resource}`, + context: ctx, + id: id, + }; + + const filter = and(authorizer.getFilter(resource, 2), [ + "=", + ["PARAM", RESOURCE_IDS[resource]], + ctx.params.id, + ]); + + if (!authorizer.hasAccess(resource, 2)) { + logUnauthorizedWarning(log); + return void (ctx.status = 403); + } + + const count = await db.count(resource, filter); + if (!count) return void (ctx.status = 404); + + logger.accessInfo(log); + ctx.body = db.downloadFile(id); + ctx.attachment(id); +}); + router.put("/files/:id", async (ctx) => { const authorizer: Authorizer = ctx.state.authorizer; const resource = "files"; diff --git a/lib/ui/db.ts b/lib/ui/db.ts index 4eec1ce..5b9f642 100644 --- a/lib/ui/db.ts +++ b/lib/ui/db.ts @@ -315,6 +315,11 @@ export function deleteUser(id: string): Promise { return deleteResource("users", id); } +export function downloadFile(filename: string): Readable { + const bucket = new GridFSBucket(db); + return bucket.openDownloadStreamByName(filename); +} + export function putFile( filename: string, metadata: Record, diff --git a/ui/files-page.ts b/ui/files-page.ts index 229c35c..54e586d 100644 --- a/ui/files-page.ts +++ b/ui/files-page.ts @@ -212,6 +212,9 @@ export const component: ClosureComponent = (): Component => { attrs["sortAttributes"] = sortAttributes; attrs["onSortChange"] = onSortChange; attrs["downloadUrl"] = downloadUrl; + attrs["recordActionsCallback"] = (file) => { + return [m("a", { href: "api/blob/files/" + file["_id"] }, "Download")]; + }; if (window.authorizer.hasAccess("files", 3)) { attrs["actionsCallback"] = (selected: Set): Children => {