mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 10:30:00 -06:00
fix: adds deleteFilesByPrefix service
This commit is contained in:
@@ -96,7 +96,6 @@ describe("client.ts", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 credentials are not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,7 +113,6 @@ describe("client.ts", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 credentials are not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,7 +131,6 @@ describe("client.ts", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 credentials are not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,7 +149,6 @@ describe("client.ts", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 credentials are not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -171,7 +167,6 @@ describe("client.ts", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 credentials are not set");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ErrorCode, type Result, type S3CredentialsError, type UnknownError, err, ok } from "../types/error";
|
||||
import { ErrorCode, type Result, type StorageError, err, ok } from "../types/error";
|
||||
import {
|
||||
S3_ACCESS_KEY,
|
||||
S3_BUCKET_NAME,
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
* Create an S3 client from environment variables
|
||||
* @returns A Result containing the S3 client or an error: S3CredentialsError | UnknownError
|
||||
*/
|
||||
export const createS3ClientFromEnv = (): Result<S3Client, S3CredentialsError | UnknownError> => {
|
||||
export const createS3ClientFromEnv = (): Result<S3Client, StorageError> => {
|
||||
try {
|
||||
if (!S3_ACCESS_KEY || !S3_SECRET_KEY || !S3_BUCKET_NAME || !S3_REGION) {
|
||||
logger.error("S3 Client: S3 credentials are not set");
|
||||
return err({
|
||||
code: ErrorCode.S3CredentialsError,
|
||||
message: "S3 credentials are not set",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,10 +32,9 @@ export const createS3ClientFromEnv = (): Result<S3Client, S3CredentialsError | U
|
||||
|
||||
return ok(s3ClientInstance);
|
||||
} catch (error) {
|
||||
logger.error("Error creating S3 client from environment variables", { error });
|
||||
logger.error({ error }, "Error creating S3 client from environment variables");
|
||||
return err({
|
||||
code: ErrorCode.Unknown,
|
||||
message: "Error creating S3 client from environment variables",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { deleteFile, getSignedDownloadUrl, getSignedUploadUrl } from "./service";
|
||||
export { deleteFile, getSignedDownloadUrl, getSignedUploadUrl, deleteFilesByPrefix } from "./service";
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
DeleteObjectsCommand,
|
||||
GetObjectCommand,
|
||||
HeadObjectCommand,
|
||||
ListObjectsCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -6,8 +12,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
// Mock AWS SDK modules
|
||||
vi.mock("@aws-sdk/client-s3", () => ({
|
||||
DeleteObjectCommand: vi.fn(),
|
||||
DeleteObjectsCommand: vi.fn(),
|
||||
GetObjectCommand: vi.fn(),
|
||||
HeadObjectCommand: vi.fn(),
|
||||
ListObjectsCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@aws-sdk/s3-presigned-post", () => ({
|
||||
@@ -26,8 +34,10 @@ vi.mock("./client", () => ({
|
||||
}));
|
||||
|
||||
const mockDeleteObjectCommand = vi.mocked(DeleteObjectCommand);
|
||||
const mockDeleteObjectsCommand = vi.mocked(DeleteObjectsCommand);
|
||||
const mockGetObjectCommand = vi.mocked(GetObjectCommand);
|
||||
const mockHeadObjectCommand = vi.mocked(HeadObjectCommand);
|
||||
const mockListObjectsCommand = vi.mocked(ListObjectsCommand);
|
||||
const mockCreatePresignedPost = vi.mocked(createPresignedPost);
|
||||
const mockGetSignedUrl = vi.mocked(getSignedUrl);
|
||||
|
||||
@@ -95,7 +105,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 bucket name is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,7 +122,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_client_error");
|
||||
expect(result.error.message).toBe("S3 client is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -135,7 +143,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("unknown");
|
||||
expect(result.error.message).toBe("Failed to get signed upload URL");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,7 +207,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 bucket name is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -297,7 +303,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_client_error");
|
||||
expect(result.error.message).toBe("S3 client is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -325,7 +330,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("unknown");
|
||||
expect(result.error.message).toBe("Failed to get signed download URL");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -356,7 +360,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("file_not_found_error");
|
||||
expect(result.error.message).toBe("File not found: non-existent-file.pdf");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -383,7 +386,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("file_not_found_error");
|
||||
expect(result.error.message).toBe("File not found: another-non-existent-file.pdf");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -408,7 +410,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
expect(result.error.message).toBe("S3 bucket name is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -512,7 +513,6 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_client_error");
|
||||
expect(result.error.message).toBe("S3 client is not set");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -535,7 +535,266 @@ describe("service.ts", () => {
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("unknown");
|
||||
expect(result.error.message).toBe("Failed to delete file");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFilesByPrefix", () => {
|
||||
test("should return error if bucket name is not set", async () => {
|
||||
vi.doMock("./constants", () => ({
|
||||
...mockConstants,
|
||||
S3_BUCKET_NAME: undefined,
|
||||
}));
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => ({
|
||||
send: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/images/");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_credentials_error");
|
||||
}
|
||||
});
|
||||
|
||||
test("should return error if s3Client is null", async () => {
|
||||
vi.doMock("./constants", () => mockConstants);
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/images/");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("s3_client_error");
|
||||
}
|
||||
});
|
||||
|
||||
test("should delete multiple files with given prefix", async () => {
|
||||
vi.doMock("./constants", () => ({
|
||||
...mockConstants,
|
||||
}));
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
Contents: [
|
||||
{ Key: "uploads/images/file1.jpg" },
|
||||
{ Key: "uploads/images/file2.png" },
|
||||
{ Key: "uploads/images/subfolder/file3.gif" },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({}), // DeleteObjectsCommand response
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/images/");
|
||||
|
||||
expect(mockListObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Prefix: "uploads/images/",
|
||||
});
|
||||
|
||||
expect(mockDeleteObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Delete: {
|
||||
Objects: [
|
||||
{ Key: "uploads/images/file1.jpg" },
|
||||
{ Key: "uploads/images/file2.png" },
|
||||
{ Key: "uploads/images/subfolder/file3.gif" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockS3Client.send).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle empty result list (no files found)", async () => {
|
||||
vi.doMock("./constants", () => ({
|
||||
...mockConstants,
|
||||
}));
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi.fn().mockResolvedValueOnce({
|
||||
Contents: undefined, // No files found
|
||||
}),
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/non-existent/");
|
||||
|
||||
expect(mockListObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Prefix: "uploads/non-existent/",
|
||||
});
|
||||
|
||||
// Should not call DeleteObjectsCommand when no files found
|
||||
expect(mockDeleteObjectsCommand).not.toHaveBeenCalled();
|
||||
expect(mockS3Client.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle empty Contents array", async () => {
|
||||
vi.doMock("./constants", () => ({
|
||||
...mockConstants,
|
||||
}));
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi.fn().mockResolvedValueOnce({
|
||||
Contents: [], // Empty array
|
||||
}),
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/empty/");
|
||||
|
||||
expect(mockListObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Prefix: "uploads/empty/",
|
||||
});
|
||||
|
||||
// Should not call DeleteObjectsCommand when Contents is empty
|
||||
expect(mockDeleteObjectsCommand).not.toHaveBeenCalled();
|
||||
expect(mockS3Client.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
expect(result.data).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle different prefix patterns", async () => {
|
||||
vi.doMock("./constants", () => ({
|
||||
...mockConstants,
|
||||
}));
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
Contents: [{ Key: "surveys/123/responses/response1.json" }],
|
||||
})
|
||||
.mockResolvedValueOnce({}),
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("surveys/123/responses/");
|
||||
|
||||
expect(mockListObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Prefix: "surveys/123/responses/",
|
||||
});
|
||||
|
||||
expect(mockDeleteObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Delete: {
|
||||
Objects: [{ Key: "surveys/123/responses/response1.json" }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle ListObjectsCommand throwing an error", async () => {
|
||||
vi.doMock("./constants", () => mockConstants);
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi.fn().mockRejectedValueOnce(new Error("AWS ListObjects Error")),
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/test/");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("unknown");
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle DeleteObjectsCommand throwing an error", async () => {
|
||||
vi.doMock("./constants", () => mockConstants);
|
||||
|
||||
const mockS3Client = {
|
||||
send: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
Contents: [{ Key: "test-file.txt" }],
|
||||
})
|
||||
.mockRejectedValueOnce(new Error("AWS Delete Error")), // DeleteObjectsCommand fails
|
||||
};
|
||||
|
||||
vi.doMock("./client", () => ({
|
||||
createS3Client: vi.fn(() => mockS3Client),
|
||||
}));
|
||||
|
||||
const { deleteFilesByPrefix } = await import("./service");
|
||||
|
||||
const result = await deleteFilesByPrefix("uploads/test/");
|
||||
|
||||
expect(mockListObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Prefix: "uploads/test/",
|
||||
});
|
||||
|
||||
expect(mockDeleteObjectsCommand).toHaveBeenCalledWith({
|
||||
Bucket: mockConstants.S3_BUCKET_NAME,
|
||||
Delete: {
|
||||
Objects: [{ Key: "test-file.txt" }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe("unknown");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
DeleteObjectsCommand,
|
||||
GetObjectCommand,
|
||||
HeadObjectCommand,
|
||||
ListObjectsCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import {
|
||||
type PresignedPost,
|
||||
type PresignedPostOptions,
|
||||
@@ -6,16 +12,7 @@ import {
|
||||
} from "@aws-sdk/s3-presigned-post";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import {
|
||||
ErrorCode,
|
||||
type FileNotFoundError,
|
||||
type Result,
|
||||
type S3ClientError,
|
||||
type S3CredentialsError,
|
||||
type UnknownError,
|
||||
err,
|
||||
ok,
|
||||
} from "../types/error";
|
||||
import { ErrorCode, type Result, type StorageError, err, ok } from "../types/error";
|
||||
import { createS3Client } from "./client";
|
||||
import { S3_BUCKET_NAME } from "./constants";
|
||||
|
||||
@@ -27,7 +24,7 @@ const s3Client = createS3Client();
|
||||
* @param contentType - The content type of the file
|
||||
* @param filePath - The path to the file in S3
|
||||
* @param maxSize - The maximum size of the file to upload or undefined if no limit is desired
|
||||
* @returns A Result containing the signed URL and presigned fields or an error: UnknownError | S3CredentialsError | S3ClientError
|
||||
* @returns A Result containing the signed URL and presigned fields or an error: StorageError
|
||||
*/
|
||||
export const getSignedUploadUrl = async (
|
||||
fileName: string,
|
||||
@@ -40,7 +37,7 @@ export const getSignedUploadUrl = async (
|
||||
signedUrl: string;
|
||||
presignedFields: PresignedPost["fields"];
|
||||
},
|
||||
UnknownError | S3CredentialsError | S3ClientError
|
||||
StorageError
|
||||
>
|
||||
> => {
|
||||
try {
|
||||
@@ -48,7 +45,6 @@ export const getSignedUploadUrl = async (
|
||||
logger.error("Failed to get signed upload URL: S3 client is not set");
|
||||
return err({
|
||||
code: ErrorCode.S3ClientError,
|
||||
message: "S3 client is not set",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,7 +56,6 @@ export const getSignedUploadUrl = async (
|
||||
logger.error("Failed to get signed upload URL: S3 bucket name is not set");
|
||||
return err({
|
||||
code: ErrorCode.S3CredentialsError,
|
||||
message: "S3 bucket name is not set",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,36 +75,30 @@ export const getSignedUploadUrl = async (
|
||||
presignedFields: fields,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to get signed upload URL", { error });
|
||||
const unknownError: UnknownError = {
|
||||
code: ErrorCode.Unknown,
|
||||
message: "Failed to get signed upload URL",
|
||||
};
|
||||
logger.error({ error }, "Failed to get signed upload URL");
|
||||
|
||||
return err(unknownError);
|
||||
return err({
|
||||
code: ErrorCode.Unknown,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a signed URL for a file in S3
|
||||
* @param fileKey - The key of the file in S3
|
||||
* @returns A Result containing the signed URL or an error: S3CredentialsError | S3ClientError | FileNotFoundError | UnknownError
|
||||
* @returns A Result containing the signed URL or an error: StorageError
|
||||
*/
|
||||
export const getSignedDownloadUrl = async (
|
||||
fileKey: string
|
||||
): Promise<Result<string, S3CredentialsError | S3ClientError | FileNotFoundError | UnknownError>> => {
|
||||
export const getSignedDownloadUrl = async (fileKey: string): Promise<Result<string, StorageError>> => {
|
||||
try {
|
||||
if (!s3Client) {
|
||||
return err({
|
||||
code: ErrorCode.S3ClientError,
|
||||
message: "S3 client is not set",
|
||||
});
|
||||
}
|
||||
|
||||
if (!S3_BUCKET_NAME) {
|
||||
return err({
|
||||
code: ErrorCode.S3CredentialsError,
|
||||
message: "S3 bucket name is not set",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,19 +110,18 @@ export const getSignedDownloadUrl = async (
|
||||
|
||||
try {
|
||||
await s3Client.send(headObjectCommand);
|
||||
} catch (headError: unknown) {
|
||||
logger.error("Failed to check if file exists", headError);
|
||||
} catch (error: unknown) {
|
||||
logger.error({ error }, "Failed to check if file exists");
|
||||
if (
|
||||
(headError as Error).name === "NotFound" ||
|
||||
(headError as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode === 404
|
||||
(error as Error).name === "NotFound" ||
|
||||
(error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode === 404
|
||||
) {
|
||||
return err({
|
||||
code: ErrorCode.FileNotFoundError,
|
||||
message: `File not found: ${fileKey}`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.warn("HeadObject check failed; proceeding to sign download URL", { error: headError, fileKey });
|
||||
logger.warn({ error, fileKey }, "HeadObject check failed; proceeding to sign download URL");
|
||||
}
|
||||
|
||||
const getObjectCommand = new GetObjectCommand({
|
||||
@@ -143,36 +131,29 @@ export const getSignedDownloadUrl = async (
|
||||
|
||||
return ok(await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 60 * 30 }));
|
||||
} catch (error) {
|
||||
logger.error("Failed to get signed download URL", { error });
|
||||
const unknownError: UnknownError = {
|
||||
logger.error({ error }, "Failed to get signed download URL");
|
||||
return err({
|
||||
code: ErrorCode.Unknown,
|
||||
message: "Failed to get signed download URL",
|
||||
};
|
||||
|
||||
return err(unknownError);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a file from S3
|
||||
* @param fileKey - The key of the file in S3 (e.g. "surveys/123/responses/456/file.pdf")
|
||||
* @returns A Result containing the void or an error: S3CredentialsError | S3ClientError | UnknownError
|
||||
* @returns A Result containing the void or an error: StorageError
|
||||
*/
|
||||
export const deleteFile = async (
|
||||
fileKey: string
|
||||
): Promise<Result<void, S3CredentialsError | S3ClientError | UnknownError>> => {
|
||||
export const deleteFile = async (fileKey: string): Promise<Result<void, StorageError>> => {
|
||||
try {
|
||||
if (!s3Client) {
|
||||
return err({
|
||||
code: ErrorCode.S3ClientError,
|
||||
message: "S3 client is not set",
|
||||
});
|
||||
}
|
||||
|
||||
if (!S3_BUCKET_NAME) {
|
||||
return err({
|
||||
code: ErrorCode.S3CredentialsError,
|
||||
message: "S3 bucket name is not set",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,13 +166,62 @@ export const deleteFile = async (
|
||||
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
logger.error("Failed to delete file", { error });
|
||||
logger.error({ error }, "Failed to delete file");
|
||||
|
||||
const unknownError: UnknownError = {
|
||||
return err({
|
||||
code: ErrorCode.Unknown,
|
||||
message: "Failed to delete file",
|
||||
};
|
||||
|
||||
return err(unknownError);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFilesByPrefix = async (prefix: string): Promise<Result<void, StorageError>> => {
|
||||
try {
|
||||
if (!s3Client) {
|
||||
return err({
|
||||
code: ErrorCode.S3ClientError,
|
||||
});
|
||||
}
|
||||
|
||||
if (!S3_BUCKET_NAME) {
|
||||
return err({
|
||||
code: ErrorCode.S3CredentialsError,
|
||||
});
|
||||
}
|
||||
|
||||
const listObjectsCommand = new ListObjectsCommand({
|
||||
Bucket: S3_BUCKET_NAME,
|
||||
Prefix: prefix,
|
||||
});
|
||||
|
||||
const listObjectsOutput = await s3Client.send(listObjectsCommand);
|
||||
|
||||
if (!listObjectsOutput.Contents) {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
const objectsToDelete = listObjectsOutput.Contents.map((obj) => {
|
||||
return { Key: obj.Key };
|
||||
});
|
||||
|
||||
if (!objectsToDelete.length) {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
const deleteObjectsCommand = new DeleteObjectsCommand({
|
||||
Bucket: S3_BUCKET_NAME,
|
||||
Delete: {
|
||||
Objects: objectsToDelete,
|
||||
},
|
||||
});
|
||||
|
||||
await s3Client.send(deleteObjectsCommand);
|
||||
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to delete files by prefix");
|
||||
|
||||
return err({
|
||||
code: ErrorCode.Unknown,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,22 +26,6 @@ export enum ErrorCode {
|
||||
FileNotFoundError = "file_not_found_error",
|
||||
}
|
||||
|
||||
export interface UnknownError {
|
||||
code: ErrorCode.Unknown;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface S3CredentialsError {
|
||||
code: ErrorCode.S3CredentialsError;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface S3ClientError {
|
||||
code: ErrorCode.S3ClientError;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface FileNotFoundError {
|
||||
code: ErrorCode.FileNotFoundError;
|
||||
message: string;
|
||||
export interface StorageError {
|
||||
code: ErrorCode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user