File size: 4,323 Bytes
f0743f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import path from 'path';
import crypto from 'node:crypto';
import { createReadStream } from 'fs';
import { readFile, stat } from 'fs/promises';
/**
* Sanitize a filename by removing any directory components, replacing non-alphanumeric characters
* @param inputName
*/
export function sanitizeFilename(inputName: string): string {
// Remove any directory components
let name = path.basename(inputName);
// Replace any non-alphanumeric characters except for '.' and '-'
name = name.replace(/[^a-zA-Z0-9.-]/g, '_');
// Ensure the name doesn't start with a dot (hidden file in Unix-like systems)
if (name.startsWith('.') || name === '') {
name = '_' + name;
}
// Limit the length of the filename
const MAX_LENGTH = 255;
if (name.length > MAX_LENGTH) {
const ext = path.extname(name);
const nameWithoutExt = path.basename(name, ext);
name =
nameWithoutExt.slice(0, MAX_LENGTH - ext.length - 7) +
'-' +
crypto.randomBytes(3).toString('hex') +
ext;
}
return name;
}
/**
* Options for reading files
*/
export interface ReadFileOptions {
encoding?: BufferEncoding;
/** Size threshold in bytes. Files larger than this will be streamed. Default: 10MB */
streamThreshold?: number;
/** Size of chunks when streaming. Default: 64KB */
highWaterMark?: number;
/** File size in bytes if known (e.g. from multer). Avoids extra stat() call. */
fileSize?: number;
}
/**
* Result from reading a file
*/
export interface ReadFileResult<T> {
content: T;
bytes: number;
}
/**
* Reads a file asynchronously. Uses streaming for large files to avoid memory issues.
*
* @param filePath - Path to the file to read
* @param options - Options for reading the file
* @returns Promise resolving to the file contents and size
* @throws Error if the file cannot be read
*/
export async function readFileAsString(
filePath: string,
options: ReadFileOptions = {},
): Promise<ReadFileResult<string>> {
const {
encoding = 'utf8',
streamThreshold = 10 * 1024 * 1024, // 10MB
highWaterMark = 64 * 1024, // 64KB
fileSize,
} = options;
// Get file size if not provided
const bytes = fileSize ?? (await stat(filePath)).size;
// For large files, use streaming to avoid memory issues
if (bytes > streamThreshold) {
const chunks: string[] = [];
const stream = createReadStream(filePath, {
encoding,
highWaterMark,
});
for await (const chunk of stream) {
chunks.push(chunk as string);
}
return { content: chunks.join(''), bytes };
}
// For smaller files, read directly
const content = await readFile(filePath, encoding);
return { content, bytes };
}
/**
* Reads a file as a Buffer asynchronously. Uses streaming for large files.
*
* @param filePath - Path to the file to read
* @param options - Options for reading the file
* @returns Promise resolving to the file contents and size
* @throws Error if the file cannot be read
*/
export async function readFileAsBuffer(
filePath: string,
options: Omit<ReadFileOptions, 'encoding'> = {},
): Promise<ReadFileResult<Buffer>> {
const {
streamThreshold = 10 * 1024 * 1024, // 10MB
highWaterMark = 64 * 1024, // 64KB
fileSize,
} = options;
// Get file size if not provided
const bytes = fileSize ?? (await stat(filePath)).size;
// For large files, use streaming to avoid memory issues
if (bytes > streamThreshold) {
const chunks: Buffer[] = [];
const stream = createReadStream(filePath, {
highWaterMark,
});
for await (const chunk of stream) {
chunks.push(chunk as Buffer);
}
return { content: Buffer.concat(chunks), bytes };
}
// For smaller files, read directly
const content = await readFile(filePath);
return { content, bytes };
}
/**
* Reads a JSON file asynchronously
*
* @param filePath - Path to the JSON file to read
* @param options - Options for reading the file
* @returns Promise resolving to the parsed JSON object
* @throws Error if the file cannot be read or parsed
*/
export async function readJsonFile<T = unknown>(
filePath: string,
options: Omit<ReadFileOptions, 'encoding'> = {},
): Promise<T> {
const { content } = await readFileAsString(filePath, { ...options, encoding: 'utf8' });
return JSON.parse(content);
}
|