108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
import crypto from 'crypto';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
|
|
export function getAttachmentsDir(): string {
|
|
return process.env.ATTACHMENTS_DIR || path.join(process.cwd(), 'data', 'attachments');
|
|
}
|
|
|
|
export async function saveAttachmentBuffer(
|
|
buffer: Buffer,
|
|
originalFilename: string,
|
|
mimeType: string
|
|
): Promise<{
|
|
md5Hash: string;
|
|
storagePath: string;
|
|
sizeBytes: number;
|
|
extractedText: string | null;
|
|
}> {
|
|
const md5Hash = crypto.createHash('md5').update(buffer).digest('hex');
|
|
const ext = path.extname(originalFilename) || guessExtensionFromMime(mimeType);
|
|
const subDir = md5Hash.slice(0, 2);
|
|
const baseDir = getAttachmentsDir();
|
|
const targetDir = path.join(baseDir, subDir);
|
|
await fs.mkdir(targetDir, { recursive: true });
|
|
|
|
const fileNameOnDisk = `${md5Hash}${ext}`;
|
|
const fullPath = path.join(targetDir, fileNameOnDisk);
|
|
|
|
try {
|
|
await fs.access(fullPath);
|
|
} catch {
|
|
await fs.writeFile(fullPath, buffer);
|
|
}
|
|
|
|
let extractedText: string | null = null;
|
|
if (
|
|
mimeType.startsWith('text/') ||
|
|
mimeType === 'application/json' ||
|
|
mimeType === 'application/xml' ||
|
|
originalFilename.endsWith('.md') ||
|
|
originalFilename.endsWith('.txt') ||
|
|
originalFilename.endsWith('.csv')
|
|
) {
|
|
extractedText = buffer.toString('utf-8').slice(0, 50000);
|
|
}
|
|
|
|
return {
|
|
md5Hash,
|
|
storagePath: path.join(subDir, fileNameOnDisk),
|
|
sizeBytes: buffer.length,
|
|
extractedText,
|
|
};
|
|
}
|
|
|
|
export function getFullAttachmentPath(storagePath: string): string {
|
|
const baseDir = getAttachmentsDir();
|
|
const resolved = path.resolve(baseDir, storagePath);
|
|
if (!resolved.startsWith(path.resolve(baseDir))) {
|
|
throw new Error('Invalid storage path');
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
export function guessExtensionFromMime(mime: string): string {
|
|
const map: Record<string, string> = {
|
|
'image/jpeg': '.jpg',
|
|
'image/jpg': '.jpg',
|
|
'image/png': '.png',
|
|
'image/gif': '.gif',
|
|
'image/webp': '.webp',
|
|
'image/svg+xml': '.svg',
|
|
'application/pdf': '.pdf',
|
|
'audio/mpeg': '.mp3',
|
|
'audio/mp3': '.mp3',
|
|
'audio/wav': '.wav',
|
|
'audio/ogg': '.ogg',
|
|
'audio/webm': '.webm',
|
|
'audio/mp4': '.m4a',
|
|
'video/mp4': '.mp4',
|
|
'text/plain': '.txt',
|
|
'text/html': '.html',
|
|
'text/markdown': '.md',
|
|
};
|
|
return map[mime.toLowerCase()] || '.bin';
|
|
}
|
|
|
|
export function stripHtmlToText(html: string): string {
|
|
if (!html) return '';
|
|
return html
|
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ /gi, ' ')
|
|
.replace(/&/gi, '&')
|
|
.replace(/</gi, '<')
|
|
.replace(/>/gi, '>')
|
|
.replace(/"/gi, '"')
|
|
.replace(/'/gi, "'")
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
export function makeExcerpt(text: string, maxLen = 220): string {
|
|
if (!text) return '';
|
|
const clean = text.replace(/\s+/g, ' ').trim();
|
|
return clean.length > maxLen ? clean.slice(0, maxLen) + '…' : clean;
|
|
}
|