diff --git a/src/components/file/FileTable.tsx b/src/components/file/FileTable.tsx index a1e69cb..3336bc6 100644 --- a/src/components/file/FileTable.tsx +++ b/src/components/file/FileTable.tsx @@ -1,9 +1,10 @@ -import { Add } from "@mui/icons-material"; +import { Add, Download } from "@mui/icons-material"; import { Alert, Box, Button, Checkbox, + IconButton, Paper, Snackbar, Table, @@ -13,6 +14,7 @@ import { TablePagination, TableRow, TextField, + Tooltip, } from "@mui/material"; import { Cursors } from "@vertexvis/api-client-node"; import debounce from "lodash.debounce"; @@ -36,6 +38,7 @@ export const headCells: readonly HeadCell[] = [ { id: "id", label: "ID" }, { id: "created", label: "Created" }, { id: "uploaded", label: "Uploaded" }, + { id: "download", label: "Download" }, ]; function useFiles({ cursor, pageSize, suppliedId }: SwrProps) { @@ -68,6 +71,7 @@ export default function FilesTable({ string | undefined >(); const [showToast, setShowToast] = React.useState(false); + const [downloadError, setDownloadError] = React.useState(); const { data, error, mutate } = useFiles({ cursor, pageSize, suppliedId }); const page = data ? toFilePage(data) : undefined; @@ -123,6 +127,30 @@ export default function FilesTable({ mutate(); } + async function handleDownload(id: string) { + setDownloadError(undefined); + + const res = await fetch( + `/api/files/${encodeURIComponent(id)}/download-url`, + { + method: "POST", + } + ); + + const body = await res.json(); + if (!res.ok || body.url == null) { + setDownloadError( + body.message ?? "Could not create a download URL for this file." + ); + return; + } + + const opened = window.open(body.url as string, "_blank", "noopener"); + if (opened == null) { + window.location.assign(body.url as string); + } + } + return ( <> @@ -174,7 +202,7 @@ export default function FilesTable({ ) : !page ? ( @@ -209,6 +237,24 @@ export default function FilesTable({ {row.id} {toLocaleString(row.created)} {toLocaleString(row.uploaded)} + { + e.stopPropagation(); + }} + > + + { + e.stopPropagation(); + handleDownload(row.id); + }} + size="small" + > + + + + ); }) @@ -249,6 +295,15 @@ export default function FilesTable({ File created! + setDownloadError(undefined)} + > + setDownloadError(undefined)} severity="error"> + {downloadError} + + ); } diff --git a/src/pages/api/files/[id]/download-url.ts b/src/pages/api/files/[id]/download-url.ts new file mode 100644 index 0000000..eefa691 --- /dev/null +++ b/src/pages/api/files/[id]/download-url.ts @@ -0,0 +1,64 @@ +import { head, logError, VertexError } from "@vertexvis/api-client-node"; +import { NextApiResponse } from "next"; + +import { + ErrorRes, + InvalidBody, + MethodNotAllowed, + Res, + ServerError, + toErrorRes, +} from "../../../../lib/api"; +import { getClientFromSession } from "../../../../lib/vertex-api"; +import withSession, { NextIronRequest } from "../../../../lib/with-session"; + +const DefaultDownloadExpirySeconds = 30; + +interface CreateDownloadUrlRes extends Res { + readonly url: string; +} + +export default withSession(async function handle( + req: NextIronRequest, + res: NextApiResponse +): Promise { + if (req.method === "POST") { + const r = await create(req); + return res.status(r.status).json(r); + } + + return res.status(MethodNotAllowed.status).json(MethodNotAllowed); +}); + +async function create( + req: NextIronRequest +): Promise { + try { + const id = head(req.query.id); + if (id == null) return InvalidBody; + + const client = await getClientFromSession(req.session); + const downloadRes = await client.files.createDownloadUrl({ + id, + createDownloadRequest: { + data: { + type: "download-url", + attributes: { expiry: DefaultDownloadExpirySeconds }, + }, + }, + }); + + const url = + downloadRes.data.data.attributes.uri ?? + downloadRes.data.data.attributes.downloadUrl; + if (url == null) return ServerError; + + return { status: 200, url }; + } catch (error) { + const e = error as VertexError; + logError(e); + return e.vertexError?.res + ? toErrorRes({ failure: e.vertexError.res }) + : ServerError; + } +}