Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
18 kB · 684 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684/** * SMB2/3 File System Provider * Handles file system operations for SMB shares */
import { promises as fs, constants as fsConstants, Stats } from 'fs';import { join, resolve, dirname, basename } from 'path';import type { SMB2ServerConfig, SMB2ShareConfig, SMB2FileId, SMB2OpenFile, SMB2Connection, SMB2Session, SMB2TreeConnect, FileBasicInformation, FileStandardInformation, FileDirectoryInformation,} from '../protocol/types.js';import { ACCESS_MASK, SHARE_ACCESS, CREATE_DISPOSITION, CREATE_OPTIONS, FILE_ATTRIBUTES, OPLOCK_LEVEL,} from '../protocol/constants.js';import { Logger } from '../utils/logger.js';import { generateFileId, fileIdToString, validateSmbPath, smbPathToLocal, localPathToSmb, getCurrentFileTime, dateToFileTime, fileTimeToDate,} from '../utils/helpers.js';
export interface FileHandle { readonly fileId: SMB2FileId; readonly localPath: string; readonly smbPath: string; readonly desiredAccess: ACCESS_MASK; readonly shareAccess: SHARE_ACCESS; readonly createOptions: CREATE_OPTIONS; readonly fileAttributes: FILE_ATTRIBUTES; readonly oplockLevel: OPLOCK_LEVEL; readonly creationTime: Date; readonly lastAccessTime: Date; readonly lastWriteTime: Date; readonly changeTime: Date; readonly isDirectory: boolean; readonly fileSize: bigint; readonly allocationSize: bigint; fdHandle?: number;}
export interface DirectoryEntry { readonly name: string; readonly isDirectory: boolean; readonly fileSize: bigint; readonly creationTime: Date; readonly lastAccessTime: Date; readonly lastWriteTime: Date; readonly changeTime: Date; readonly fileAttributes: FILE_ATTRIBUTES;}
export class SMB2FileSystemProvider { private readonly config: SMB2ServerConfig; private readonly logger: Logger; private readonly openFiles: Map<string, FileHandle>; private readonly shares: Map<string, SMB2ShareConfig>;
constructor(config: SMB2ServerConfig) { this.config = config; this.logger = new Logger('SMB2FileSystemProvider'); this.openFiles = new Map(); this.shares = new Map();
// Load share configurations this.loadShares(); }
/** * Open or create a file */ public async openFile( shareName: string, path: string, desiredAccess: ACCESS_MASK, shareAccess: SHARE_ACCESS, createDisposition: CREATE_DISPOSITION, createOptions: CREATE_OPTIONS, fileAttributes: FILE_ATTRIBUTES = FILE_ATTRIBUTES.NORMAL, session: SMB2Session, treeConnect: SMB2TreeConnect ): Promise<FileHandle> { try { this.logger.debug(`Opening file: ${shareName}\\${path}`);
// Validate path if (!validateSmbPath(path)) { throw new Error('Invalid path'); }
// Get share configuration const share = this.shares.get(shareName); if (!share) { throw new Error(`Share not found: ${shareName}`); }
// Convert to local path const localPath = smbPathToLocal(path, share.path); this.logger.debug(`Local path: ${localPath}`);
// Check if path exists let stats: Stats | null = null; let exists = false; try { stats = await fs.stat(localPath); exists = true; } catch { exists = false; }
// Handle create disposition const shouldCreate = await this.handleCreateDisposition( localPath, exists, createDisposition, createOptions );
if (shouldCreate && !exists) { await this.createFileOrDirectory(localPath, createOptions, fileAttributes); stats = await fs.stat(localPath); }
if (!stats) { throw new Error('File not found and cannot be created'); }
// Generate file ID const fileId = generateFileId(); const fileIdStr = fileIdToString(fileId);
// Create file handle const handle: FileHandle = { fileId, localPath, smbPath: path, desiredAccess, shareAccess, createOptions, fileAttributes: this.getFileAttributes(stats), oplockLevel: OPLOCK_LEVEL.NONE, creationTime: stats.birthtime, lastAccessTime: stats.atime, lastWriteTime: stats.mtime, changeTime: stats.ctime, isDirectory: stats.isDirectory(), fileSize: BigInt(stats.size), allocationSize: BigInt(Math.ceil(stats.size / 4096) * 4096), // Align to 4KB };
// Open file descriptor if needed if (!handle.isDirectory && this.needsFileDescriptor(desiredAccess)) { const flags = this.getFileFlags(desiredAccess, createDisposition); handle.fdHandle = await fs.open(localPath, flags).then(fd => fd.fd); }
this.openFiles.set(fileIdStr, handle); this.logger.info(`File opened: ${path} (ID: ${fileIdStr})`);
return handle;
} catch (error) { this.logger.error(`Error opening file ${path}:`, error); throw error; } }
/** * Close a file */ public async closeFile(fileId: SMB2FileId): Promise<void> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { this.logger.warn(`Attempted to close non-existent file: ${fileIdStr}`); return; }
// Close file descriptor if open if (handle.fdHandle !== undefined) { await fs.close(handle.fdHandle); }
this.openFiles.delete(fileIdStr); this.logger.debug(`File closed: ${handle.smbPath} (ID: ${fileIdStr})`);
} catch (error) { this.logger.error(`Error closing file:`, error); throw error; } }
/** * Read data from file */ public async readFile( fileId: SMB2FileId, offset: bigint, length: number ): Promise<Buffer> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { throw new Error('File not open'); }
if (handle.isDirectory) { throw new Error('Cannot read from directory'); }
// Check access rights if (!(handle.desiredAccess & ACCESS_MASK.FILE_READ_DATA)) { throw new Error('Read access denied'); }
let data: Buffer;
if (handle.fdHandle !== undefined) { // Read using file descriptor const buffer = Buffer.alloc(length); const result = await fs.read(handle.fdHandle, buffer, 0, length, Number(offset)); data = buffer.subarray(0, result.bytesRead); } else { // Read entire file and slice const fileData = await fs.readFile(handle.localPath); const start = Number(offset); const end = Math.min(start + length, fileData.length); data = fileData.subarray(start, end); }
// Update access time await this.updateAccessTime(handle.localPath);
this.logger.debug(`Read ${data.length} bytes from ${handle.smbPath} at offset ${offset}`); return data;
} catch (error) { this.logger.error(`Error reading file:`, error); throw error; } }
/** * Write data to file */ public async writeFile( fileId: SMB2FileId, offset: bigint, data: Buffer ): Promise<number> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { throw new Error('File not open'); }
if (handle.isDirectory) { throw new Error('Cannot write to directory'); }
// Check access rights if (!(handle.desiredAccess & ACCESS_MASK.FILE_WRITE_DATA)) { throw new Error('Write access denied'); }
let bytesWritten: number;
if (handle.fdHandle !== undefined) { // Write using file descriptor const result = await fs.write(handle.fdHandle, data, 0, data.length, Number(offset)); bytesWritten = result.bytesWritten; } else { // For now, just write the entire data (not ideal for large files) await fs.writeFile(handle.localPath, data); bytesWritten = data.length; }
this.logger.debug(`Wrote ${bytesWritten} bytes to ${handle.smbPath} at offset ${offset}`); return bytesWritten;
} catch (error) { this.logger.error(`Error writing file:`, error); throw error; } }
/** * Query directory contents */ public async queryDirectory( fileId: SMB2FileId, pattern: string = '*' ): Promise<DirectoryEntry[]> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { throw new Error('Directory not open'); }
if (!handle.isDirectory) { throw new Error('Not a directory'); }
const entries: DirectoryEntry[] = []; const files = await fs.readdir(handle.localPath, { withFileTypes: true });
for (const file of files) { // Simple pattern matching (only * wildcard) if (pattern !== '*' && !this.matchPattern(file.name, pattern)) { continue; }
try { const filePath = join(handle.localPath, file.name); const stats = await fs.stat(filePath);
const entry: DirectoryEntry = { name: file.name, isDirectory: file.isDirectory(), fileSize: BigInt(stats.size), creationTime: stats.birthtime, lastAccessTime: stats.atime, lastWriteTime: stats.mtime, changeTime: stats.ctime, fileAttributes: this.getFileAttributes(stats), };
entries.push(entry); } catch (error) { this.logger.warn(`Error getting stats for ${file.name}:`, error); continue; } }
this.logger.debug(`Listed ${entries.length} entries from ${handle.smbPath}`); return entries;
} catch (error) { this.logger.error(`Error querying directory:`, error); throw error; } }
/** * Get file information */ public async getFileInfo(fileId: SMB2FileId): Promise<{ basic: FileBasicInformation; standard: FileStandardInformation; }> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { throw new Error('File not open'); }
// Get current stats const stats = await fs.stat(handle.localPath);
const basic: FileBasicInformation = { creationTime: dateToFileTime(stats.birthtime), lastAccessTime: dateToFileTime(stats.atime), lastWriteTime: dateToFileTime(stats.mtime), changeTime: dateToFileTime(stats.ctime), fileAttributes: this.getFileAttributes(stats), reserved: 0, };
const standard: FileStandardInformation = { allocationSize: BigInt(Math.ceil(stats.size / 4096) * 4096), endOfFile: BigInt(stats.size), numberOfLinks: stats.nlink, deletePending: false, directory: stats.isDirectory(), reserved: 0, };
return { basic, standard };
} catch (error) { this.logger.error(`Error getting file info:`, error); throw error; } }
/** * Set file information */ public async setFileInfo( fileId: SMB2FileId, info: Partial<FileBasicInformation> ): Promise<void> { try { const fileIdStr = fileIdToString(fileId); const handle = this.openFiles.get(fileIdStr);
if (!handle) { throw new Error('File not open'); }
// Check access rights if (!(handle.desiredAccess & ACCESS_MASK.FILE_WRITE_ATTRIBUTES)) { throw new Error('Write attributes access denied'); }
// Update timestamps if provided if (info.lastAccessTime || info.lastWriteTime) { const atime = info.lastAccessTime ? fileTimeToDate(info.lastAccessTime) : undefined; const mtime = info.lastWriteTime ? fileTimeToDate(info.lastWriteTime) : undefined; if (atime || mtime) { await fs.utimes(handle.localPath, atime || new Date(), mtime || new Date()); } }
// Note: File attributes setting would require platform-specific implementation this.logger.debug(`File info updated: ${handle.smbPath}`);
} catch (error) { this.logger.error(`Error setting file info:`, error); throw error; } }
/** * Delete a file or directory */ public async deleteFile(shareName: string, path: string): Promise<void> { try { if (!validateSmbPath(path)) { throw new Error('Invalid path'); }
const share = this.shares.get(shareName); if (!share) { throw new Error(`Share not found: ${shareName}`); }
const localPath = smbPathToLocal(path, share.path); const stats = await fs.stat(localPath);
if (stats.isDirectory()) { await fs.rmdir(localPath); } else { await fs.unlink(localPath); }
this.logger.info(`Deleted: ${path}`);
} catch (error) { this.logger.error(`Error deleting ${path}:`, error); throw error; } }
/** * Create directory */ public async createDirectory(shareName: string, path: string): Promise<void> { try { if (!validateSmbPath(path)) { throw new Error('Invalid path'); }
const share = this.shares.get(shareName); if (!share) { throw new Error(`Share not found: ${shareName}`); }
const localPath = smbPathToLocal(path, share.path); await fs.mkdir(localPath, { recursive: true });
this.logger.info(`Directory created: ${path}`);
} catch (error) { this.logger.error(`Error creating directory ${path}:`, error); throw error; } }
/** * Get file handle by ID */ public getFileHandle(fileId: SMB2FileId): FileHandle | undefined { return this.openFiles.get(fileIdToString(fileId)); }
/** * Get all open files */ public getOpenFiles(): FileHandle[] { return Array.from(this.openFiles.values()); }
/** * Load share configurations */ private loadShares(): void { for (const share of this.config.shares) { this.shares.set(share.name, share); this.logger.debug(`Loaded share: ${share.name} -> ${share.path}`); } this.logger.info(`Loaded ${this.shares.size} shares`); }
/** * Handle create disposition logic */ private async handleCreateDisposition( localPath: string, exists: boolean, disposition: CREATE_DISPOSITION, createOptions: CREATE_OPTIONS ): Promise<boolean> { switch (disposition) { case CREATE_DISPOSITION.SUPERSEDE: if (exists) { await fs.unlink(localPath); } return true;
case CREATE_DISPOSITION.OPEN: if (!exists) { throw new Error('File does not exist'); } return false;
case CREATE_DISPOSITION.CREATE: if (exists) { throw new Error('File already exists'); } return true;
case CREATE_DISPOSITION.OPEN_IF: return !exists;
case CREATE_DISPOSITION.OVERWRITE: if (!exists) { throw new Error('File does not exist'); } return true;
case CREATE_DISPOSITION.OVERWRITE_IF: return true;
default: throw new Error(`Unsupported create disposition: ${disposition}`); } }
/** * Create file or directory based on options */ private async createFileOrDirectory( localPath: string, createOptions: CREATE_OPTIONS, fileAttributes: FILE_ATTRIBUTES ): Promise<void> { // Ensure parent directory exists const parentDir = dirname(localPath); await fs.mkdir(parentDir, { recursive: true });
if (createOptions & CREATE_OPTIONS.DIRECTORY_FILE) { await fs.mkdir(localPath); } else { // Create empty file await fs.writeFile(localPath, Buffer.alloc(0)); } }
/** * Get file attributes from stats */ private getFileAttributes(stats: Stats): FILE_ATTRIBUTES { let attributes = FILE_ATTRIBUTES.NORMAL;
if (stats.isDirectory()) { attributes |= FILE_ATTRIBUTES.DIRECTORY; }
// Add other attributes based on file properties // This is a simplified implementation return attributes; }
/** * Check if file descriptor is needed */ private needsFileDescriptor(access: ACCESS_MASK): boolean { return !!(access & (ACCESS_MASK.FILE_READ_DATA | ACCESS_MASK.FILE_WRITE_DATA)); }
/** * Get file flags for fs.open */ private getFileFlags(access: ACCESS_MASK, disposition: CREATE_DISPOSITION): string { let flags = '';
const canRead = !!(access & ACCESS_MASK.FILE_READ_DATA); const canWrite = !!(access & ACCESS_MASK.FILE_WRITE_DATA);
if (canRead && canWrite) { flags = 'r+'; } else if (canWrite) { flags = 'w'; } else { flags = 'r'; }
return flags; }
/** * Update file access time */ private async updateAccessTime(localPath: string): Promise<void> { try { const now = new Date(); const stats = await fs.stat(localPath); await fs.utimes(localPath, now, stats.mtime); } catch { // Ignore errors when updating access time } }
/** * Simple pattern matching (supports * wildcard only) */ private matchPattern(filename: string, pattern: string): boolean { if (pattern === '*') { return true; }
// Convert pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`, 'i'); return regex.test(filename); }
/** * Cleanup resources */ public async cleanup(): Promise<void> { // Close all open file descriptors const closePromises = Array.from(this.openFiles.values()) .filter(handle => handle.fdHandle !== undefined) .map(handle => fs.close(handle.fdHandle!).catch(() => {}));
await Promise.all(closePromises); this.openFiles.clear(); this.logger.info('File system provider cleanup completed'); }}