mirror of
https://github.com/documenso/documenso.git
synced 2026-06-23 04:42:09 +10:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f5903760 | |||
| f153a7c437 | |||
| f9cb8d84ed | |||
| cc67454513 | |||
| 665a0d0ea0 | |||
| 02ff8df09b | |||
| d4bee4aed1 | |||
| f4cfa71379 | |||
| b1763e422c | |||
| 64e0695811 | |||
| 8a4205d808 | |||
| 74db3d7a1c | |||
| 3976531045 | |||
| 7a499270be |
@@ -4,6 +4,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
@@ -115,6 +116,12 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => t`You have reached the limit of the number of files per envelope.`)
|
||||
.with('CONVERSION_SERVICE_UNAVAILABLE', () => t`File conversion is not available. Please upload a PDF file.`)
|
||||
.with('CONVERSION_FAILED', () => t`Failed to convert file. Please try uploading a PDF instead.`)
|
||||
.with(
|
||||
'UNSUPPORTED_FILE_TYPE',
|
||||
() => t`This file type is not supported. Please upload a PDF, Word document, or image.`,
|
||||
)
|
||||
.otherwise(() => t`An error occurred during upload.`);
|
||||
|
||||
toast({
|
||||
@@ -158,9 +165,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
||||
});
|
||||
};
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||
multiple: true,
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
maxFiles: maximumEnvelopeItemCount,
|
||||
@@ -183,7 +188,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
||||
</h2>
|
||||
|
||||
<p className="mt-4 text-md text-muted-foreground">
|
||||
<Trans>Drag and drop your PDF file here</Trans>
|
||||
<Trans>Drag and drop your document here</Trans>
|
||||
</p>
|
||||
|
||||
{isUploadDisabled && IS_BILLING_ENABLED() && (
|
||||
|
||||
@@ -114,6 +114,18 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
'UNSUPPORTED_FILE_TYPE',
|
||||
() => t`This file type is not supported. Please upload a PDF, DOCX, JPEG, or PNG file.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => t`File conversion is temporarily unavailable. Please upload a PDF file instead.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => t`Failed to convert the file to PDF. Please try again or upload a PDF file instead.`,
|
||||
)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -24,6 +25,8 @@ type DocumentDataInput = {
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
initialData: string;
|
||||
originalData?: string | null;
|
||||
originalMimeType?: string | null;
|
||||
};
|
||||
|
||||
type EnvelopeForPendingDownload = {
|
||||
@@ -84,7 +87,19 @@ const handleStaticFileRequest = async ({
|
||||
isDownload,
|
||||
context: c,
|
||||
}: StaticFileRequestOptions) => {
|
||||
const documentDataToUse = version === 'signed' ? documentData.data : documentData.initialData;
|
||||
const shouldServeOriginalSourceFile =
|
||||
version === 'original' &&
|
||||
documentData.originalData &&
|
||||
documentData.originalMimeType &&
|
||||
documentData.originalMimeType !== 'application/pdf';
|
||||
|
||||
const documentDataToUse = shouldServeOriginalSourceFile
|
||||
? documentData.originalData!
|
||||
: version === 'signed'
|
||||
? documentData.data
|
||||
: documentData.initialData;
|
||||
|
||||
const contentType = shouldServeOriginalSourceFile ? documentData.originalMimeType! : 'application/pdf';
|
||||
|
||||
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||
|
||||
@@ -105,7 +120,7 @@ const handleStaticFileRequest = async ({
|
||||
return c.json({ error: 'File not found' }, 404);
|
||||
}
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header('Content-Type', contentType);
|
||||
c.header('ETag', etag);
|
||||
|
||||
if (!isDownload) {
|
||||
@@ -117,10 +132,17 @@ const handleStaticFileRequest = async ({
|
||||
}
|
||||
|
||||
if (isDownload) {
|
||||
// Generate filename following the pattern from envelope-download-dialog.tsx
|
||||
const baseTitle = title.replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
const filename = `${baseTitle}${suffix}`;
|
||||
const baseTitle = title.replace(/\.[^/.]+$/, '');
|
||||
|
||||
let filename: string;
|
||||
if (version === 'signed') {
|
||||
filename = `${baseTitle}_signed.pdf`;
|
||||
} else if (shouldServeOriginalSourceFile) {
|
||||
const extension = getFileExtensionForMimeType(documentData.originalMimeType!);
|
||||
filename = `${baseTitle}${extension}`;
|
||||
} else {
|
||||
filename = `${baseTitle}.pdf`;
|
||||
}
|
||||
|
||||
c.header('Content-Disposition', contentDisposition(filename));
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
return c.json({ error: 'File too large' }, 400);
|
||||
}
|
||||
|
||||
const result = await putNormalizedPdfFileServerSide(file);
|
||||
const result = await putNormalizedPdfFileServerSide({ file });
|
||||
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 809 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -48,6 +48,15 @@ services:
|
||||
entrypoint: sh
|
||||
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
container_name: gotenberg
|
||||
ports:
|
||||
- 3001:3000
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--api-timeout=30s'
|
||||
|
||||
volumes:
|
||||
minio:
|
||||
redis:
|
||||
|
||||
@@ -814,9 +814,11 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
});
|
||||
|
||||
const newDocumentData = await putNormalizedPdfFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
file: {
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.envelopeItem.update({
|
||||
|
||||
@@ -13,7 +13,7 @@ type DownloadPDFProps = {
|
||||
/**
|
||||
* Specifies which version of the document to download.
|
||||
* 'signed': Downloads the signed version (default).
|
||||
* 'original': Downloads the original version.
|
||||
* 'original': Downloads the original version (may be DOCX, PNG, JPEG if converted).
|
||||
* 'pending': Downloads the original document with currently-inserted fields burned in.
|
||||
* Only valid while the envelope is in PENDING status. Not supported via
|
||||
* recipient token.
|
||||
@@ -21,6 +21,29 @@ type DownloadPDFProps = {
|
||||
version?: DocumentVersion;
|
||||
};
|
||||
|
||||
const getFilenameFromContentDisposition = (header: string | null): string | null => {
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filenameStarMatch = header.match(/filename\*=(?:UTF-8''|utf-8'')([^;]+)/i);
|
||||
if (filenameStarMatch) {
|
||||
return decodeURIComponent(filenameStarMatch[1]);
|
||||
}
|
||||
|
||||
const filenameMatch = header.match(/filename="([^"]+)"/);
|
||||
if (filenameMatch) {
|
||||
return filenameMatch[1];
|
||||
}
|
||||
|
||||
const filenameNoQuotesMatch = header.match(/filename=([^;\s]+)/);
|
||||
if (filenameNoQuotesMatch) {
|
||||
return filenameNoQuotesMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const versionToFilenameSuffix = (version: DocumentVersion): string => {
|
||||
switch (version) {
|
||||
case 'signed':
|
||||
@@ -40,12 +63,22 @@ export const downloadPDF = async ({ envelopeItem, token, fileName, version = 'si
|
||||
version,
|
||||
});
|
||||
|
||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||
const response = await fetch(downloadUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const serverFilename = getFilenameFromContentDisposition(contentDisposition);
|
||||
|
||||
let filename: string;
|
||||
if (serverFilename) {
|
||||
filename = serverFilename;
|
||||
} else {
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.[^/.]+$/, '');
|
||||
filename = `${baseTitle}${versionToFilenameSuffix(version)}`;
|
||||
}
|
||||
|
||||
downloadFile({
|
||||
filename: `${baseTitle}${versionToFilenameSuffix(version)}`,
|
||||
filename,
|
||||
data: blob,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
export const ALLOWED_UPLOAD_MIME_TYPES: Record<string, string[]> = {
|
||||
'application/pdf': ['.pdf'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||
'image/jpeg': ['.jpg', '.jpeg'],
|
||||
'image/png': ['.png'],
|
||||
};
|
||||
|
||||
export const isAllowedMimeType = (mimeType: string): boolean =>
|
||||
mimeType in ALLOWED_UPLOAD_MIME_TYPES;
|
||||
|
||||
export const getGotenbergUrl = (): string | undefined => env('NEXT_PRIVATE_GOTENBERG_URL');
|
||||
|
||||
export const getGotenbergTimeout = (): number => {
|
||||
const timeout = env('NEXT_PRIVATE_GOTENBERG_TIMEOUT');
|
||||
return timeout ? parseInt(timeout, 10) : 30_000;
|
||||
};
|
||||
|
||||
export const getFileExtensionForMimeType = (mimeType: string): string => {
|
||||
const extensions = ALLOWED_UPLOAD_MIME_TYPES[mimeType];
|
||||
return extensions?.[0] ?? '.pdf';
|
||||
};
|
||||
@@ -471,7 +471,7 @@ const decorateAndSignPdf = async ({
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBytes),
|
||||
},
|
||||
envelopeItem.documentData.initialData,
|
||||
{ initialData: envelopeItem.documentData.initialData },
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,14 +11,24 @@ export type CreateDocumentDataOptions = {
|
||||
* If not provided, the current data will be used.
|
||||
*/
|
||||
initialData?: string;
|
||||
originalData?: string;
|
||||
originalMimeType?: string;
|
||||
};
|
||||
|
||||
export const createDocumentData = async ({ type, data, initialData }: CreateDocumentDataOptions) => {
|
||||
export const createDocumentData = async ({
|
||||
type,
|
||||
data,
|
||||
initialData,
|
||||
originalData,
|
||||
originalMimeType,
|
||||
}: CreateDocumentDataOptions) => {
|
||||
return await prisma.documentData.create({
|
||||
data: {
|
||||
type,
|
||||
data,
|
||||
initialData: initialData || data,
|
||||
originalData,
|
||||
originalMimeType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
originalData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -74,6 +74,8 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
originalData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -336,9 +338,11 @@ const injectFormValuesIntoDocument = async (
|
||||
}
|
||||
|
||||
const newDocumentData = await putNormalizedPdfFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
file: {
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.envelopeItem.update({
|
||||
|
||||
@@ -49,6 +49,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId, overrides }: Dupli
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { isAllowedMimeType } from '../../constants/upload';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { convertFileToPdfViaGotenberg } from '../gotenberg/gotenberg-client';
|
||||
|
||||
type FileInput = {
|
||||
name: string;
|
||||
type: string;
|
||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
export type ConvertToPdfResult = {
|
||||
pdfBuffer: Buffer;
|
||||
originalBuffer: Buffer | null;
|
||||
originalMimeType: string;
|
||||
};
|
||||
|
||||
export const convertToPdfIfNeeded = async (file: FileInput): Promise<ConvertToPdfResult> => {
|
||||
const originalMimeType = file.type;
|
||||
|
||||
if (!isAllowedMimeType(originalMimeType)) {
|
||||
throw new AppError('UNSUPPORTED_FILE_TYPE', {
|
||||
message: `File type '${originalMimeType}' is not supported`,
|
||||
userMessage: 'This file type is not supported. Please upload a PDF, DOCX, JPEG, or PNG file.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
if (originalMimeType === 'application/pdf') {
|
||||
return {
|
||||
pdfBuffer: buffer,
|
||||
originalBuffer: null,
|
||||
originalMimeType,
|
||||
};
|
||||
}
|
||||
|
||||
const pdfBuffer = await convertFileToPdfViaGotenberg({
|
||||
file: buffer,
|
||||
filename: file.name,
|
||||
mimeType: originalMimeType,
|
||||
});
|
||||
|
||||
return {
|
||||
pdfBuffer,
|
||||
originalBuffer: buffer,
|
||||
originalMimeType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { getGotenbergTimeout, getGotenbergUrl } from '../../constants/upload';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
|
||||
export type ConvertFileToPdfOptions = {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
export const convertFileToPdfViaGotenberg = async ({
|
||||
file,
|
||||
filename,
|
||||
mimeType,
|
||||
}: ConvertFileToPdfOptions): Promise<Buffer> => {
|
||||
const gotenbergUrl = getGotenbergUrl();
|
||||
|
||||
if (!gotenbergUrl) {
|
||||
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
|
||||
message: 'Gotenberg URL is not configured',
|
||||
userMessage: 'File conversion service is not available. Please upload a PDF file instead.',
|
||||
statusCode: 503,
|
||||
});
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([file], { type: mimeType });
|
||||
formData.append('files', blob, filename);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), getGotenbergTimeout());
|
||||
|
||||
try {
|
||||
const response = await fetch(`${gotenbergUrl}/forms/libreoffice/convert`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
|
||||
console.error(`Gotenberg conversion failed: ${response.status} - ${errorText}`);
|
||||
|
||||
throw new AppError('CONVERSION_FAILED', {
|
||||
message: `Gotenberg returned status ${response.status}: ${errorText}`,
|
||||
userMessage:
|
||||
'Failed to convert the file to PDF. Please try again or upload a PDF file instead.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
|
||||
message: 'Gotenberg request timed out',
|
||||
userMessage:
|
||||
'File conversion timed out. Please try again with a smaller file or upload a PDF instead.',
|
||||
statusCode: 503,
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Gotenberg conversion error:', error);
|
||||
|
||||
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
|
||||
message: `Failed to reach Gotenberg: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
userMessage:
|
||||
'File conversion service is temporarily unavailable. Please upload a PDF file instead.',
|
||||
statusCode: 503,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -477,9 +477,11 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
|
||||
const duplicatedFile = await putNormalizedPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
file: {
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(buffer),
|
||||
},
|
||||
});
|
||||
|
||||
const newDocumentData = await prisma.documentData.create({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+100
-332
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
+109
-339
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+121
-351
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}).extend({
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
|
||||
@@ -41,6 +41,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}).extend({
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
|
||||
@@ -15,11 +15,17 @@ type File = {
|
||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
type PutPdfFileOptions = {
|
||||
initialData?: string;
|
||||
originalData?: string;
|
||||
originalMimeType?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a document file to the appropriate storage location and creates
|
||||
* a document data record.
|
||||
*/
|
||||
export const putPdfFileServerSide = async (file: File, initialData?: string) => {
|
||||
export const putPdfFileServerSide = async (file: File, options: PutPdfFileOptions = {}) => {
|
||||
const isEncryptedDocumentsAllowed = false; // Was feature flag.
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
@@ -40,7 +46,13 @@ export const putPdfFileServerSide = async (file: File, initialData?: string) =>
|
||||
|
||||
const { type, data } = await putFileServerSide(file);
|
||||
|
||||
const createdData = await createDocumentData({ type, data, initialData });
|
||||
const createdData = await createDocumentData({
|
||||
type,
|
||||
data,
|
||||
initialData: options.initialData,
|
||||
originalData: options.originalData,
|
||||
originalMimeType: options.originalMimeType,
|
||||
});
|
||||
|
||||
return {
|
||||
documentData: createdData,
|
||||
@@ -48,13 +60,25 @@ export const putPdfFileServerSide = async (file: File, initialData?: string) =>
|
||||
};
|
||||
};
|
||||
|
||||
type PutNormalizedPdfOptions = {
|
||||
file: File;
|
||||
originalData?: string;
|
||||
originalMimeType?: string;
|
||||
flattenForm?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a pdf file and normalizes it.
|
||||
*/
|
||||
export const putNormalizedPdfFileServerSide = async (file: File, options: { flattenForm?: boolean } = {}) => {
|
||||
export const putNormalizedPdfFileServerSide = async ({
|
||||
file,
|
||||
originalData,
|
||||
originalMimeType,
|
||||
flattenForm = true,
|
||||
}: PutNormalizedPdfOptions) => {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const normalized = await normalizePdf(buffer, options);
|
||||
const normalized = await normalizePdf(buffer, { flattenForm });
|
||||
|
||||
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
|
||||
|
||||
@@ -67,6 +91,8 @@ export const putNormalizedPdfFileServerSide = async (file: File, options: { flat
|
||||
return await createDocumentData({
|
||||
type: documentData.type,
|
||||
data: documentData.data,
|
||||
originalData,
|
||||
originalMimeType,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentData" ADD COLUMN "originalMimeType" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentData" ADD COLUMN "originalData" TEXT;
|
||||
@@ -495,11 +495,13 @@ enum DocumentSigningOrder {
|
||||
}
|
||||
|
||||
model DocumentData {
|
||||
id String @id @default(cuid())
|
||||
type DocumentDataType
|
||||
data String
|
||||
initialData String
|
||||
envelopeItem EnvelopeItem?
|
||||
id String @id @default(cuid())
|
||||
type DocumentDataType
|
||||
data String
|
||||
initialData String
|
||||
originalData String?
|
||||
originalMimeType String?
|
||||
envelopeItem EnvelopeItem?
|
||||
}
|
||||
|
||||
enum DocumentDistributionMethod {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { convertToPdfIfNeeded } from '@documenso/lib/server-only/file-conversion/convert-to-pdf';
|
||||
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { putFileServerSide, putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
@@ -35,7 +36,9 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
attachments,
|
||||
} = payload;
|
||||
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||
|
||||
let pdf = pdfBuffer;
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
@@ -45,10 +48,24 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
let originalData: string | undefined;
|
||||
if (originalBuffer) {
|
||||
const stored = await putFileServerSide({
|
||||
name: `original-${file.name}`,
|
||||
type: originalMimeType,
|
||||
arrayBuffer: async () => Promise.resolve(originalBuffer),
|
||||
});
|
||||
originalData = stored.data;
|
||||
}
|
||||
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdf),
|
||||
file: {
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdf),
|
||||
},
|
||||
originalData,
|
||||
originalMimeType,
|
||||
});
|
||||
|
||||
ctx.logger.info({
|
||||
|
||||
@@ -17,6 +17,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}),
|
||||
documentMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||
import { convertToPdfIfNeeded } from '@documenso/lib/server-only/file-conversion/convert-to-pdf';
|
||||
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { putFileServerSide, putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
@@ -96,17 +97,12 @@ export const createEnvelopeRouteCaller = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'You cannot upload non-PDF files',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// For each file: normalize, extract & clean placeholders, then upload.
|
||||
// For each file: convert to PDF if needed, normalize, extract & clean placeholders, then upload.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||
|
||||
let pdf = pdfBuffer;
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
@@ -123,11 +119,27 @@ export const createEnvelopeRouteCaller = async ({
|
||||
// Todo: Embeds - Might need to add this for client-side embeds in the future.
|
||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||
|
||||
const { documentData } = await putPdfFileServerSide({
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
});
|
||||
let originalData: string | undefined;
|
||||
if (originalBuffer) {
|
||||
const stored = await putFileServerSide({
|
||||
name: `original-${file.name}`,
|
||||
type: originalMimeType,
|
||||
arrayBuffer: async () => Promise.resolve(originalBuffer),
|
||||
});
|
||||
originalData = stored.data;
|
||||
}
|
||||
|
||||
const { documentData } = await putPdfFileServerSide(
|
||||
{
|
||||
name: file.name,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
||||
},
|
||||
{
|
||||
originalData,
|
||||
originalMimeType: originalBuffer ? originalMimeType : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const ZGetEnvelopeItemsResponseSchema = z.object({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
|
||||
@@ -74,7 +74,8 @@ export const useEnvelopeRoute = authenticatedProcedure
|
||||
const uploadedFiles = await Promise.all(
|
||||
filesToUpload.map(async (file) => {
|
||||
// We disable flattening here since `createDocumentFromTemplate` will handle it.
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file, {
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||
file,
|
||||
flattenForm: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -269,7 +269,8 @@ export const templateRouter = router({
|
||||
attachments,
|
||||
} = payload;
|
||||
|
||||
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file, {
|
||||
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide({
|
||||
file,
|
||||
flattenForm: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
@@ -54,9 +55,7 @@ export const DocumentDropzone = ({
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||
multiple: allowMultiple,
|
||||
disabled,
|
||||
onDrop: (acceptedFiles) => {
|
||||
@@ -151,7 +150,7 @@ export const DocumentDropzone = ({
|
||||
<p className="mt-6 font-medium text-foreground">{_(heading[type])}</p>
|
||||
|
||||
<p className="mt-1 text-center text-muted-foreground/80 text-sm">
|
||||
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}
|
||||
{_(disabled ? disabledMessage : msg`Drag & drop PDF, DOCX, or images here.`)}
|
||||
</p>
|
||||
|
||||
{disabled && IS_BILLING_ENABLED() && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
@@ -51,9 +52,7 @@ export const DocumentUploadButton = ({
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||
multiple: internalVersion === '2',
|
||||
disabled,
|
||||
maxFiles,
|
||||
|
||||
Reference in New Issue
Block a user