Initial commit: Everclone self-hosted Evernote alternative
This commit is contained in:
commit
ee2ee84de6
51 files changed
+17728
No files matched your search
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user