Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/common/inMemoryFilesystemProvider.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { VSBuffer } from '../../../base/common/buffer.js';
7
import { Emitter, Event } from '../../../base/common/event.js';
8
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
9
import * as resources from '../../../base/common/resources.js';
10
import { ReadableStreamEvents, newWriteableStream } from '../../../base/common/stream.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability, isFileOpenForWriteOptions } from './files.js';
13
14
class File implements IStat {
15
16
readonly type: FileType.File;
17
readonly ctime: number;
18
mtime: number;
19
size: number;
20
21
name: string;
22
data?: Uint8Array;
23
24
constructor(name: string) {
25
this.type = FileType.File;
26
this.ctime = Date.now();
27
this.mtime = Date.now();
28
this.size = 0;
29
this.name = name;
30
}
31
}
32
33
class Directory implements IStat {
34
35
readonly type: FileType.Directory;
36
readonly ctime: number;
37
mtime: number;
38
size: number;
39
40
name: string;
41
readonly entries: Map<string, File | Directory>;
42
43
constructor(name: string) {
44
this.type = FileType.Directory;
45
this.ctime = Date.now();
46
this.mtime = Date.now();
47
this.size = 0;
48
this.name = name;
49
this.entries = new Map();
50
}
51
}
52
53
type Entry = File | Directory;
54
55
export class InMemoryFileSystemProvider extends Disposable implements
56
IFileSystemProviderWithFileReadWriteCapability,
57
IFileSystemProviderWithOpenReadWriteCloseCapability,
58
IFileSystemProviderWithFileReadStreamCapability,
59
IFileSystemProviderWithFileAtomicReadCapability,
60
IFileSystemProviderWithFileAtomicWriteCapability,
61
IFileSystemProviderWithFileAtomicDeleteCapability {
62
63
private memoryFdCounter = 0;
64
private readonly fdMemory = new Map<number, { file: File; resource: URI; append: boolean; write: boolean }>();
65
private _onDidChangeCapabilities = this._register(new Emitter<void>());
66
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
67
68
private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive;
69
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
70
71
setReadOnly(readonly: boolean) {
72
const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly);
73
if (readonly !== isReadonly) {
74
this._capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive | (readonly ? FileSystemProviderCapabilities.Readonly : 0);
75
this._onDidChangeCapabilities.fire();
76
}
77
}
78
79
root = new Directory('');
80
81
// --- manage file metadata
82
83
async stat(resource: URI): Promise<IStat> {
84
return this._lookup(resource, false);
85
}
86
87
async readdir(resource: URI): Promise<[string, FileType][]> {
88
const entry = this._lookupAsDirectory(resource, false);
89
const result: [string, FileType][] = [];
90
entry.entries.forEach((child, name) => result.push([name, child.type]));
91
return result;
92
}
93
94
// --- manage file contents
95
96
async readFile(resource: URI): Promise<Uint8Array> {
97
const data = this._lookupAsFile(resource, false).data;
98
if (data) {
99
return data;
100
}
101
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
102
}
103
104
readFileStream(resource: URI): ReadableStreamEvents<Uint8Array> {
105
const data = this._lookupAsFile(resource, false).data;
106
107
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
108
stream.end(data);
109
110
return stream;
111
}
112
113
async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
114
const basename = resources.basename(resource);
115
const parent = this._lookupParentDirectory(resource);
116
let entry = parent.entries.get(basename);
117
if (entry instanceof Directory) {
118
throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
119
}
120
if (!entry && !opts.create) {
121
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
122
}
123
if (entry && opts.create && !opts.overwrite) {
124
throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
125
}
126
if (!entry) {
127
entry = new File(basename);
128
parent.entries.set(basename, entry);
129
this._fireSoon({ type: FileChangeType.ADDED, resource });
130
}
131
entry.mtime = Date.now();
132
133
if (opts.append) {
134
entry.size += content.byteLength;
135
const oldData = entry.data ?? new Uint8Array(0);
136
const newData = new Uint8Array(oldData.byteLength + content.byteLength);
137
newData.set(oldData, 0);
138
newData.set(content, oldData.byteLength);
139
entry.data = newData;
140
} else {
141
entry.size = content.byteLength;
142
entry.data = content;
143
}
144
145
this._fireSoon({ type: FileChangeType.UPDATED, resource });
146
}
147
148
// file open/read/write/close
149
open(resource: URI, opts: IFileOpenOptions): Promise<number> {
150
let file = this._lookup(resource, true);
151
const write = isFileOpenForWriteOptions(opts);
152
const append = write && !!opts.append;
153
154
if (!file) {
155
if (!write) {
156
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
157
}
158
// Create the file if opening for write
159
const basename = resources.basename(resource);
160
const parent = this._lookupParentDirectory(resource);
161
file = new File(basename);
162
file.data = new Uint8Array(0);
163
parent.entries.set(basename, file);
164
this._fireSoon({ type: FileChangeType.ADDED, resource });
165
} else if (file instanceof Directory) {
166
throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
167
}
168
169
if (!file.data) {
170
file.data = new Uint8Array(0);
171
}
172
173
const fd = this.memoryFdCounter++;
174
this.fdMemory.set(fd, { file, resource, write, append });
175
return Promise.resolve(fd);
176
}
177
178
close(fd: number): Promise<void> {
179
const fdData = this.fdMemory.get(fd);
180
if (fdData?.write) {
181
// Update file metadata on close
182
fdData.file.mtime = Date.now();
183
fdData.file.size = fdData.file.data?.byteLength ?? 0;
184
this._fireSoon({ type: FileChangeType.UPDATED, resource: fdData.resource });
185
}
186
this.fdMemory.delete(fd);
187
return Promise.resolve();
188
}
189
190
read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
191
const fdData = this.fdMemory.get(fd);
192
if (!fdData) {
193
throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);
194
}
195
196
if (!fdData.file.data) {
197
return Promise.resolve(0);
198
}
199
200
const toWrite = VSBuffer.wrap(fdData.file.data).slice(pos, pos + length);
201
data.set(toWrite.buffer, offset);
202
return Promise.resolve(toWrite.byteLength);
203
}
204
205
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
206
const fdData = this.fdMemory.get(fd);
207
if (!fdData) {
208
throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);
209
}
210
211
const toWrite = VSBuffer.wrap(data).slice(offset, offset + length);
212
fdData.file.data ??= new Uint8Array(0);
213
214
// In append mode, always write at the end
215
const writePos = fdData.append ? fdData.file.data.byteLength : pos;
216
217
// Grow the buffer if needed
218
const endPos = writePos + toWrite.byteLength;
219
if (endPos > fdData.file.data.byteLength) {
220
const newData = new Uint8Array(endPos);
221
newData.set(fdData.file.data, 0);
222
fdData.file.data = newData;
223
}
224
225
fdData.file.data.set(toWrite.buffer, writePos);
226
return Promise.resolve(toWrite.byteLength);
227
}
228
229
// --- manage files/folders
230
231
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
232
if (!opts.overwrite && this._lookup(to, true)) {
233
throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
234
}
235
236
const entry = this._lookup(from, false);
237
const oldParent = this._lookupParentDirectory(from);
238
239
const newParent = this._lookupParentDirectory(to);
240
const newName = resources.basename(to);
241
242
oldParent.entries.delete(entry.name);
243
entry.name = newName;
244
newParent.entries.set(newName, entry);
245
246
this._fireSoon(
247
{ type: FileChangeType.DELETED, resource: from },
248
{ type: FileChangeType.ADDED, resource: to }
249
);
250
}
251
252
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
253
const dirname = resources.dirname(resource);
254
const basename = resources.basename(resource);
255
const parent = this._lookupAsDirectory(dirname, false);
256
if (parent.entries.delete(basename)) {
257
parent.mtime = Date.now();
258
parent.size -= 1;
259
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { resource, type: FileChangeType.DELETED });
260
}
261
}
262
263
async mkdir(resource: URI): Promise<void> {
264
if (this._lookup(resource, true)) {
265
throw createFileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
266
}
267
268
const basename = resources.basename(resource);
269
const dirname = resources.dirname(resource);
270
const parent = this._lookupAsDirectory(dirname, false);
271
272
const entry = new Directory(basename);
273
parent.entries.set(entry.name, entry);
274
parent.mtime = Date.now();
275
parent.size += 1;
276
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });
277
}
278
279
// --- lookup
280
281
private _lookup(uri: URI, silent: false): Entry;
282
private _lookup(uri: URI, silent: boolean): Entry | undefined;
283
private _lookup(uri: URI, silent: boolean): Entry | undefined {
284
const parts = uri.path.split('/');
285
let entry: Entry = this.root;
286
for (const part of parts) {
287
if (!part) {
288
continue;
289
}
290
let child: Entry | undefined;
291
if (entry instanceof Directory) {
292
child = entry.entries.get(part);
293
}
294
if (!child) {
295
if (!silent) {
296
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
297
} else {
298
return undefined;
299
}
300
}
301
entry = child;
302
}
303
return entry;
304
}
305
306
private _lookupAsDirectory(uri: URI, silent: boolean): Directory {
307
const entry = this._lookup(uri, silent);
308
if (entry instanceof Directory) {
309
return entry;
310
}
311
throw createFileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);
312
}
313
314
private _lookupAsFile(uri: URI, silent: boolean): File {
315
const entry = this._lookup(uri, silent);
316
if (entry instanceof File) {
317
return entry;
318
}
319
throw createFileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);
320
}
321
322
private _lookupParentDirectory(uri: URI): Directory {
323
const dirname = resources.dirname(uri);
324
return this._lookupAsDirectory(dirname, false);
325
}
326
327
// --- manage file events
328
329
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
330
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
331
332
private _bufferedChanges: IFileChange[] = [];
333
private _fireSoonHandle?: Timeout;
334
335
watch(resource: URI, opts: IWatchOptions): IDisposable {
336
// ignore, fires for all changes...
337
return Disposable.None;
338
}
339
340
private _fireSoon(...changes: IFileChange[]): void {
341
this._bufferedChanges.push(...changes);
342
343
if (this._fireSoonHandle) {
344
clearTimeout(this._fireSoonHandle);
345
}
346
347
this._fireSoonHandle = setTimeout(() => {
348
this._onDidChangeFile.fire(this._bufferedChanges);
349
this._bufferedChanges.length = 0;
350
}, 5);
351
}
352
353
override dispose(): void {
354
super.dispose();
355
356
this.fdMemory.clear();
357
}
358
}
359
360