Files
outline/plugins/storage/server/api/files.ts
Nanguan Lin 3a7dd94e14 Migrate from s3 sdk v2 to v3 (#6731)
* chore: migrate from s3 sdk v2 to v3

* import signature-v4-crt

* downgrade minor version

* Add s3-presigned-post manually

* Change s3 mock

* Update server/storage/files/S3Storage.ts

* docs

* Upgrade aws-sdk

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-05-19 06:01:42 -07:00

131 lines
3.5 KiB
TypeScript

import JWT from "jsonwebtoken";
import Router from "koa-router";
import mime from "mime-types";
import env from "@server/env";
import {
AuthenticationError,
AuthorizationError,
ValidationError,
} from "@server/errors";
import auth from "@server/middlewares/authentication";
import multipart from "@server/middlewares/multipart";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import validate from "@server/middlewares/validate";
import { Attachment } from "@server/models";
import AttachmentHelper from "@server/models/helpers/AttachmentHelper";
import { authorize } from "@server/policies";
import FileStorage from "@server/storage/files";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { getJWTPayload } from "@server/utils/jwt";
import * as T from "./schema";
const router = new Router();
router.post(
"files.create",
rateLimiter(RateLimiterStrategy.TenPerMinute),
auth(),
validate(T.FilesCreateSchema),
multipart({
maximumFileSize: Math.max(
env.FILE_STORAGE_UPLOAD_MAX_SIZE,
env.FILE_STORAGE_IMPORT_MAX_SIZE
),
}),
async (ctx: APIContext<T.FilesCreateReq>) => {
const actor = ctx.state.auth.user;
const { key } = ctx.input.body;
const file = ctx.input.file;
const attachment = await Attachment.findOne({
where: { key },
rejectOnEmpty: true,
});
if (attachment.userId !== actor.id) {
throw AuthorizationError("Invalid key");
}
try {
await attachment.writeFile(file);
} catch (err) {
if (err.message.includes("permission denied")) {
throw Error(
`Permission denied writing to "${key}". Check the host machine file system permissions.`
);
}
throw err;
}
ctx.body = {
success: true,
};
}
);
router.get(
"files.get",
auth({ optional: true }),
validate(T.FilesGetSchema),
async (ctx: APIContext<T.FilesGetReq>) => {
const actor = ctx.state.auth.user;
const key = getKeyFromContext(ctx);
const isSignedRequest = !!ctx.input.query.sig;
const { isPublicBucket, fileName } = AttachmentHelper.parseKey(key);
const skipAuthorize = isPublicBucket || isSignedRequest;
const cacheHeader = "max-age=604800, immutable";
if (skipAuthorize) {
ctx.set("Cache-Control", cacheHeader);
ctx.set(
"Content-Type",
(fileName ? mime.lookup(fileName) : undefined) ||
"application/octet-stream"
);
ctx.attachment(fileName);
ctx.body = await FileStorage.getFileStream(key);
} else {
const attachment = await Attachment.findOne({
where: { key },
rejectOnEmpty: true,
});
authorize(actor, "read", attachment);
ctx.set("Cache-Control", cacheHeader);
ctx.set("Content-Type", attachment.contentType);
ctx.attachment(attachment.name, {
type: FileStorage.getContentDisposition(attachment.contentType),
});
ctx.body = attachment.stream;
}
}
);
function getKeyFromContext(ctx: APIContext<T.FilesGetReq>): string {
const { key, sig } = ctx.input.query;
if (sig) {
const payload = getJWTPayload(sig);
if (payload.type !== "attachment") {
throw AuthenticationError("Invalid signature");
}
try {
JWT.verify(sig, env.SECRET_KEY);
} catch (err) {
throw AuthenticationError("Invalid signature");
}
return payload.key as string;
}
if (key) {
return key;
}
throw ValidationError("Must provide either key or sig parameter");
}
export default router;