mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 00:49:42 -06:00
fix: fixes duplicate named file uploads (#1888)
This commit is contained in:
@@ -5,6 +5,7 @@ import { DownloadIcon, FileIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
|
||||
import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
@@ -81,29 +82,31 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
|
||||
|
||||
{Array.isArray(response.value) &&
|
||||
(response.value.length > 0 ? (
|
||||
response.value.map((fileUrl, index) => (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
response.value.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{fileUrl.split("/").pop()}
|
||||
</p>
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download={fileName}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center p-2">
|
||||
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>
|
||||
|
||||
@@ -42,7 +42,7 @@ export class StorageAPI {
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields } = data;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StorageAPI {
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(file.name),
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-Survey-ID": surveyId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} 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 mime from "mime";
|
||||
import { unstable_cache } from "next/cache";
|
||||
@@ -61,6 +62,7 @@ type TGetSignedUrlResponse =
|
||||
| { signedUrl: string; fileUrl: string; presignedFields: Object }
|
||||
| {
|
||||
signedUrl: string;
|
||||
updatedFileName: string;
|
||||
fileUrl: string;
|
||||
signingData: {
|
||||
signature: string;
|
||||
@@ -171,10 +173,21 @@ export const getUploadSignedUrl = async (
|
||||
accessType: TAccessType,
|
||||
plan: "free" | "pro" = "free"
|
||||
): 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 (!IS_S3_CONFIGURED) {
|
||||
try {
|
||||
const { signature, timestamp, uuid } = generateLocalSignedUrl(fileName, environmentId, fileType);
|
||||
const { signature, timestamp, uuid } = generateLocalSignedUrl(updatedFileName, environmentId, fileType);
|
||||
|
||||
return {
|
||||
signedUrl:
|
||||
@@ -186,7 +199,8 @@ export const getUploadSignedUrl = async (
|
||||
timestamp,
|
||||
uuid,
|
||||
},
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
|
||||
updatedFileName,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
@@ -195,7 +209,7 @@ export const getUploadSignedUrl = async (
|
||||
|
||||
try {
|
||||
const { presignedFields, signedUrl } = await getS3UploadSignedUrl(
|
||||
fileName,
|
||||
updatedFileName,
|
||||
fileType,
|
||||
accessType,
|
||||
environmentId,
|
||||
@@ -206,7 +220,7 @@ export const getUploadSignedUrl = async (
|
||||
return {
|
||||
signedUrl,
|
||||
presignedFields,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
|
||||
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
14
packages/lib/storage/utils.ts
Normal file
14
packages/lib/storage/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const getOriginalFileNameFromUrl = (fileURL: string) => {
|
||||
const fileNameFromURL = new URL(fileURL).pathname.split("/").pop();
|
||||
const fileExt = fileNameFromURL?.split(".").pop();
|
||||
const originalFileName = fileNameFromURL?.split("--fid--")[0];
|
||||
const fileId = fileNameFromURL?.split("--fid--")[1];
|
||||
|
||||
if (!fileId) {
|
||||
const fileName = originalFileName ? decodeURIComponent(originalFileName || "") : "";
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : "";
|
||||
return fileName;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from "preact/hooks";
|
||||
import { JSXInternal } from "preact/src/jsx";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
|
||||
@@ -204,45 +205,47 @@ export default function FileInput({
|
||||
<div className="items-left relative mt-3 flex w-full cursor-pointer flex-col justify-center rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800">
|
||||
<div>
|
||||
{fileUrls &&
|
||||
fileUrls?.map((file, index) => (
|
||||
<div key={index} className="relative m-2 rounded-md bg-slate-200">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
|
||||
fileUrls?.map((file, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(file);
|
||||
|
||||
return (
|
||||
<div key={index} className="relative m-2 rounded-md bg-slate-200">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 26 26"
|
||||
strokeWidth={1}
|
||||
stroke="currentColor"
|
||||
className="h-5 text-slate-700 hover:text-slate-900"
|
||||
onClick={(e) => handleDeleteFile(index, e)}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
viewBox="0 0 26 26"
|
||||
strokeWidth={1}
|
||||
stroke="currentColor"
|
||||
className="h-5 text-slate-700 hover:text-slate-900"
|
||||
onClick={(e) => handleDeleteFile(index, e)}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file"
|
||||
className="h-6 text-slate-500">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-file"
|
||||
className="h-6 text-slate-500">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
{decodeURIComponent(file).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -46,7 +46,7 @@ const uploadFile = async (
|
||||
const json = await response.json();
|
||||
|
||||
const { data } = json;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields } = data;
|
||||
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
|
||||
@@ -55,7 +55,7 @@ const uploadFile = async (
|
||||
|
||||
requestHeaders = {
|
||||
"X-File-Type": file.type,
|
||||
"X-File-Name": encodeURIComponent(file.name),
|
||||
"X-File-Name": encodeURIComponent(updatedFileName),
|
||||
"X-Environment-ID": environmentId ?? "",
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": String(timestamp),
|
||||
|
||||
@@ -2,11 +2,49 @@
|
||||
|
||||
import { FileIcon } from "lucide-react";
|
||||
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
|
||||
interface FileUploadResponseProps {
|
||||
selected: string | number | string[];
|
||||
}
|
||||
|
||||
export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
|
||||
const SingleFileResponse = () => {
|
||||
const selectedFile = selected as string;
|
||||
const fileName = getOriginalFileNameFromUrl(selectedFile);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-300">
|
||||
<a href={selected as string} download={fileName}>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{selected === "selected" ? (
|
||||
@@ -14,65 +52,40 @@ export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
|
||||
) : (
|
||||
<div className="col-span-2 grid md:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.isArray(selected) ? (
|
||||
selected.map((fileUrl, index) => (
|
||||
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
|
||||
<a href={fileUrl as string} key={index} download>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
selected.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
|
||||
<a href={fileUrl as string} key={index} download={fileName}>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{decodeURIComponent(fileUrl).split("/").pop()}
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="relative m-2 rounded-lg bg-slate-300">
|
||||
<a href={selected as string} download>
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SingleFileResponse />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user