Skip to content

Commit

Permalink
feat: File API module (#2418)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Nov 14, 2023
1 parent 2fe18bb commit 7cce5f9
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 165 deletions.
7 changes: 7 additions & 0 deletions api.planx.uk/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ const securitySchemes = {
scheme: "bearer",
bearerFormat: "JWT",
},
fileAPIKeyAuth: {
type: "apiKey",
in: "header",
name: "api-key",
description:
"API key granted to third-party integration partners to access files uploaded by users as part of their application",
},
hasuraAuth: {
type: "apiKey",
in: "header",
Expand Down
118 changes: 118 additions & 0 deletions api.planx.uk/modules/file/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import assert from "assert";
import { uploadPrivateFile, uploadPublicFile } from "./service/uploadFile";
import { buildFilePath } from "./service/utils";
import { getFileFromS3 } from "./service/getFile";
import { z } from "zod";
import { ValidatedRequestHandler } from "../../shared/middleware/validate";
import { ServerError } from "../../errors";

assert(process.env.AWS_S3_BUCKET);
assert(process.env.AWS_S3_REGION);
assert(process.env.AWS_ACCESS_KEY);
assert(process.env.AWS_SECRET_KEY);

interface UploadFileResponse {
fileType: string | null;
fileUrl: string;
}

export const uploadFileSchema = z.object({
body: z.object({
filename: z.string().trim().min(1),
}),
});

export type UploadController = ValidatedRequestHandler<
typeof uploadFileSchema,
UploadFileResponse
>;

export const privateUploadController: UploadController = async (
req,
res,
next,
) => {
try {
if (!req.file) throw Error("Missing file");
const { filename } = res.locals.parsedReq.body;
const fileResponse = await uploadPrivateFile(req.file, filename);
res.json(fileResponse);
} catch (error) {
return next(
new ServerError({ message: `Failed to upload private file: ${error}` }),
);
}
};

export const publicUploadController: UploadController = async (
req,
res,
next,
) => {
try {
if (!req.file) throw Error("Missing file");
const { filename } = res.locals.parsedReq.body;
const fileResponse = await uploadPublicFile(req.file, filename);
res.json(fileResponse);
} catch (error) {
return next(
new ServerError({
message: `Failed to upload public file: ${(error as Error).message}`,
}),
);
}
};

export const downloadFileSchema = z.object({
params: z.object({
fileKey: z.string(),
fileName: z.string(),
}),
});

export type DownloadController = ValidatedRequestHandler<
typeof downloadFileSchema,
Buffer | undefined
>;

export const publicDownloadController: DownloadController = async (
_req,
res,
next,
) => {
const { fileKey, fileName } = res.locals.parsedReq.params;
const filePath = buildFilePath(fileKey, fileName);

try {
const { body, headers, isPrivate } = await getFileFromS3(filePath);

if (isPrivate) throw Error("Bad request");

res.set(headers);
res.send(body);
} catch (error) {
return next(
new ServerError({ message: `Failed to download public file: ${error}` }),
);
}
};

export const privateDownloadController: DownloadController = async (
_req,
res,
next,
) => {
const { fileKey, fileName } = res.locals.parsedReq.params;
const filePath = buildFilePath(fileKey, fileName);

try {
const { body, headers } = await getFileFromS3(filePath);

res.set(headers);
res.send(body);
} catch (error) {
return next(
new ServerError({ message: `Failed to download private file: ${error}` }),
);
}
};
99 changes: 99 additions & 0 deletions api.planx.uk/modules/file/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
tags:
- name: file
description: Endpoints for uploading and downloading files
components:
parameters:
fileKey:
in: path
name: fileKey
type: string
required: true
fileName:
in: path
name: fileName
type: string
required: true
schemas:
UploadFile:
type: object
properties:
filename:
type: string
required: true
file:
type: string
format: binary
responses:
UploadFile:
type: object
properties:
fileType:
oneOf:
- type: string
- type: "null"
fileUrl:
type: string
DownloadFile:
description: Successful response
content:
application/octet-stream:
schema:
type: string
format: binary
paths:
/file/private/upload:
post:
tags: ["file"]
security:
- bearerAuth: []
requestBody:
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/UploadFile"
responses:
"200":
$ref: "#/components/responses/UploadFile"
"500":
$ref: "#/components/responses/ErrorMessage"
/file/public/upload:
post:
tags: ["file"]
requestBody:
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/UploadFile"
responses:
"200":
$ref: "#/components/responses/UploadFile"
"500":
$ref: "#/components/responses/ErrorMessage"
/file/public/{fileKey}/{fileName}:
get:
tags: ["file"]
parameters:
- $ref: "#/components/parameters/fileKey"
- $ref: "#/components/parameters/fileName"
responses:
"200":
$ref: "#/components/responses/DownloadFile"
"500":
$ref: "#/components/responses/ErrorMessage"
/file/private/{fileKey}/{fileName}:
get:
tags: ["file"]
parameters:
- $ref: "#/components/parameters/fileKey"
- $ref: "#/components/parameters/fileName"
security:
- fileAPIKeyAuth: []
responses:
"200":
$ref: "#/components/responses/DownloadFile"
"500":
$ref: "#/components/responses/ErrorMessage"
Loading

0 comments on commit 7cce5f9

Please sign in to comment.