mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 13:20:03 -06:00
406 lines
10 KiB
TypeScript
406 lines
10 KiB
TypeScript
import {
|
|
DeleteObjectCommand,
|
|
DeleteObjectsCommand,
|
|
GetObjectCommand,
|
|
HeadBucketCommand,
|
|
ListObjectsCommand,
|
|
PutObjectCommand,
|
|
S3Client,
|
|
} from "@aws-sdk/client-s3";
|
|
import { PresignedPostOptions, createPresignedPost } from "@aws-sdk/s3-presigned-post";
|
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
import { randomUUID } from "crypto";
|
|
import { access, mkdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
|
|
import { lookup } from "mime-types";
|
|
import path, { join } from "path";
|
|
import { TAccessType } from "@formbricks/types/storage";
|
|
import {
|
|
IS_FORMBRICKS_CLOUD,
|
|
MAX_SIZES,
|
|
S3_ACCESS_KEY,
|
|
S3_BUCKET_NAME,
|
|
S3_ENDPOINT_URL,
|
|
S3_REGION,
|
|
S3_SECRET_KEY,
|
|
UPLOADS_DIR,
|
|
WEBAPP_URL,
|
|
isS3Configured,
|
|
} from "../constants";
|
|
import { generateLocalSignedUrl } from "../crypto";
|
|
import { env } from "../env";
|
|
|
|
// S3Client Singleton
|
|
let s3ClientInstance: S3Client | null = null;
|
|
|
|
export const getS3Client = () => {
|
|
if (!s3ClientInstance) {
|
|
const credentials =
|
|
S3_ACCESS_KEY && S3_SECRET_KEY
|
|
? { accessKeyId: S3_ACCESS_KEY, secretAccessKey: S3_SECRET_KEY }
|
|
: undefined;
|
|
|
|
s3ClientInstance = new S3Client({
|
|
credentials,
|
|
region: S3_REGION,
|
|
...(S3_ENDPOINT_URL && { endpoint: S3_ENDPOINT_URL }),
|
|
});
|
|
}
|
|
|
|
return s3ClientInstance;
|
|
};
|
|
|
|
export const testS3BucketAccess = async () => {
|
|
const s3Client = getS3Client();
|
|
|
|
try {
|
|
// Attempt to retrieve metadata about the bucket
|
|
const headBucketCommand = new HeadBucketCommand({
|
|
Bucket: S3_BUCKET_NAME,
|
|
});
|
|
|
|
await s3Client.send(headBucketCommand);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Failed to access S3 bucket: ${error}`);
|
|
throw new Error(`S3 Bucket Access Test Failed: ${error}`);
|
|
}
|
|
};
|
|
|
|
const ensureDirectoryExists = async (dirPath: string) => {
|
|
try {
|
|
await access(dirPath);
|
|
} catch (error: any) {
|
|
if (error.code === "ENOENT") {
|
|
await mkdir(dirPath, { recursive: true });
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
type TGetFileResponse = {
|
|
fileBuffer: Buffer;
|
|
metaData: {
|
|
contentType: string;
|
|
};
|
|
};
|
|
|
|
// discriminated union
|
|
type TGetSignedUrlResponse =
|
|
| { signedUrl: string; fileUrl: string; presignedFields: Object }
|
|
| {
|
|
signedUrl: string;
|
|
updatedFileName: string;
|
|
fileUrl: string;
|
|
signingData: {
|
|
signature: string;
|
|
timestamp: number;
|
|
uuid: string;
|
|
};
|
|
};
|
|
|
|
const getS3SignedUrl = async (fileKey: string): Promise<string> => {
|
|
const [_, accessType] = fileKey.split("/");
|
|
const expiresIn = accessType === "public" ? 60 * 60 : 10 * 60;
|
|
|
|
const getObjectCommand = new GetObjectCommand({
|
|
Bucket: S3_BUCKET_NAME,
|
|
Key: fileKey,
|
|
});
|
|
|
|
try {
|
|
const s3Client = getS3Client();
|
|
return await getSignedUrl(s3Client, getObjectCommand, { expiresIn });
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const getS3File = async (fileKey: string): Promise<string> => {
|
|
const signedUrl = await getS3SignedUrl(fileKey);
|
|
return signedUrl;
|
|
};
|
|
|
|
export const getLocalFile = async (filePath: string): Promise<TGetFileResponse> => {
|
|
try {
|
|
const file = await readFile(filePath);
|
|
let contentType = "";
|
|
|
|
try {
|
|
contentType = lookup(filePath) || "";
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
fileBuffer: file,
|
|
metaData: {
|
|
contentType: contentType ?? "",
|
|
},
|
|
};
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
// a single service for generating a signed url based on user's environment variables
|
|
export const getUploadSignedUrl = async (
|
|
fileName: string,
|
|
environmentId: string,
|
|
fileType: string,
|
|
accessType: TAccessType,
|
|
isBiggerFileUploadAllowed: boolean = false
|
|
): Promise<TGetSignedUrlResponse> => {
|
|
// add a unique id to the file name
|
|
|
|
const fileExtension = fileName.split(".").pop();
|
|
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
|
|
|
if (!fileExtension) {
|
|
throw new Error("File extension not found");
|
|
}
|
|
|
|
const updatedFileName = `${fileNameWithoutExtension}--fid--${randomUUID()}.${fileExtension}`;
|
|
|
|
// handle the local storage case first
|
|
if (!isS3Configured()) {
|
|
try {
|
|
const { signature, timestamp, uuid } = generateLocalSignedUrl(updatedFileName, environmentId, fileType);
|
|
|
|
return {
|
|
signedUrl:
|
|
accessType === "private"
|
|
? new URL(`${WEBAPP_URL}/api/v1/client/${environmentId}/storage/local`).href
|
|
: new URL(`${WEBAPP_URL}/api/v1/management/storage/local`).href,
|
|
signingData: {
|
|
signature,
|
|
timestamp,
|
|
uuid,
|
|
},
|
|
updatedFileName,
|
|
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
|
};
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const { presignedFields, signedUrl } = await getS3UploadSignedUrl(
|
|
updatedFileName,
|
|
fileType,
|
|
accessType,
|
|
environmentId,
|
|
isBiggerFileUploadAllowed
|
|
);
|
|
|
|
return {
|
|
signedUrl,
|
|
presignedFields,
|
|
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
|
};
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const getS3UploadSignedUrl = async (
|
|
fileName: string,
|
|
contentType: string,
|
|
accessType: string,
|
|
environmentId: string,
|
|
isBiggerFileUploadAllowed: boolean = false
|
|
) => {
|
|
const maxSize = IS_FORMBRICKS_CLOUD
|
|
? isBiggerFileUploadAllowed
|
|
? MAX_SIZES.big
|
|
: MAX_SIZES.standard
|
|
: Infinity;
|
|
|
|
const postConditions: PresignedPostOptions["Conditions"] = IS_FORMBRICKS_CLOUD
|
|
? [["content-length-range", 0, maxSize]]
|
|
: undefined;
|
|
|
|
try {
|
|
const s3Client = getS3Client();
|
|
const { fields, url } = await createPresignedPost(s3Client, {
|
|
Expires: 10 * 60, // 10 minutes
|
|
Bucket: env.S3_BUCKET_NAME!,
|
|
Key: `${environmentId}/${accessType}/${fileName}`,
|
|
Fields: {
|
|
"Content-Type": contentType,
|
|
},
|
|
Conditions: postConditions,
|
|
});
|
|
|
|
return {
|
|
signedUrl: url,
|
|
presignedFields: fields,
|
|
};
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const putFileToLocalStorage = async (
|
|
fileName: string,
|
|
fileBuffer: Buffer,
|
|
accessType: string,
|
|
environmentId: string,
|
|
rootDir: string,
|
|
isBiggerFileUploadAllowed: boolean = false
|
|
) => {
|
|
try {
|
|
await ensureDirectoryExists(`${rootDir}/${environmentId}/${accessType}`);
|
|
|
|
const uploadPath = `${rootDir}/${environmentId}/${accessType}/${fileName}`;
|
|
|
|
const buffer = Buffer.from(fileBuffer);
|
|
const bufferBytes = buffer.byteLength;
|
|
|
|
const maxSize = IS_FORMBRICKS_CLOUD
|
|
? isBiggerFileUploadAllowed
|
|
? MAX_SIZES.big
|
|
: MAX_SIZES.standard
|
|
: Infinity;
|
|
|
|
if (bufferBytes > maxSize) {
|
|
const err = new Error(`File size exceeds the ${maxSize / (1024 * 1024)} MB limit`);
|
|
err.name = "FileTooLargeError";
|
|
|
|
throw err;
|
|
}
|
|
|
|
await writeFile(uploadPath, buffer);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
// a single service to put file in the storage(local or S3), based on the S3 configuration
|
|
export const putFile = async (
|
|
fileName: string,
|
|
fileBuffer: Buffer,
|
|
accessType: TAccessType,
|
|
environmentId: string
|
|
) => {
|
|
try {
|
|
if (!isS3Configured()) {
|
|
await putFileToLocalStorage(fileName, fileBuffer, accessType, environmentId, UPLOADS_DIR);
|
|
return { success: true, message: "File uploaded" };
|
|
} else {
|
|
const input = {
|
|
Body: fileBuffer,
|
|
Bucket: S3_BUCKET_NAME,
|
|
Key: `${environmentId}/${accessType}/${fileName}`,
|
|
};
|
|
|
|
const command = new PutObjectCommand(input);
|
|
const s3Client = getS3Client();
|
|
await s3Client.send(command);
|
|
return { success: true, message: "File uploaded" };
|
|
}
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const deleteFile = async (environmentId: string, accessType: TAccessType, fileName: string) => {
|
|
if (!isS3Configured()) {
|
|
try {
|
|
await deleteLocalFile(path.join(UPLOADS_DIR, environmentId, accessType, fileName));
|
|
return { success: true, message: "File deleted" };
|
|
} catch (err: any) {
|
|
if (err.code !== "ENOENT") {
|
|
return { success: false, message: err.message ?? "Something went wrong" };
|
|
}
|
|
|
|
return { success: false, message: "File not found", code: 404 };
|
|
}
|
|
}
|
|
|
|
try {
|
|
await deleteS3File(`${environmentId}/${accessType}/${fileName}`);
|
|
return { success: true, message: "File deleted" };
|
|
} catch (err: any) {
|
|
if (err.name === "NoSuchKey") {
|
|
return { success: false, message: "File not found", code: 404 };
|
|
} else {
|
|
return { success: false, message: err.message ?? "Something went wrong" };
|
|
}
|
|
}
|
|
};
|
|
|
|
export const deleteLocalFile = async (filePath: string) => {
|
|
try {
|
|
await unlink(filePath);
|
|
} catch (err: any) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const deleteS3File = async (fileKey: string) => {
|
|
const deleteObjectCommand = new DeleteObjectCommand({
|
|
Bucket: S3_BUCKET_NAME,
|
|
Key: fileKey,
|
|
});
|
|
|
|
try {
|
|
const s3Client = getS3Client();
|
|
await s3Client.send(deleteObjectCommand);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const deleteS3FilesByEnvironmentId = async (environmentId: string) => {
|
|
try {
|
|
// List all objects in the bucket with the prefix of environmentId
|
|
const s3Client = getS3Client();
|
|
const listObjectsOutput = await s3Client.send(
|
|
new ListObjectsCommand({
|
|
Bucket: S3_BUCKET_NAME,
|
|
Prefix: environmentId,
|
|
})
|
|
);
|
|
|
|
if (listObjectsOutput.Contents) {
|
|
const objectsToDelete = listObjectsOutput.Contents.map((obj) => {
|
|
return { Key: obj.Key };
|
|
});
|
|
|
|
if (!objectsToDelete.length) {
|
|
// no objects to delete
|
|
return null;
|
|
}
|
|
|
|
// Delete the objects
|
|
await s3Client.send(
|
|
new DeleteObjectsCommand({
|
|
Bucket: S3_BUCKET_NAME,
|
|
Delete: {
|
|
Objects: objectsToDelete,
|
|
},
|
|
})
|
|
);
|
|
} else {
|
|
// no objects to delete
|
|
return null;
|
|
}
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const deleteLocalFilesByEnvironmentId = async (environmentId: string) => {
|
|
const dirPath = join(UPLOADS_DIR, environmentId);
|
|
|
|
try {
|
|
await ensureDirectoryExists(dirPath);
|
|
await rmdir(dirPath, { recursive: true });
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|