fix: fixes duplicate named file uploads (#1888)

This commit is contained in:
Anshuman Pandey
2024-01-12 22:20:16 +05:30
committed by GitHub
parent ad63be3005
commit 3e9f61792f
7 changed files with 163 additions and 116 deletions

View File

@@ -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>

View File

@@ -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),

View File

@@ -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;

View 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;
};

View File

@@ -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>

View File

@@ -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),

View File

@@ -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>
)}