Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: File API module #2418

Merged
merged 6 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading