// ***************************************************************************** // Copyright (C) 2020 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import URI from '@theia/core/lib/common/uri'; import { Event, Emitter, CancellationToken } from '@theia/core/lib/common'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { FileSystemProvider, FileSystemProviderCapabilities, WatchOptions, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileChange, Stat, FileType, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, hasAccessCapability, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability } from './files'; import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; import { ReadableStreamEvents } from '@theia/core/lib/common/stream'; export class DelegatingFileSystemProvider implements Required, Disposable { private readonly onDidChangeFileEmitter = new Emitter(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; private readonly onFileWatchErrorEmitter = new Emitter(); readonly onFileWatchError = this.onFileWatchErrorEmitter.event; constructor( protected readonly delegate: FileSystemProvider, protected readonly options: DelegatingFileSystemProvider.Options, protected readonly toDispose = new DisposableCollection() ) { this.toDispose.push(this.onDidChangeFileEmitter); this.toDispose.push(delegate.onDidChangeFile(changes => this.handleFileChanges(changes))); this.toDispose.push(this.onFileWatchErrorEmitter); this.toDispose.push(delegate.onFileWatchError(changes => this.onFileWatchErrorEmitter.fire())); } dispose(): void { this.toDispose.dispose(); } get capabilities(): FileSystemProviderCapabilities { return this.delegate.capabilities; } get onDidChangeCapabilities(): Event { return this.delegate.onDidChangeCapabilities; } watch(resource: URI, opts: WatchOptions): Disposable { return this.delegate.watch(this.toUnderlyingResource(resource), opts); } stat(resource: URI): Promise { return this.delegate.stat(this.toUnderlyingResource(resource)); } access(resource: URI, mode?: number): Promise { if (hasAccessCapability(this.delegate)) { return this.delegate.access(this.toUnderlyingResource(resource), mode); } throw new Error('not supported'); } fsPath(resource: URI): Promise { if (hasAccessCapability(this.delegate)) { return this.delegate.fsPath(this.toUnderlyingResource(resource)); } throw new Error('not supported'); } mkdir(resource: URI): Promise { return this.delegate.mkdir(this.toUnderlyingResource(resource)); } rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { return this.delegate.rename(this.toUnderlyingResource(from), this.toUnderlyingResource(to), opts); } copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise { if (hasFileFolderCopyCapability(this.delegate)) { return this.delegate.copy(this.toUnderlyingResource(from), this.toUnderlyingResource(to), opts); } throw new Error('not supported'); } readFile(resource: URI): Promise { if (hasReadWriteCapability(this.delegate)) { return this.delegate.readFile(this.toUnderlyingResource(resource)); } throw new Error('not supported'); } readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { if (hasFileReadStreamCapability(this.delegate)) { return this.delegate.readFileStream(this.toUnderlyingResource(resource), opts, token); } throw new Error('not supported'); } readdir(resource: URI): Promise<[string, FileType][]> { return this.delegate.readdir(this.toUnderlyingResource(resource)); } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { if (hasReadWriteCapability(this.delegate)) { return this.delegate.writeFile(this.toUnderlyingResource(resource), content, opts); } throw new Error('not supported'); } open(resource: URI, opts: FileOpenOptions): Promise { if (hasOpenReadWriteCloseCapability(this.delegate)) { return this.delegate.open(this.toUnderlyingResource(resource), opts); } throw new Error('not supported'); } close(fd: number): Promise { if (hasOpenReadWriteCloseCapability(this.delegate)) { return this.delegate.close(fd); } throw new Error('not supported'); } read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { if (hasOpenReadWriteCloseCapability(this.delegate)) { return this.delegate.read(fd, pos, data, offset, length); } throw new Error('not supported'); } write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { if (hasOpenReadWriteCloseCapability(this.delegate)) { return this.delegate.write(fd, pos, data, offset, length); } throw new Error('not supported'); } delete(resource: URI, opts: FileDeleteOptions): Promise { return this.delegate.delete(this.toUnderlyingResource(resource), opts); } updateFile(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { if (hasUpdateCapability(this.delegate)) { return this.delegate.updateFile(resource, changes, opts); } throw new Error('not supported'); } protected handleFileChanges(changes: readonly FileChange[]): void { const delegatingChanges: FileChange[] = []; for (const change of changes) { const delegatingResource = this.fromUnderlyingResource(change.resource); if (delegatingResource) { delegatingChanges.push({ resource: delegatingResource, type: change.type }); } } if (delegatingChanges.length) { this.onDidChangeFileEmitter.fire(delegatingChanges); } } /** * Converts to an underlying fs provider resource format. * * For example converting `user-storage` resources to `file` resources under a user home: * user-storage:/user/settings.json => file://home/.theia/settings.json */ toUnderlyingResource(resource: URI): URI { const underlying = this.options.uriConverter.to(resource); if (!underlying) { throw new Error('invalid resource: ' + resource.toString()); } return underlying; } /** * Converts from an underlying fs provider resource format. * * For example converting `file` resources under a user home to `user-storage` resource: * - file://home/.theia/settings.json => user-storage:/user/settings.json * - file://documents/some-document.txt => undefined */ fromUnderlyingResource(resource: URI): URI | undefined { return this.options.uriConverter.from(resource); } } export namespace DelegatingFileSystemProvider { export interface Options { uriConverter: URIConverter } export interface URIConverter { /** * Converts to an underlying fs provider resource format. * Returns undefined if the given resource is not valid resource. * * For example converting `user-storage` resources to `file` resources under a user home: * user-storage:/user/settings.json => file://home/.theia/settings.json * user-storage:/settings.json => undefined */ to(resource: URI): URI | undefined; /** * Converts from an underlying fs provider resource format. * * For example converting `file` resources under a user home to `user-storage` resource: * - file://home/.theia/settings.json => user-storage:/settings.json * - file://documents/some-document.txt => undefined */ from(resource: URI): URI | undefined; } }