Path: blob/main/src/vs/platform/files/common/inMemoryFilesystemProvider.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../base/common/buffer.js';6import { Emitter, Event } from '../../../base/common/event.js';7import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';8import * as resources from '../../../base/common/resources.js';9import { ReadableStreamEvents, newWriteableStream } from '../../../base/common/stream.js';10import { URI } from '../../../base/common/uri.js';11import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability, isFileOpenForWriteOptions } from './files.js';1213class File implements IStat {1415readonly type: FileType.File;16readonly ctime: number;17mtime: number;18size: number;1920name: string;21data?: Uint8Array;2223constructor(name: string) {24this.type = FileType.File;25this.ctime = Date.now();26this.mtime = Date.now();27this.size = 0;28this.name = name;29}30}3132class Directory implements IStat {3334readonly type: FileType.Directory;35readonly ctime: number;36mtime: number;37size: number;3839name: string;40readonly entries: Map<string, File | Directory>;4142constructor(name: string) {43this.type = FileType.Directory;44this.ctime = Date.now();45this.mtime = Date.now();46this.size = 0;47this.name = name;48this.entries = new Map();49}50}5152type Entry = File | Directory;5354export class InMemoryFileSystemProvider extends Disposable implements55IFileSystemProviderWithFileReadWriteCapability,56IFileSystemProviderWithOpenReadWriteCloseCapability,57IFileSystemProviderWithFileReadStreamCapability,58IFileSystemProviderWithFileAtomicReadCapability,59IFileSystemProviderWithFileAtomicWriteCapability,60IFileSystemProviderWithFileAtomicDeleteCapability {6162private memoryFdCounter = 0;63private readonly fdMemory = new Map<number, { file: File; resource: URI; append: boolean; write: boolean }>();64private _onDidChangeCapabilities = this._register(new Emitter<void>());65readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;6667private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive;68get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }6970setReadOnly(readonly: boolean) {71const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly);72if (readonly !== isReadonly) {73this._capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive | (readonly ? FileSystemProviderCapabilities.Readonly : 0);74this._onDidChangeCapabilities.fire();75}76}7778root = new Directory('');7980// --- manage file metadata8182async stat(resource: URI): Promise<IStat> {83return this._lookup(resource, false);84}8586async readdir(resource: URI): Promise<[string, FileType][]> {87const entry = this._lookupAsDirectory(resource, false);88const result: [string, FileType][] = [];89entry.entries.forEach((child, name) => result.push([name, child.type]));90return result;91}9293// --- manage file contents9495async readFile(resource: URI): Promise<Uint8Array> {96const data = this._lookupAsFile(resource, false).data;97if (data) {98return data;99}100throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);101}102103readFileStream(resource: URI): ReadableStreamEvents<Uint8Array> {104const data = this._lookupAsFile(resource, false).data;105106const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);107stream.end(data);108109return stream;110}111112async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {113const basename = resources.basename(resource);114const parent = this._lookupParentDirectory(resource);115let entry = parent.entries.get(basename);116if (entry instanceof Directory) {117throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);118}119if (!entry && !opts.create) {120throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);121}122if (entry && opts.create && !opts.overwrite) {123throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);124}125if (!entry) {126entry = new File(basename);127parent.entries.set(basename, entry);128this._fireSoon({ type: FileChangeType.ADDED, resource });129}130entry.mtime = Date.now();131132if (opts.append) {133entry.size += content.byteLength;134const oldData = entry.data ?? new Uint8Array(0);135const newData = new Uint8Array(oldData.byteLength + content.byteLength);136newData.set(oldData, 0);137newData.set(content, oldData.byteLength);138entry.data = newData;139} else {140entry.size = content.byteLength;141entry.data = content;142}143144this._fireSoon({ type: FileChangeType.UPDATED, resource });145}146147// file open/read/write/close148open(resource: URI, opts: IFileOpenOptions): Promise<number> {149let file = this._lookup(resource, true);150const write = isFileOpenForWriteOptions(opts);151const append = write && !!opts.append;152153if (!file) {154if (!write) {155throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);156}157// Create the file if opening for write158const basename = resources.basename(resource);159const parent = this._lookupParentDirectory(resource);160file = new File(basename);161file.data = new Uint8Array(0);162parent.entries.set(basename, file);163this._fireSoon({ type: FileChangeType.ADDED, resource });164} else if (file instanceof Directory) {165throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);166}167168if (!file.data) {169file.data = new Uint8Array(0);170}171172const fd = this.memoryFdCounter++;173this.fdMemory.set(fd, { file, resource, write, append });174return Promise.resolve(fd);175}176177close(fd: number): Promise<void> {178const fdData = this.fdMemory.get(fd);179if (fdData?.write) {180// Update file metadata on close181fdData.file.mtime = Date.now();182fdData.file.size = fdData.file.data?.byteLength ?? 0;183this._fireSoon({ type: FileChangeType.UPDATED, resource: fdData.resource });184}185this.fdMemory.delete(fd);186return Promise.resolve();187}188189read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {190const fdData = this.fdMemory.get(fd);191if (!fdData) {192throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);193}194195if (!fdData.file.data) {196return Promise.resolve(0);197}198199const toWrite = VSBuffer.wrap(fdData.file.data).slice(pos, pos + length);200data.set(toWrite.buffer, offset);201return Promise.resolve(toWrite.byteLength);202}203204write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {205const fdData = this.fdMemory.get(fd);206if (!fdData) {207throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);208}209210const toWrite = VSBuffer.wrap(data).slice(offset, offset + length);211fdData.file.data ??= new Uint8Array(0);212213// In append mode, always write at the end214const writePos = fdData.append ? fdData.file.data.byteLength : pos;215216// Grow the buffer if needed217const endPos = writePos + toWrite.byteLength;218if (endPos > fdData.file.data.byteLength) {219const newData = new Uint8Array(endPos);220newData.set(fdData.file.data, 0);221fdData.file.data = newData;222}223224fdData.file.data.set(toWrite.buffer, writePos);225return Promise.resolve(toWrite.byteLength);226}227228// --- manage files/folders229230async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {231if (!opts.overwrite && this._lookup(to, true)) {232throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);233}234235const entry = this._lookup(from, false);236const oldParent = this._lookupParentDirectory(from);237238const newParent = this._lookupParentDirectory(to);239const newName = resources.basename(to);240241oldParent.entries.delete(entry.name);242entry.name = newName;243newParent.entries.set(newName, entry);244245this._fireSoon(246{ type: FileChangeType.DELETED, resource: from },247{ type: FileChangeType.ADDED, resource: to }248);249}250251async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {252const dirname = resources.dirname(resource);253const basename = resources.basename(resource);254const parent = this._lookupAsDirectory(dirname, false);255if (parent.entries.delete(basename)) {256parent.mtime = Date.now();257parent.size -= 1;258this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { resource, type: FileChangeType.DELETED });259}260}261262async mkdir(resource: URI): Promise<void> {263if (this._lookup(resource, true)) {264throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);265}266267const basename = resources.basename(resource);268const dirname = resources.dirname(resource);269const parent = this._lookupAsDirectory(dirname, false);270271const entry = new Directory(basename);272parent.entries.set(entry.name, entry);273parent.mtime = Date.now();274parent.size += 1;275this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });276}277278// --- lookup279280private _lookup(uri: URI, silent: false): Entry;281private _lookup(uri: URI, silent: boolean): Entry | undefined;282private _lookup(uri: URI, silent: boolean): Entry | undefined {283const parts = uri.path.split('/');284let entry: Entry = this.root;285for (const part of parts) {286if (!part) {287continue;288}289let child: Entry | undefined;290if (entry instanceof Directory) {291child = entry.entries.get(part);292}293if (!child) {294if (!silent) {295throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);296} else {297return undefined;298}299}300entry = child;301}302return entry;303}304305private _lookupAsDirectory(uri: URI, silent: boolean): Directory {306const entry = this._lookup(uri, silent);307if (entry instanceof Directory) {308return entry;309}310throw createFileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);311}312313private _lookupAsFile(uri: URI, silent: boolean): File {314const entry = this._lookup(uri, silent);315if (entry instanceof File) {316return entry;317}318throw createFileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);319}320321private _lookupParentDirectory(uri: URI): Directory {322const dirname = resources.dirname(uri);323return this._lookupAsDirectory(dirname, false);324}325326// --- manage file events327328private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());329readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;330331private _bufferedChanges: IFileChange[] = [];332private _fireSoonHandle?: Timeout;333334watch(resource: URI, opts: IWatchOptions): IDisposable {335// ignore, fires for all changes...336return Disposable.None;337}338339private _fireSoon(...changes: IFileChange[]): void {340this._bufferedChanges.push(...changes);341342if (this._fireSoonHandle) {343clearTimeout(this._fireSoonHandle);344}345346this._fireSoonHandle = setTimeout(() => {347this._onDidChangeFile.fire(this._bufferedChanges);348this._bufferedChanges.length = 0;349}, 5);350}351352override dispose(): void {353super.dispose();354355this.fdMemory.clear();356}357}358359360