mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-10 19:29:50 -05:00
Merge branch 'main' of https://github.com/Dhruwang/formbricks into issues/SingleUseLinkSurveys
This commit is contained in:
12
README.md
12
README.md
@@ -117,12 +117,18 @@ We are very happy if you are interested in contributing to Formbricks 🤗
|
||||
|
||||
Here are a few options:
|
||||
|
||||
- Star this repo
|
||||
- Create issues every time you feel something is missing or goes wrong
|
||||
- Upvote issues with 👍 reaction so we know what's the demand for particular issue to prioritize it within roadmap
|
||||
- Star this repo.
|
||||
- Create issues every time you feel something is missing or goes wrong.
|
||||
- Upvote issues with 👍 reaction so we know what's the demand for a particular issue to prioritize it within the roadmap.
|
||||
|
||||
Please check out [our contribution guide](https://formbricks.com/docs/contributing/introduction) and our [list of open issues](https://github.com/formbricks/formbricks/issues) for more information.
|
||||
|
||||
## All Thanks To Our Contributors
|
||||
|
||||
<a href="https://github.com/formbricks/formbricks/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=formbricks/formbricks" />
|
||||
</a>
|
||||
|
||||
## 📆 Contact us
|
||||
|
||||
Let's have a chat about your survey needs and get you started.
|
||||
|
||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -40,3 +40,6 @@ next-env.d.ts
|
||||
|
||||
# Google Sheets Token File
|
||||
token.json
|
||||
|
||||
# Local Uploads
|
||||
uploads/
|
||||
@@ -61,6 +61,7 @@ export default function ResponseFeed({
|
||||
<SurveyStatusIndicator
|
||||
status={response.survey.status}
|
||||
environment={environment}
|
||||
type={response.survey.type}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,12 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
<div className="flex items-center">
|
||||
{survey.status !== "draft" && (
|
||||
<>
|
||||
<SurveyStatusIndicator status={survey.status} tooltip environment={environment} />
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
tooltip
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{survey.status === "draft" && (
|
||||
|
||||
@@ -101,7 +101,11 @@ const SummaryHeader = ({
|
||||
disabled={isStatusChangeDisabled}
|
||||
style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
<span className="ml-1 text-sm text-slate-700">
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
|
||||
95
apps/web/app/api/v1/client/storage/route.ts
Normal file
95
apps/web/app/api/v1/client/storage/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { UPLOADS_DIR, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { putFileToLocalStorage, putFileToS3 } from "@formbricks/lib/storage/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
const accessType = "private"; // private files are only accessible by the user who has access to the environment
|
||||
const { fileName, contentType, fileBuffer, surveyId } = await req.json();
|
||||
|
||||
if (!surveyId) {
|
||||
return responses.badRequestResponse("surveyId ID is required");
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
return responses.badRequestResponse("fileName is required");
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
return responses.badRequestResponse("contentType is required");
|
||||
}
|
||||
|
||||
if (!fileBuffer) {
|
||||
return responses.badRequestResponse("no file provided, fileBuffer is required");
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", surveyId);
|
||||
}
|
||||
|
||||
const { environmentId } = survey;
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
|
||||
}
|
||||
|
||||
const { plan } = team;
|
||||
|
||||
const buffer = Buffer.from(fileBuffer);
|
||||
|
||||
const bufferBytes = buffer.byteLength;
|
||||
const bufferKB = bufferBytes / 1024;
|
||||
|
||||
if (plan === "free" && bufferKB > 10240) {
|
||||
return responses.badRequestResponse("Maximum file size for free plan is 10MB, please upgrade your plan");
|
||||
}
|
||||
|
||||
if (plan === "pro" && bufferKB > 1024 * 1024) {
|
||||
return responses.badRequestResponse("Maximum file size for pro plan is 1GB");
|
||||
}
|
||||
|
||||
const uploadPrivateFile = async () => {
|
||||
// if s3 is not configured, we'll upload to a local folder named uploads
|
||||
|
||||
if (!env.AWS_ACCESS_KEY || !env.AWS_SECRET_KEY || !env.S3_REGION || !env.S3_BUCKET_NAME) {
|
||||
try {
|
||||
await putFileToLocalStorage(fileName, fileBuffer, accessType, environmentId, UPLOADS_DIR);
|
||||
|
||||
return responses.successResponse({
|
||||
uploaded: true,
|
||||
url: `${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === "FileTooLargeError") {
|
||||
return responses.badRequestResponse(err.message);
|
||||
}
|
||||
|
||||
return responses.internalServerErrorResponse(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await putFileToS3(fileName, contentType, fileBuffer, accessType, environmentId);
|
||||
|
||||
return responses.successResponse({
|
||||
uploaded: true,
|
||||
url: `${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === "FileTooLargeError") {
|
||||
return responses.badRequestResponse(err.message);
|
||||
}
|
||||
|
||||
return responses.internalServerErrorResponse(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return await uploadPrivateFile();
|
||||
}
|
||||
104
apps/web/app/api/v1/management/storage/route.ts
Normal file
104
apps/web/app/api/v1/management/storage/route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/env.mjs";
|
||||
import { putFileToLocalStorage, putFileToS3 } from "@formbricks/lib/storage/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { UPLOADS_DIR, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
|
||||
// api endpoint for uploading public files
|
||||
// uploaded files will be public, anyone can access the file
|
||||
// uploading public files requires authentication
|
||||
// use this to upload files for a specific resource, e.g. a user profile picture or a survey
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
const accessType = "public"; // public files are accessible by anyone
|
||||
const { fileName, contentType, environmentId, fileBuffer, allowedFileExtensions } = await req.json();
|
||||
|
||||
if (!fileName) {
|
||||
return responses.badRequestResponse("fileName is required");
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
return responses.badRequestResponse("contentType is required");
|
||||
}
|
||||
|
||||
if (!fileBuffer) {
|
||||
return responses.badRequestResponse("no file provided, fileBuffer is required");
|
||||
}
|
||||
|
||||
if (!environmentId) {
|
||||
return responses.badRequestResponse("environmentId is required");
|
||||
}
|
||||
|
||||
if (allowedFileExtensions?.length) {
|
||||
const fileExtension = fileName.split(".").pop();
|
||||
if (!fileExtension || !allowedFileExtensions.includes(fileExtension)) {
|
||||
return responses.badRequestResponse(
|
||||
`File extension is not allowed, allowed extensions are: ${allowedFileExtensions.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// auth and upload private file
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session || !session.user) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
return await uploadPublicFile(fileName, fileBuffer, accessType, environmentId, contentType);
|
||||
}
|
||||
|
||||
const uploadPublicFile = async (
|
||||
fileName: string,
|
||||
fileBuffer: Buffer,
|
||||
accessType: "public" | "private",
|
||||
environmentId,
|
||||
contentType?: string
|
||||
) => {
|
||||
// if s3 is not configured, we'll upload to a local folder named uploads
|
||||
|
||||
if (!env.AWS_ACCESS_KEY || !env.AWS_SECRET_KEY || !env.S3_REGION || !env.S3_BUCKET_NAME) {
|
||||
try {
|
||||
await putFileToLocalStorage(fileName, fileBuffer, accessType, environmentId, UPLOADS_DIR, true);
|
||||
|
||||
return responses.successResponse({
|
||||
uploaded: true,
|
||||
url: `${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === "FileTooLargeError") {
|
||||
return responses.badRequestResponse(err.message);
|
||||
}
|
||||
|
||||
return responses.internalServerErrorResponse("Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!contentType) {
|
||||
return responses.badRequestResponse("contentType is required");
|
||||
}
|
||||
|
||||
await putFileToS3(fileName, contentType, fileBuffer, accessType, environmentId, true);
|
||||
|
||||
return responses.successResponse({
|
||||
uploaded: true,
|
||||
url: `${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === "FileTooLargeError") {
|
||||
return responses.badRequestResponse(err.message);
|
||||
}
|
||||
|
||||
return responses.internalServerErrorResponse("Internal server error");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { UPLOADS_DIR } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getFileFromLocalStorage, getFileFromS3 } from "@formbricks/lib/storage/service";
|
||||
import { ZStorageRetrievalParams } from "@formbricks/types/v1/storage";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextRequest } from "next/server";
|
||||
import path from "path";
|
||||
|
||||
export async function GET(
|
||||
_: NextRequest,
|
||||
{ params }: { params: { environmentId: string; accessType: string; fileName: string } }
|
||||
) {
|
||||
const paramValidation = ZStorageRetrievalParams.safeParse(params);
|
||||
|
||||
if (!paramValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(paramValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, accessType, fileName } = params;
|
||||
|
||||
const getFile = async () => {
|
||||
if (!env.AWS_ACCESS_KEY || !env.AWS_SECRET_KEY || !env.S3_REGION || !env.S3_BUCKET_NAME) {
|
||||
try {
|
||||
const { fileBuffer, metaData } = await getFileFromLocalStorage(
|
||||
path.join(UPLOADS_DIR, environmentId, accessType, fileName)
|
||||
);
|
||||
|
||||
return new Response(fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": metaData.contentType,
|
||||
"Content-Disposition": "inline",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { fileBuffer, metaData } = await getFileFromS3(fileName);
|
||||
|
||||
return new Response(fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": metaData.contentType,
|
||||
"Content-Disposition": "inline",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === "NoSuchKey") {
|
||||
return responses.notFoundResponse("File not found", fileName);
|
||||
} else {
|
||||
return responses.internalServerErrorResponse("Internal server error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (accessType === "public") {
|
||||
return await getFile();
|
||||
}
|
||||
|
||||
// auth and download private file
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session || !session.user) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
return await getFile();
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function SurveyStatusDropdown({
|
||||
<>
|
||||
{survey.status === "draft" ? (
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} type={survey.type} />
|
||||
{survey.status === "draft" && <p className="text-sm italic text-slate-600">Draft</p>}
|
||||
</div>
|
||||
) : (
|
||||
@@ -69,7 +69,11 @@ export default function SurveyStatusDropdown({
|
||||
<SelectTrigger className="w-[170px] bg-white py-6 md:w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
<span className="ml-2 text-sm text-slate-700">
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
|
||||
@@ -2,16 +2,32 @@
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { ArchiveBoxIcon, CheckIcon, PauseIcon } from "@heroicons/react/24/solid";
|
||||
import { ArchiveBoxIcon, CheckIcon, PauseIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface SurveyStatusIndicatorProps {
|
||||
status: string;
|
||||
tooltip?: boolean;
|
||||
environment: TEnvironment;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function SurveyStatusIndicator({ status, tooltip, environment }: SurveyStatusIndicatorProps) {
|
||||
if (!environment.widgetSetupCompleted) return null;
|
||||
export default function SurveyStatusIndicator({
|
||||
status,
|
||||
tooltip,
|
||||
environment,
|
||||
type,
|
||||
}: SurveyStatusIndicatorProps) {
|
||||
if (!environment.widgetSetupCompleted) {
|
||||
if (type === "web") {
|
||||
return (
|
||||
<div>
|
||||
<ExclamationTriangleIcon className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -52,6 +52,10 @@ export const env = createEnv({
|
||||
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
|
||||
AWS_ACCESS_KEY: z.string().optional(),
|
||||
AWS_SECRET_KEY: z.string().optional(),
|
||||
S3_REGION: z.string().optional(),
|
||||
S3_BUCKET_NAME: z.string().optional(),
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -108,6 +112,10 @@ export const env = createEnv({
|
||||
GOOGLE_SHEETS_CLIENT_ID: process.env.GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: process.env.GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL: process.env.GOOGLE_SHEETS_REDIRECT_URL,
|
||||
AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY,
|
||||
AWS_SECRET_KEY: process.env.AWS_SECRET_KEY,
|
||||
S3_REGION: process.env.S3_REGION,
|
||||
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID,
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.418.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.418.0",
|
||||
"@formbricks/api": "workspace:*",
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/ee": "workspace:*",
|
||||
@@ -34,6 +36,7 @@
|
||||
"googleapis": "^126.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^3.0.0",
|
||||
"lucide-react": "^0.284.0",
|
||||
"next": "13.5.4",
|
||||
"next-auth": "^4.23.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
@@ -1,4 +1,5 @@
|
||||
import "server-only";
|
||||
import path from "path";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const RESPONSES_LIMIT_FREE = 100;
|
||||
@@ -54,3 +55,6 @@ export const MAIL_FROM = env.MAIL_FROM;
|
||||
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
|
||||
export const NEXTAUTH_URL = env.NEXTAUTH_URL;
|
||||
export const PEOPLE_PER_PAGE = 50;
|
||||
|
||||
// Storage constants
|
||||
export const UPLOADS_DIR = path.resolve("./uploads");
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { cache } from "react";
|
||||
import { getPerson, transformPrismaPerson } from "../person/service";
|
||||
@@ -19,6 +18,7 @@ import { captureTelemetry } from "../telemetry";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const responseSelection = {
|
||||
id: true,
|
||||
|
||||
157
packages/lib/storage/service.ts
Normal file
157
packages/lib/storage/service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { access, mkdir, writeFile, readFile } from "fs/promises";
|
||||
import mime from "mime";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
// global variables
|
||||
|
||||
const AWS_BUCKET_NAME = env.S3_BUCKET_NAME!;
|
||||
const AWS_REGION = env.S3_REGION!;
|
||||
const AWS_ACCESS_KEY = env.AWS_ACCESS_KEY!;
|
||||
const AWS_SECRET_KEY = env.AWS_SECRET_KEY!;
|
||||
|
||||
// S3Client Singleton
|
||||
|
||||
const s3Client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: AWS_ACCESS_KEY,
|
||||
secretAccessKey: AWS_SECRET_KEY!,
|
||||
},
|
||||
region: AWS_REGION!,
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
export const getFileFromS3 = async (fileKey: string): Promise<TGetFileResponse> => {
|
||||
const getObjectCommand = new GetObjectCommand({
|
||||
Bucket: AWS_BUCKET_NAME,
|
||||
Key: fileKey,
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await s3Client.send(getObjectCommand);
|
||||
const byteArray = await data.Body?.transformToByteArray();
|
||||
const buffer = Buffer.from(byteArray as Uint8Array);
|
||||
|
||||
return {
|
||||
fileBuffer: buffer,
|
||||
metaData: {
|
||||
contentType: data.ContentType ?? "",
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileFromLocalStorage = async (filePath: string): Promise<TGetFileResponse> => {
|
||||
try {
|
||||
const file = await readFile(filePath);
|
||||
let contentType = "";
|
||||
|
||||
try {
|
||||
contentType = mime.getType(filePath) ?? "";
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return {
|
||||
fileBuffer: file,
|
||||
metaData: {
|
||||
contentType: contentType ?? "",
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const putFileToS3 = async (
|
||||
fileName: string,
|
||||
contentType: string,
|
||||
fileBuffer: Buffer,
|
||||
accessType: string,
|
||||
environmentId: string,
|
||||
isPublic: boolean = false
|
||||
) => {
|
||||
try {
|
||||
const buffer = Buffer.from(fileBuffer);
|
||||
|
||||
if (isPublic) {
|
||||
//check the size of buffer and if it is greater than 10MB, return error
|
||||
|
||||
const bufferBytes = buffer.byteLength;
|
||||
const bufferKB = bufferBytes / 1024;
|
||||
|
||||
if (bufferKB > 10240) {
|
||||
const err = new Error("File size is greater than 10MB");
|
||||
err.name = "FileTooLargeError";
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const putObjectCommand = new PutObjectCommand({
|
||||
Bucket: AWS_BUCKET_NAME,
|
||||
Key: `${environmentId}/${accessType}/${fileName}`,
|
||||
Body: buffer,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await s3Client.send(putObjectCommand);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const putFileToLocalStorage = async (
|
||||
fileName: string,
|
||||
fileBuffer: Buffer,
|
||||
accessType: string,
|
||||
environmentId: string,
|
||||
rootDir: string,
|
||||
isPublic: boolean = false
|
||||
) => {
|
||||
try {
|
||||
await ensureDirectoryExists(`${rootDir}/${environmentId}/${accessType}`);
|
||||
|
||||
const uploadPath = `${rootDir}/${environmentId}/${accessType}/${fileName}`;
|
||||
|
||||
const buffer = Buffer.from(fileBuffer);
|
||||
|
||||
if (isPublic) {
|
||||
//check the size of buffer and if it is greater than 10MB, return error
|
||||
|
||||
const bufferBytes = buffer.byteLength;
|
||||
const bufferKB = bufferBytes / 1024;
|
||||
|
||||
if (bufferKB > 10240) {
|
||||
const err = new Error("File size is greater than 10MB");
|
||||
err.name = "FileTooLargeError";
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(uploadPath, buffer);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
9
packages/types/v1/storage.ts
Normal file
9
packages/types/v1/storage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZAccessType = z.enum(["public", "private"]);
|
||||
|
||||
export const ZStorageRetrievalParams = z.object({
|
||||
fileName: z.string(),
|
||||
environmentId: z.string().cuid(),
|
||||
accessType: ZAccessType,
|
||||
});
|
||||
1387
pnpm-lock.yaml
generated
1387
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,11 @@
|
||||
"STRIPE_WEBHOOK_SECRET",
|
||||
"TELEMETRY_DISABLED",
|
||||
"VERCEL_URL",
|
||||
"WEBAPP_URL"
|
||||
"WEBAPP_URL",
|
||||
"AWS_ACCESS_KEY",
|
||||
"AWS_SECRET_KEY",
|
||||
"S3_REGION",
|
||||
"S3_BUCKET_NAME"
|
||||
]
|
||||
},
|
||||
"post-install": {
|
||||
|
||||
Reference in New Issue
Block a user