Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/browser/htmlFileSystemProvider.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 { localize } from '../../../nls.js';
7
import { URI } from '../../../base/common/uri.js';
8
import { VSBuffer } from '../../../base/common/buffer.js';
9
import { CancellationToken } from '../../../base/common/cancellation.js';
10
import { Emitter, Event } from '../../../base/common/event.js';
11
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
12
import { Schemas } from '../../../base/common/network.js';
13
import { basename, extname, normalize } from '../../../base/common/path.js';
14
import { isLinux } from '../../../base/common/platform.js';
15
import { extUri, extUriIgnorePathCase, joinPath } from '../../../base/common/resources.js';
16
import { newWriteableStream, ReadableStreamEvents } from '../../../base/common/stream.js';
17
import { createFileSystemProviderError, IFileDeleteOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, IFileChange, FileChangeType } from '../common/files.js';
18
import { FileSystemObserverRecord, WebFileSystemAccess, WebFileSystemObserver } from './webFileSystemAccess.js';
19
import { IndexedDB } from '../../../base/browser/indexedDB.js';
20
import { ILogService, LogLevel } from '../../log/common/log.js';
21
22
export class HTMLFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability {
23
24
//#region Events (unsupported)
25
26
readonly onDidChangeCapabilities = Event.None;
27
28
//#endregion
29
30
//#region File Capabilities
31
32
private extUri = isLinux ? extUri : extUriIgnorePathCase;
33
34
private _capabilities: FileSystemProviderCapabilities | undefined;
35
get capabilities(): FileSystemProviderCapabilities {
36
if (!this._capabilities) {
37
this._capabilities =
38
FileSystemProviderCapabilities.FileReadWrite |
39
FileSystemProviderCapabilities.FileReadStream;
40
41
if (isLinux) {
42
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
43
}
44
}
45
46
return this._capabilities;
47
}
48
49
//#endregion
50
51
52
constructor(
53
private indexedDB: IndexedDB | undefined,
54
private readonly store: string,
55
private logService: ILogService
56
) {
57
super();
58
}
59
60
//#region File Metadata Resolving
61
62
async stat(resource: URI): Promise<IStat> {
63
try {
64
const handle = await this.getHandle(resource);
65
if (!handle) {
66
throw this.createFileSystemProviderError(resource, 'No such file or directory, stat', FileSystemProviderErrorCode.FileNotFound);
67
}
68
69
if (WebFileSystemAccess.isFileSystemFileHandle(handle)) {
70
const file = await handle.getFile();
71
72
return {
73
type: FileType.File,
74
mtime: file.lastModified,
75
ctime: 0,
76
size: file.size
77
};
78
}
79
80
return {
81
type: FileType.Directory,
82
mtime: 0,
83
ctime: 0,
84
size: 0
85
};
86
} catch (error) {
87
throw this.toFileSystemProviderError(error);
88
}
89
}
90
91
async readdir(resource: URI): Promise<[string, FileType][]> {
92
try {
93
const handle = await this.getDirectoryHandle(resource);
94
if (!handle) {
95
throw this.createFileSystemProviderError(resource, 'No such file or directory, readdir', FileSystemProviderErrorCode.FileNotFound);
96
}
97
98
const result: [string, FileType][] = [];
99
100
for await (const [name, child] of handle) {
101
result.push([name, WebFileSystemAccess.isFileSystemFileHandle(child) ? FileType.File : FileType.Directory]);
102
}
103
104
return result;
105
} catch (error) {
106
throw this.toFileSystemProviderError(error);
107
}
108
}
109
110
//#endregion
111
112
//#region File Reading/Writing
113
114
readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
115
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer, {
116
// Set a highWaterMark to prevent the stream
117
// for file upload to produce large buffers
118
// in-memory
119
highWaterMark: 10
120
});
121
122
(async () => {
123
try {
124
const handle = await this.getFileHandle(resource);
125
if (!handle) {
126
throw this.createFileSystemProviderError(resource, 'No such file or directory, readFile', FileSystemProviderErrorCode.FileNotFound);
127
}
128
129
const file = await handle.getFile();
130
131
// Partial file: implemented simply via `readFile`
132
if (typeof opts.length === 'number' || typeof opts.position === 'number') {
133
let buffer = new Uint8Array(await file.arrayBuffer());
134
135
if (typeof opts?.position === 'number') {
136
buffer = buffer.slice(opts.position);
137
}
138
139
if (typeof opts?.length === 'number') {
140
buffer = buffer.slice(0, opts.length);
141
}
142
143
stream.end(buffer);
144
}
145
146
// Entire file
147
else {
148
const reader: ReadableStreamDefaultReader<Uint8Array> = file.stream().getReader();
149
150
let res = await reader.read();
151
while (!res.done) {
152
if (token.isCancellationRequested) {
153
break;
154
}
155
156
// Write buffer into stream but make sure to wait
157
// in case the `highWaterMark` is reached
158
await stream.write(res.value);
159
160
if (token.isCancellationRequested) {
161
break;
162
}
163
164
res = await reader.read();
165
}
166
stream.end(undefined);
167
}
168
} catch (error) {
169
stream.error(this.toFileSystemProviderError(error));
170
stream.end();
171
}
172
})();
173
174
return stream;
175
}
176
177
async readFile(resource: URI): Promise<Uint8Array> {
178
try {
179
const handle = await this.getFileHandle(resource);
180
if (!handle) {
181
throw this.createFileSystemProviderError(resource, 'No such file or directory, readFile', FileSystemProviderErrorCode.FileNotFound);
182
}
183
184
const file = await handle.getFile();
185
186
return new Uint8Array(await file.arrayBuffer());
187
} catch (error) {
188
throw this.toFileSystemProviderError(error);
189
}
190
}
191
192
async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
193
try {
194
let handle = await this.getFileHandle(resource);
195
196
// Validate target unless { create: true, overwrite: true }
197
if (!opts.create || !opts.overwrite) {
198
if (handle) {
199
if (!opts.overwrite) {
200
throw this.createFileSystemProviderError(resource, 'File already exists, writeFile', FileSystemProviderErrorCode.FileExists);
201
}
202
} else {
203
if (!opts.create) {
204
throw this.createFileSystemProviderError(resource, 'No such file, writeFile', FileSystemProviderErrorCode.FileNotFound);
205
}
206
}
207
}
208
209
// Create target as needed
210
if (!handle) {
211
const parent = await this.getDirectoryHandle(this.extUri.dirname(resource));
212
if (!parent) {
213
throw this.createFileSystemProviderError(resource, 'No such parent directory, writeFile', FileSystemProviderErrorCode.FileNotFound);
214
}
215
216
handle = await parent.getFileHandle(this.extUri.basename(resource), { create: true });
217
if (!handle) {
218
throw this.createFileSystemProviderError(resource, 'Unable to create file , writeFile', FileSystemProviderErrorCode.Unknown);
219
}
220
}
221
222
// Write to target overwriting any existing contents
223
const writable = await handle.createWritable();
224
await writable.write(content as Uint8Array<ArrayBuffer>);
225
await writable.close();
226
} catch (error) {
227
throw this.toFileSystemProviderError(error);
228
}
229
}
230
231
//#endregion
232
233
//#region Move/Copy/Delete/Create Folder
234
235
async mkdir(resource: URI): Promise<void> {
236
try {
237
const parent = await this.getDirectoryHandle(this.extUri.dirname(resource));
238
if (!parent) {
239
throw this.createFileSystemProviderError(resource, 'No such parent directory, mkdir', FileSystemProviderErrorCode.FileNotFound);
240
}
241
242
await parent.getDirectoryHandle(this.extUri.basename(resource), { create: true });
243
} catch (error) {
244
throw this.toFileSystemProviderError(error);
245
}
246
}
247
248
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
249
try {
250
const parent = await this.getDirectoryHandle(this.extUri.dirname(resource));
251
if (!parent) {
252
throw this.createFileSystemProviderError(resource, 'No such parent directory, delete', FileSystemProviderErrorCode.FileNotFound);
253
}
254
255
return parent.removeEntry(this.extUri.basename(resource), { recursive: opts.recursive });
256
} catch (error) {
257
throw this.toFileSystemProviderError(error);
258
}
259
}
260
261
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
262
try {
263
if (this.extUri.isEqual(from, to)) {
264
return; // no-op if the paths are the same
265
}
266
267
// Implement file rename by write + delete
268
const fileHandle = await this.getFileHandle(from);
269
if (fileHandle) {
270
const file = await fileHandle.getFile();
271
const contents = new Uint8Array(await file.arrayBuffer());
272
273
await this.writeFile(to, contents, { create: true, overwrite: opts.overwrite, unlock: false, atomic: false });
274
await this.delete(from, { recursive: false, useTrash: false, atomic: false });
275
}
276
277
// File API does not support any real rename otherwise
278
else {
279
throw this.createFileSystemProviderError(from, localize('fileSystemRenameError', "Rename is only supported for files."), FileSystemProviderErrorCode.Unavailable);
280
}
281
} catch (error) {
282
throw this.toFileSystemProviderError(error);
283
}
284
}
285
286
//#endregion
287
288
//#region File Watching (unsupported)
289
290
private readonly _onDidChangeFileEmitter = this._register(new Emitter<readonly IFileChange[]>());
291
readonly onDidChangeFile = this._onDidChangeFileEmitter.event;
292
293
watch(resource: URI, opts: IWatchOptions): IDisposable {
294
const disposables = new DisposableStore();
295
296
this.doWatch(resource, opts, disposables).catch(error => this.logService.error(`[File Watcher ('FileSystemObserver')] Error: ${error} (${resource})`));
297
298
return disposables;
299
}
300
301
private async doWatch(resource: URI, opts: IWatchOptions, disposables: DisposableStore): Promise<void> {
302
if (!WebFileSystemObserver.supported(globalThis)) {
303
return;
304
}
305
306
const handle = await this.getHandle(resource);
307
if (!handle || disposables.isDisposed) {
308
return;
309
}
310
311
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
312
const observer = new (globalThis as any).FileSystemObserver((records: FileSystemObserverRecord[]) => {
313
if (disposables.isDisposed) {
314
return;
315
}
316
317
const events: IFileChange[] = [];
318
for (const record of records) {
319
if (this.logService.getLevel() === LogLevel.Trace) {
320
this.logService.trace(`[File Watcher ('FileSystemObserver')] [${record.type}] ${joinPath(resource, ...record.relativePathComponents)}`);
321
}
322
323
switch (record.type) {
324
case 'appeared':
325
events.push({ resource: joinPath(resource, ...record.relativePathComponents), type: FileChangeType.ADDED });
326
break;
327
case 'disappeared':
328
events.push({ resource: joinPath(resource, ...record.relativePathComponents), type: FileChangeType.DELETED });
329
break;
330
case 'modified':
331
events.push({ resource: joinPath(resource, ...record.relativePathComponents), type: FileChangeType.UPDATED });
332
break;
333
case 'errored':
334
this.logService.trace(`[File Watcher ('FileSystemObserver')] errored, disposing observer (${resource})`);
335
disposables.dispose();
336
}
337
}
338
339
if (events.length) {
340
this._onDidChangeFileEmitter.fire(events);
341
}
342
});
343
344
try {
345
await observer.observe(handle, opts.recursive ? { recursive: true } : undefined);
346
} finally {
347
if (disposables.isDisposed) {
348
observer.disconnect();
349
} else {
350
disposables.add(toDisposable(() => observer.disconnect()));
351
}
352
}
353
}
354
355
//#endregion
356
357
//#region File/Directoy Handle Registry
358
359
private readonly _files = new Map<string, FileSystemFileHandle>();
360
private readonly _directories = new Map<string, FileSystemDirectoryHandle>();
361
362
registerFileHandle(handle: FileSystemFileHandle): Promise<URI> {
363
return this.registerHandle(handle, this._files);
364
}
365
366
registerDirectoryHandle(handle: FileSystemDirectoryHandle): Promise<URI> {
367
return this.registerHandle(handle, this._directories);
368
}
369
370
get directories(): Iterable<FileSystemDirectoryHandle> {
371
return this._directories.values();
372
}
373
374
private async registerHandle(handle: FileSystemHandle, map: Map<string, FileSystemHandle>): Promise<URI> {
375
let handleId = `/${handle.name}`;
376
377
// Compute a valid handle ID in case this exists already
378
if (map.has(handleId) && !await map.get(handleId)?.isSameEntry(handle)) {
379
const fileExt = extname(handle.name);
380
const fileName = basename(handle.name, fileExt);
381
382
let handleIdCounter = 1;
383
do {
384
handleId = `/${fileName}-${handleIdCounter++}${fileExt}`;
385
} while (map.has(handleId) && !await map.get(handleId)?.isSameEntry(handle));
386
}
387
388
map.set(handleId, handle);
389
390
// Remember in IndexDB for future lookup
391
try {
392
await this.indexedDB?.runInTransaction(this.store, 'readwrite', objectStore => objectStore.put(handle, handleId));
393
} catch (error) {
394
this.logService.error(error);
395
}
396
397
return URI.from({ scheme: Schemas.file, path: handleId });
398
}
399
400
async getHandle(resource: URI): Promise<FileSystemHandle | undefined> {
401
402
// First: try to find a well known handle first
403
let handle = await this.doGetHandle(resource);
404
405
// Second: walk up parent directories and resolve handle if possible
406
if (!handle) {
407
const parent = await this.getDirectoryHandle(this.extUri.dirname(resource));
408
if (parent) {
409
const name = extUri.basename(resource);
410
try {
411
handle = await parent.getFileHandle(name);
412
} catch (error) {
413
try {
414
handle = await parent.getDirectoryHandle(name);
415
} catch (error) {
416
// Ignore
417
}
418
}
419
}
420
}
421
422
return handle;
423
}
424
425
private async getFileHandle(resource: URI): Promise<FileSystemFileHandle | undefined> {
426
const handle = await this.doGetHandle(resource);
427
if (handle instanceof FileSystemFileHandle) {
428
return handle;
429
}
430
431
const parent = await this.getDirectoryHandle(this.extUri.dirname(resource));
432
433
try {
434
return await parent?.getFileHandle(extUri.basename(resource));
435
} catch (error) {
436
return undefined; // guard against possible DOMException
437
}
438
}
439
440
private async getDirectoryHandle(resource: URI): Promise<FileSystemDirectoryHandle | undefined> {
441
const handle = await this.doGetHandle(resource);
442
if (handle instanceof FileSystemDirectoryHandle) {
443
return handle;
444
}
445
446
const parentUri = this.extUri.dirname(resource);
447
if (this.extUri.isEqual(parentUri, resource)) {
448
return undefined; // return when root is reached to prevent infinite recursion
449
}
450
451
const parent = await this.getDirectoryHandle(parentUri);
452
453
try {
454
return await parent?.getDirectoryHandle(extUri.basename(resource));
455
} catch (error) {
456
return undefined; // guard against possible DOMException
457
}
458
}
459
460
private async doGetHandle(resource: URI): Promise<FileSystemHandle | undefined> {
461
462
// We store file system handles with the `handle.name`
463
// and as such require the resource to be on the root
464
if (this.extUri.dirname(resource).path !== '/') {
465
return undefined;
466
}
467
468
const handleId = resource.path.replace(/\/$/, ''); // remove potential slash from the end of the path
469
470
// First: check if we have a known handle stored in memory
471
const inMemoryHandle = this._files.get(handleId) ?? this._directories.get(handleId);
472
if (inMemoryHandle) {
473
return inMemoryHandle;
474
}
475
476
// Second: check if we have a persisted handle in IndexedDB
477
const persistedHandle = await this.indexedDB?.runInTransaction(this.store, 'readonly', store => store.get(handleId));
478
if (WebFileSystemAccess.isFileSystemHandle(persistedHandle)) {
479
let hasPermissions = await persistedHandle.queryPermission() === 'granted';
480
try {
481
if (!hasPermissions) {
482
hasPermissions = await persistedHandle.requestPermission() === 'granted';
483
}
484
} catch (error) {
485
this.logService.error(error); // this can fail with a DOMException
486
}
487
488
if (hasPermissions) {
489
if (WebFileSystemAccess.isFileSystemFileHandle(persistedHandle)) {
490
this._files.set(handleId, persistedHandle);
491
} else if (WebFileSystemAccess.isFileSystemDirectoryHandle(persistedHandle)) {
492
this._directories.set(handleId, persistedHandle);
493
}
494
495
return persistedHandle;
496
}
497
}
498
499
// Third: fail with an error
500
throw this.createFileSystemProviderError(resource, 'No file system handle registered', FileSystemProviderErrorCode.Unavailable);
501
}
502
503
//#endregion
504
505
private toFileSystemProviderError(error: Error): FileSystemProviderError {
506
if (error instanceof FileSystemProviderError) {
507
return error; // avoid double conversion
508
}
509
510
let code = FileSystemProviderErrorCode.Unknown;
511
if (error.name === 'NotAllowedError') {
512
error = new Error(localize('fileSystemNotAllowedError', "Insufficient permissions. Please retry and allow the operation."));
513
code = FileSystemProviderErrorCode.Unavailable;
514
}
515
516
return createFileSystemProviderError(error, code);
517
}
518
519
private createFileSystemProviderError(resource: URI, msg: string, code: FileSystemProviderErrorCode): FileSystemProviderError {
520
return createFileSystemProviderError(new Error(`${msg} (${normalize(resource.path)})`), code);
521
}
522
}
523
524