mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 09:54:51 +10:00
feat: document file conversion
This commit is contained in:
@@ -5,14 +5,20 @@ import { prisma } from '@documenso/prisma';
|
||||
export type CreateDocumentDataOptions = {
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
originalMimeType?: string;
|
||||
};
|
||||
|
||||
export const createDocumentData = async ({ type, data }: CreateDocumentDataOptions) => {
|
||||
export const createDocumentData = async ({
|
||||
type,
|
||||
data,
|
||||
originalMimeType,
|
||||
}: CreateDocumentDataOptions) => {
|
||||
return await prisma.documentData.create({
|
||||
data: {
|
||||
type,
|
||||
data,
|
||||
initialData: data,
|
||||
originalMimeType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -82,6 +82,7 @@ export const sendDocument = async ({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
if (originalMimeType === 'application/pdf') {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
return {
|
||||
pdfBuffer: Buffer.from(arrayBuffer),
|
||||
originalMimeType,
|
||||
};
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const pdfBuffer = await convertFileToPdfViaGotenberg({
|
||||
file: buffer,
|
||||
filename: file.name,
|
||||
mimeType: originalMimeType,
|
||||
});
|
||||
|
||||
return {
|
||||
pdfBuffer,
|
||||
originalMimeType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { getGotenbergUrl } from '../../constants/upload';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
|
||||
export type ConvertFileToPdfOptions = {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
const CONVERSION_TIMEOUT_MS = 30_000;
|
||||
|
||||
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(), CONVERSION_TIMEOUT_MS);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user