mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-14 04:00:38 +01:00
256 lines
9.1 KiB
TypeScript
256 lines
9.1 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { VSBuffer } from 'vs/base/common/buffer';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
|
import { canceled } from 'vs/base/common/errors';
|
|
import { Emitter, Event } from 'vs/base/common/event';
|
|
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
|
import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream';
|
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
|
import { generateUuid } from 'vs/base/common/uuid';
|
|
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
|
import { createFileSystemProviderError, IFileAtomicReadOptions, FileChangeType, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files';
|
|
|
|
export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem';
|
|
|
|
/**
|
|
* An implementation of a local disk file system provider
|
|
* that is backed by a `IChannel` and thus implemented via
|
|
* IPC on a different process.
|
|
*/
|
|
export class DiskFileSystemProviderClient extends Disposable implements
|
|
IFileSystemProviderWithFileReadWriteCapability,
|
|
IFileSystemProviderWithOpenReadWriteCloseCapability,
|
|
IFileSystemProviderWithFileReadStreamCapability,
|
|
IFileSystemProviderWithFileFolderCopyCapability,
|
|
IFileSystemProviderWithFileAtomicReadCapability,
|
|
IFileSystemProviderWithFileCloneCapability {
|
|
|
|
constructor(
|
|
private readonly channel: IChannel,
|
|
private readonly extraCapabilities: { trash?: boolean; pathCaseSensitive?: boolean }
|
|
) {
|
|
super();
|
|
|
|
this.registerFileChangeListeners();
|
|
}
|
|
|
|
//#region File Capabilities
|
|
|
|
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
|
|
|
private _capabilities: FileSystemProviderCapabilities | undefined;
|
|
get capabilities(): FileSystemProviderCapabilities {
|
|
if (!this._capabilities) {
|
|
this._capabilities =
|
|
FileSystemProviderCapabilities.FileReadWrite |
|
|
FileSystemProviderCapabilities.FileOpenReadWriteClose |
|
|
FileSystemProviderCapabilities.FileReadStream |
|
|
FileSystemProviderCapabilities.FileFolderCopy |
|
|
FileSystemProviderCapabilities.FileWriteUnlock |
|
|
FileSystemProviderCapabilities.FileAtomicRead |
|
|
FileSystemProviderCapabilities.FileAtomicWrite |
|
|
FileSystemProviderCapabilities.FileAtomicDelete |
|
|
FileSystemProviderCapabilities.FileClone;
|
|
|
|
if (this.extraCapabilities.pathCaseSensitive) {
|
|
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
|
}
|
|
|
|
if (this.extraCapabilities.trash) {
|
|
this._capabilities |= FileSystemProviderCapabilities.Trash;
|
|
}
|
|
}
|
|
|
|
return this._capabilities;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region File Metadata Resolving
|
|
|
|
stat(resource: URI): Promise<IStat> {
|
|
return this.channel.call('stat', [resource]);
|
|
}
|
|
|
|
readdir(resource: URI): Promise<[string, FileType][]> {
|
|
return this.channel.call('readdir', [resource]);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region File Reading/Writing
|
|
|
|
async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise<Uint8Array> {
|
|
const { buffer } = await this.channel.call('readFile', [resource, opts]) as VSBuffer;
|
|
|
|
return buffer;
|
|
}
|
|
|
|
readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
|
|
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
|
|
const disposables = new DisposableStore();
|
|
|
|
// Reading as file stream goes through an event to the remote side
|
|
disposables.add(this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
|
|
|
|
// data
|
|
if (dataOrErrorOrEnd instanceof VSBuffer) {
|
|
stream.write(dataOrErrorOrEnd.buffer);
|
|
}
|
|
|
|
// end or error
|
|
else {
|
|
if (dataOrErrorOrEnd === 'end') {
|
|
stream.end();
|
|
} else {
|
|
let error: Error;
|
|
|
|
// Take Error as is if type matches
|
|
if (dataOrErrorOrEnd instanceof Error) {
|
|
error = dataOrErrorOrEnd;
|
|
}
|
|
|
|
// Otherwise, try to deserialize into an error.
|
|
// Since we communicate via IPC, we cannot be sure
|
|
// that Error objects are properly serialized.
|
|
else {
|
|
const errorCandidate = dataOrErrorOrEnd as IFileSystemProviderError;
|
|
|
|
error = createFileSystemProviderError(errorCandidate.message ?? toErrorMessage(errorCandidate), errorCandidate.code ?? FileSystemProviderErrorCode.Unknown);
|
|
}
|
|
|
|
stream.error(error);
|
|
stream.end();
|
|
}
|
|
|
|
// Signal to the remote side that we no longer listen
|
|
disposables.dispose();
|
|
}
|
|
}));
|
|
|
|
// Support cancellation
|
|
disposables.add(token.onCancellationRequested(() => {
|
|
|
|
// Ensure to end the stream properly with an error
|
|
// to indicate the cancellation.
|
|
stream.error(canceled());
|
|
stream.end();
|
|
|
|
// Ensure to dispose the listener upon cancellation. This will
|
|
// bubble through the remote side as event and allows to stop
|
|
// reading the file.
|
|
disposables.dispose();
|
|
}));
|
|
|
|
return stream;
|
|
}
|
|
|
|
writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
|
|
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
|
|
}
|
|
|
|
open(resource: URI, opts: IFileOpenOptions): Promise<number> {
|
|
return this.channel.call('open', [resource, opts]);
|
|
}
|
|
|
|
close(fd: number): Promise<void> {
|
|
return this.channel.call('close', [fd]);
|
|
}
|
|
|
|
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
|
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
|
|
|
|
// copy back the data that was written into the buffer on the remote
|
|
// side. we need to do this because buffers are not referenced by
|
|
// pointer, but only by value and as such cannot be directly written
|
|
// to from the other process.
|
|
data.set(bytes.buffer.slice(0, bytesRead), offset);
|
|
|
|
return bytesRead;
|
|
}
|
|
|
|
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
|
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Move/Copy/Delete/Create Folder
|
|
|
|
mkdir(resource: URI): Promise<void> {
|
|
return this.channel.call('mkdir', [resource]);
|
|
}
|
|
|
|
delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
|
|
return this.channel.call('delete', [resource, opts]);
|
|
}
|
|
|
|
rename(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {
|
|
return this.channel.call('rename', [resource, target, opts]);
|
|
}
|
|
|
|
copy(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise<void> {
|
|
return this.channel.call('copy', [resource, target, opts]);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Clone File
|
|
|
|
cloneFile(resource: URI, target: URI): Promise<void> {
|
|
return this.channel.call('cloneFile', [resource, target]);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region File Watching
|
|
|
|
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
|
|
readonly onDidChangeFile = this._onDidChange.event;
|
|
|
|
private readonly _onDidWatchError = this._register(new Emitter<string>());
|
|
readonly onDidWatchError = this._onDidWatchError.event;
|
|
|
|
// The contract for file watching via remote is to identify us
|
|
// via a unique but readonly session ID. Since the remote is
|
|
// managing potentially many watchers from different clients,
|
|
// this helps the server to properly partition events to the right
|
|
// clients.
|
|
private readonly sessionId = generateUuid();
|
|
|
|
private registerFileChangeListeners(): void {
|
|
|
|
// The contract for file changes is that there is one listener
|
|
// for both events and errors from the watcher. So we need to
|
|
// unwrap the event from the remote and emit through the proper
|
|
// emitter.
|
|
this._register(this.channel.listen<{ resource: UriComponents; type: FileChangeType }[] | string>('fileChange', [this.sessionId])(eventsOrError => {
|
|
if (Array.isArray(eventsOrError)) {
|
|
const events = eventsOrError;
|
|
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
|
|
} else {
|
|
const error = eventsOrError;
|
|
this._onDidWatchError.fire(error);
|
|
}
|
|
}));
|
|
}
|
|
|
|
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
|
|
|
// Generate a request UUID to correlate the watcher
|
|
// back to us when we ask to dispose the watcher later.
|
|
const req = generateUuid();
|
|
|
|
this.channel.call('watch', [this.sessionId, req, resource, opts]);
|
|
|
|
return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req]));
|
|
}
|
|
|
|
//#endregion
|
|
}
|