Path: blob/main/src/vs/platform/files/common/diskFileSystemProviderClient.ts
5237 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 { CancellationToken } from '../../../base/common/cancellation.js';7import { toErrorMessage } from '../../../base/common/errorMessage.js';8import { canceled } from '../../../base/common/errors.js';9import { Emitter, Event } from '../../../base/common/event.js';10import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';11import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from '../../../base/common/stream.js';12import { URI } from '../../../base/common/uri.js';13import { generateUuid } from '../../../base/common/uuid.js';14import { IChannel } from '../../../base/parts/ipc/common/ipc.js';15import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileAtomicReadOptions, IFileChange, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, IFileSystemProviderError, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileWriteOptions, IStat, IWatchOptions } from './files.js';16import { reviveFileChanges } from './watcher.js';1718export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem';1920/**21* An implementation of a local disk file system provider22* that is backed by a `IChannel` and thus implemented via23* IPC on a different process.24*/25export class DiskFileSystemProviderClient extends Disposable implements26IFileSystemProviderWithFileReadWriteCapability,27IFileSystemProviderWithOpenReadWriteCloseCapability,28IFileSystemProviderWithFileReadStreamCapability,29IFileSystemProviderWithFileFolderCopyCapability,30IFileSystemProviderWithFileAtomicReadCapability,31IFileSystemProviderWithFileCloneCapability {3233constructor(34private readonly channel: IChannel,35private readonly extraCapabilities: { trash?: boolean; pathCaseSensitive?: boolean }36) {37super();3839this.registerFileChangeListeners();40}4142//#region File Capabilities4344readonly onDidChangeCapabilities: Event<void> = Event.None;4546private _capabilities: FileSystemProviderCapabilities | undefined;47get capabilities(): FileSystemProviderCapabilities {48if (!this._capabilities) {49this._capabilities =50FileSystemProviderCapabilities.FileReadWrite |51FileSystemProviderCapabilities.FileOpenReadWriteClose |52FileSystemProviderCapabilities.FileReadStream |53FileSystemProviderCapabilities.FileFolderCopy |54FileSystemProviderCapabilities.FileWriteUnlock |55FileSystemProviderCapabilities.FileAtomicRead |56FileSystemProviderCapabilities.FileAtomicWrite |57FileSystemProviderCapabilities.FileAtomicDelete |58FileSystemProviderCapabilities.FileAppend |59FileSystemProviderCapabilities.FileClone |60FileSystemProviderCapabilities.FileRealpath;6162if (this.extraCapabilities.pathCaseSensitive) {63this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;64}6566if (this.extraCapabilities.trash) {67this._capabilities |= FileSystemProviderCapabilities.Trash;68}69}7071return this._capabilities;72}7374//#endregion7576//#region File Metadata Resolving7778stat(resource: URI): Promise<IStat> {79return this.channel.call('stat', [resource]);80}8182realpath(resource: URI): Promise<string> {83return this.channel.call('realpath', [resource]);84}8586readdir(resource: URI): Promise<[string, FileType][]> {87return this.channel.call('readdir', [resource]);88}8990//#endregion9192//#region File Reading/Writing9394async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array> {95const { buffer } = await this.channel.call('readFile', [resource, opts]) as VSBuffer;9697return buffer;98}99100readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {101const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);102const disposables = new DisposableStore();103104// Reading as file stream goes through an event to the remote side105disposables.add(this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {106107// data108if (dataOrErrorOrEnd instanceof VSBuffer) {109stream.write(dataOrErrorOrEnd.buffer);110}111112// end or error113else {114if (dataOrErrorOrEnd === 'end') {115stream.end();116} else {117let error: Error;118119// Take Error as is if type matches120if (dataOrErrorOrEnd instanceof Error) {121error = dataOrErrorOrEnd;122}123124// Otherwise, try to deserialize into an error.125// Since we communicate via IPC, we cannot be sure126// that Error objects are properly serialized.127else {128const errorCandidate = dataOrErrorOrEnd as IFileSystemProviderError;129130error = createFileSystemProviderError(errorCandidate.message ?? toErrorMessage(errorCandidate), errorCandidate.code ?? FileSystemProviderErrorCode.Unknown);131}132133stream.error(error);134stream.end();135}136137// Signal to the remote side that we no longer listen138disposables.dispose();139}140}));141142// Support cancellation143disposables.add(token.onCancellationRequested(() => {144145// Ensure to end the stream properly with an error146// to indicate the cancellation.147stream.error(canceled());148stream.end();149150// Ensure to dispose the listener upon cancellation. This will151// bubble through the remote side as event and allows to stop152// reading the file.153disposables.dispose();154}));155156return stream;157}158159writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {160return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);161}162163open(resource: URI, opts: IFileOpenOptions): Promise<number> {164return this.channel.call('open', [resource, opts]);165}166167close(fd: number): Promise<void> {168return this.channel.call('close', [fd]);169}170171async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {172const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);173174// copy back the data that was written into the buffer on the remote175// side. we need to do this because buffers are not referenced by176// pointer, but only by value and as such cannot be directly written177// to from the other process.178data.set(bytes.buffer.slice(0, bytesRead), offset);179180return bytesRead;181}182183write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {184return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);185}186187//#endregion188189//#region Move/Copy/Delete/Create Folder190191mkdir(resource: URI): Promise<void> {192return this.channel.call('mkdir', [resource]);193}194195delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {196return this.channel.call('delete', [resource, opts]);197}198199rename(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {200return this.channel.call('rename', [resource, target, opts]);201}202203copy(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {204return this.channel.call('copy', [resource, target, opts]);205}206207//#endregion208209//#region Clone File210211cloneFile(resource: URI, target: URI): Promise<void> {212return this.channel.call('cloneFile', [resource, target]);213}214215//#endregion216217//#region File Watching218219private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());220readonly onDidChangeFile = this._onDidChange.event;221222private readonly _onDidWatchError = this._register(new Emitter<string>());223readonly onDidWatchError = this._onDidWatchError.event;224225// The contract for file watching via remote is to identify us226// via a unique but readonly session ID. Since the remote is227// managing potentially many watchers from different clients,228// this helps the server to properly partition events to the right229// clients.230private readonly sessionId = generateUuid();231232private registerFileChangeListeners(): void {233234// The contract for file changes is that there is one listener235// for both events and errors from the watcher. So we need to236// unwrap the event from the remote and emit through the proper237// emitter.238this._register(this.channel.listen<IFileChange[] | string>('fileChange', [this.sessionId])(eventsOrError => {239if (Array.isArray(eventsOrError)) {240const events = eventsOrError;241this._onDidChange.fire(reviveFileChanges(events));242} else {243const error = eventsOrError;244this._onDidWatchError.fire(error);245}246}));247}248249watch(resource: URI, opts: IWatchOptions): IDisposable {250251// Generate a request UUID to correlate the watcher252// back to us when we ask to dispose the watcher later.253const req = generateUuid();254255this.channel.call('watch', [this.sessionId, req, resource, opts]);256257return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req]));258}259260//#endregion261}262263264