Path: blob/main/src/vs/platform/files/common/diskFileSystemProviderClient.ts
3296 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, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } 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.FileClone |59FileSystemProviderCapabilities.FileRealpath;6061if (this.extraCapabilities.pathCaseSensitive) {62this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;63}6465if (this.extraCapabilities.trash) {66this._capabilities |= FileSystemProviderCapabilities.Trash;67}68}6970return this._capabilities;71}7273//#endregion7475//#region File Metadata Resolving7677stat(resource: URI): Promise<IStat> {78return this.channel.call('stat', [resource]);79}8081realpath(resource: URI): Promise<string> {82return this.channel.call('realpath', [resource]);83}8485readdir(resource: URI): Promise<[string, FileType][]> {86return this.channel.call('readdir', [resource]);87}8889//#endregion9091//#region File Reading/Writing9293async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array> {94const { buffer } = await this.channel.call('readFile', [resource, opts]) as VSBuffer;9596return buffer;97}9899readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {100const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);101const disposables = new DisposableStore();102103// Reading as file stream goes through an event to the remote side104disposables.add(this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {105106// data107if (dataOrErrorOrEnd instanceof VSBuffer) {108stream.write(dataOrErrorOrEnd.buffer);109}110111// end or error112else {113if (dataOrErrorOrEnd === 'end') {114stream.end();115} else {116let error: Error;117118// Take Error as is if type matches119if (dataOrErrorOrEnd instanceof Error) {120error = dataOrErrorOrEnd;121}122123// Otherwise, try to deserialize into an error.124// Since we communicate via IPC, we cannot be sure125// that Error objects are properly serialized.126else {127const errorCandidate = dataOrErrorOrEnd as IFileSystemProviderError;128129error = createFileSystemProviderError(errorCandidate.message ?? toErrorMessage(errorCandidate), errorCandidate.code ?? FileSystemProviderErrorCode.Unknown);130}131132stream.error(error);133stream.end();134}135136// Signal to the remote side that we no longer listen137disposables.dispose();138}139}));140141// Support cancellation142disposables.add(token.onCancellationRequested(() => {143144// Ensure to end the stream properly with an error145// to indicate the cancellation.146stream.error(canceled());147stream.end();148149// Ensure to dispose the listener upon cancellation. This will150// bubble through the remote side as event and allows to stop151// reading the file.152disposables.dispose();153}));154155return stream;156}157158writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {159return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);160}161162open(resource: URI, opts: IFileOpenOptions): Promise<number> {163return this.channel.call('open', [resource, opts]);164}165166close(fd: number): Promise<void> {167return this.channel.call('close', [fd]);168}169170async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {171const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);172173// copy back the data that was written into the buffer on the remote174// side. we need to do this because buffers are not referenced by175// pointer, but only by value and as such cannot be directly written176// to from the other process.177data.set(bytes.buffer.slice(0, bytesRead), offset);178179return bytesRead;180}181182write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {183return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);184}185186//#endregion187188//#region Move/Copy/Delete/Create Folder189190mkdir(resource: URI): Promise<void> {191return this.channel.call('mkdir', [resource]);192}193194delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {195return this.channel.call('delete', [resource, opts]);196}197198rename(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {199return this.channel.call('rename', [resource, target, opts]);200}201202copy(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {203return this.channel.call('copy', [resource, target, opts]);204}205206//#endregion207208//#region Clone File209210cloneFile(resource: URI, target: URI): Promise<void> {211return this.channel.call('cloneFile', [resource, target]);212}213214//#endregion215216//#region File Watching217218private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());219readonly onDidChangeFile = this._onDidChange.event;220221private readonly _onDidWatchError = this._register(new Emitter<string>());222readonly onDidWatchError = this._onDidWatchError.event;223224// The contract for file watching via remote is to identify us225// via a unique but readonly session ID. Since the remote is226// managing potentially many watchers from different clients,227// this helps the server to properly partition events to the right228// clients.229private readonly sessionId = generateUuid();230231private registerFileChangeListeners(): void {232233// The contract for file changes is that there is one listener234// for both events and errors from the watcher. So we need to235// unwrap the event from the remote and emit through the proper236// emitter.237this._register(this.channel.listen<IFileChange[] | string>('fileChange', [this.sessionId])(eventsOrError => {238if (Array.isArray(eventsOrError)) {239const events = eventsOrError;240this._onDidChange.fire(reviveFileChanges(events));241} else {242const error = eventsOrError;243this._onDidWatchError.fire(error);244}245}));246}247248watch(resource: URI, opts: IWatchOptions): IDisposable {249250// Generate a request UUID to correlate the watcher251// back to us when we ask to dispose the watcher later.252const req = generateUuid();253254this.channel.call('watch', [this.sessionId, req, resource, opts]);255256return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req]));257}258259//#endregion260}261262263