Allow file access not in Attachments table (#5876)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
import { buildAttachment } from "@server/test/factories";
|
||||
import Attachment from "./Attachment";
|
||||
|
||||
describe("#findByKey", () => {
|
||||
it("should return the correct attachment given a key", async () => {
|
||||
const attachment = await buildAttachment();
|
||||
const found = await Attachment.findByKey(attachment.key);
|
||||
expect(found?.id).toBe(attachment.id);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createReadStream } from "fs";
|
||||
import path from "path";
|
||||
import { File } from "formidable";
|
||||
import JWT from "jsonwebtoken";
|
||||
import { QueryTypes } from "sequelize";
|
||||
import {
|
||||
BeforeDestroy,
|
||||
@@ -15,10 +14,7 @@ import {
|
||||
IsNumeric,
|
||||
BeforeUpdate,
|
||||
} from "sequelize-typescript";
|
||||
import env from "@server/env";
|
||||
import { AuthenticationError } from "@server/errors";
|
||||
import FileStorage from "@server/storage/files";
|
||||
import { getJWTPayload } from "@server/utils/jwt";
|
||||
import { ValidateKey } from "@server/validation";
|
||||
import Document from "./Document";
|
||||
import Team from "./Team";
|
||||
@@ -172,42 +168,6 @@ class Attachment extends IdModel {
|
||||
return parseInt(result?.[0]?.total ?? "0", 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an attachment given a JWT signature.
|
||||
*
|
||||
* @param sign - The signature that uniquely identifies an attachment
|
||||
* @returns A promise resolving to attachment corresponding to the signature
|
||||
* @throws {AuthenticationError} Invalid signature if the signature verification fails
|
||||
*/
|
||||
static async findBySignature(sign: string): Promise<Attachment> {
|
||||
const payload = getJWTPayload(sign);
|
||||
|
||||
if (payload.type !== "attachment") {
|
||||
throw AuthenticationError("Invalid signature");
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(sign, env.SECRET_KEY);
|
||||
} catch (err) {
|
||||
throw AuthenticationError("Invalid signature");
|
||||
}
|
||||
|
||||
return this.findByKey(payload.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an attachment given a key
|
||||
*
|
||||
* @param key The key representing attachment file path
|
||||
* @returns A promise resolving to attachment corresponding to the key
|
||||
*/
|
||||
static async findByKey(key: string): Promise<Attachment> {
|
||||
return this.findOne({
|
||||
where: { key },
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
}
|
||||
|
||||
// associations
|
||||
|
||||
@BelongsTo(() => Team, "teamId")
|
||||
|
||||
@@ -33,6 +33,34 @@ export default class AttachmentHelper {
|
||||
return `${keyPrefix}/${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a key into its constituent parts
|
||||
*
|
||||
* @param key The key to parse
|
||||
* @returns The constituent parts
|
||||
*/
|
||||
static parseKey(key: string): {
|
||||
bucket: string;
|
||||
userId: string;
|
||||
id: string;
|
||||
fileName: string | undefined;
|
||||
isPublicBucket: boolean;
|
||||
} {
|
||||
const parts = key.split("/");
|
||||
const bucket = parts[0];
|
||||
const userId = parts[1];
|
||||
const id = parts[2];
|
||||
const [fileName] = parts.length > 3 ? parts.slice(-1) : [];
|
||||
|
||||
return {
|
||||
bucket,
|
||||
userId,
|
||||
id,
|
||||
fileName,
|
||||
isPublicBucket: bucket === Buckets.avatars || bucket === Buckets.public,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ACL to use for a given attachment preset
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ import path from "path";
|
||||
import { Readable } from "stream";
|
||||
import invariant from "invariant";
|
||||
import JWT from "jsonwebtoken";
|
||||
import safeResolvePath from "resolve-path";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import BaseStorage from "./BaseStorage";
|
||||
@@ -68,11 +69,11 @@ export default class LocalStorage extends BaseStorage {
|
||||
src = Readable.from(body);
|
||||
}
|
||||
|
||||
const destPath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key);
|
||||
closeSync(openSync(destPath, "w"));
|
||||
const filePath = this.getFilePath(key);
|
||||
closeSync(openSync(filePath, "w"));
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const dest = createWriteStream(destPath)
|
||||
const dest = createWriteStream(filePath)
|
||||
.on("error", reject)
|
||||
.on("finish", () => resolve(this.getUrlForKey(key)));
|
||||
|
||||
@@ -86,7 +87,7 @@ export default class LocalStorage extends BaseStorage {
|
||||
};
|
||||
|
||||
public async deleteFile(key: string) {
|
||||
const filePath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key);
|
||||
const filePath = this.getFilePath(key);
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch (err) {
|
||||
@@ -112,12 +113,15 @@ export default class LocalStorage extends BaseStorage {
|
||||
};
|
||||
|
||||
public getFileStream(key: string) {
|
||||
return createReadStream(this.getFilePath(key));
|
||||
}
|
||||
|
||||
private getFilePath(key: string) {
|
||||
invariant(
|
||||
env.FILE_STORAGE_LOCAL_ROOT_DIR,
|
||||
"FILE_STORAGE_LOCAL_ROOT_DIR is required"
|
||||
);
|
||||
|
||||
const filePath = path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key);
|
||||
return createReadStream(filePath);
|
||||
return safeResolvePath(env.FILE_STORAGE_LOCAL_ROOT_DIR, key);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user