// ***************************************************************************** // 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 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/browser/mainThreadFileSystem.ts /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/tslint/config */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { URI } from '@theia/core/shared/vscode-uri'; import { interfaces } from '@theia/core/shared/inversify'; import CoreURI from '@theia/core/lib/common/uri'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt, IFileChangeDto } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; import { FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions, FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, FileStat, FileChange, FileOperationError, FileOperationResult, ReadOnlyMessageFileSystemProvider } from '@theia/filesystem/lib/common/files'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { MarkdownString } from '../../common/plugin-api-rpc-model'; type IDisposable = Disposable; export class FileSystemMainImpl implements FileSystemMain, Disposable { private readonly _proxy: FileSystemExt; private readonly _fileProvider = new Map(); private readonly _fileService: FileService; private readonly _disposables = new DisposableCollection(); constructor(rpc: RPCProtocol, container: interfaces.Container) { this._proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT); this._fileService = container.get(FileService); for (const { scheme, capabilities } of this._fileService.listCapabilities()) { this._proxy.$acceptProviderInfos(scheme, capabilities); } this._disposables.push(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider?.capabilities))); this._disposables.push(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider.capabilities))); this._disposables.push(Disposable.create(() => this._fileProvider.forEach(value => value.dispose()))); this._disposables.push(Disposable.create(() => this._fileProvider.clear())); } dispose(): void { this._disposables.dispose(); } $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void { this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy, readonlyMessage)); } $unregisterProvider(handle: number): void { const provider = this._fileProvider.get(handle); if (provider) { provider.dispose(); this._fileProvider.delete(handle); } } $onFileSystemChange(handle: number, changes: IFileChangeDto[]): void { const fileProvider = this._fileProvider.get(handle); if (!fileProvider) { throw new Error('Unknown file provider'); } fileProvider.$onFileSystemChange(changes); } // --- consumer fs, vscode.workspace.fs $stat(uri: UriComponents): Promise { return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: true }).then(stat => ({ ctime: stat.ctime, mtime: stat.mtime, size: stat.size, type: FileStat.asFileType(stat) })).catch(FileSystemMainImpl._handleError); } $readdir(uri: UriComponents): Promise<[string, FileType][]> { return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: false }).then(stat => { if (!stat.isDirectory) { const err = new Error(stat.name); err.name = FileSystemProviderErrorCode.FileNotADirectory; throw err; } return !stat.children ? [] : stat.children.map(child => [child.name, FileStat.asFileType(child)] as [string, FileType]); }).catch(FileSystemMainImpl._handleError); } $readFile(uri: UriComponents): Promise { return this._fileService.readFile(new CoreURI(URI.revive(uri))).then(file => file.value).catch(FileSystemMainImpl._handleError); } $writeFile(uri: UriComponents, content: BinaryBuffer): Promise { return this._fileService.writeFile(new CoreURI(URI.revive(uri)), content) .then(() => undefined).catch(FileSystemMainImpl._handleError); } $rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { return this._fileService.move(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), { ...opts, fromUserGesture: false }).then(() => undefined).catch(FileSystemMainImpl._handleError); } $copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { return this._fileService.copy(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), { ...opts, fromUserGesture: false }).then(() => undefined).catch(FileSystemMainImpl._handleError); } $mkdir(uri: UriComponents): Promise { return this._fileService.createFolder(new CoreURI(URI.revive(uri)), { fromUserGesture: false }) .then(() => undefined).catch(FileSystemMainImpl._handleError); } $delete(uri: UriComponents, opts: FileDeleteOptions): Promise { return this._fileService.delete(new CoreURI(URI.revive(uri)), opts).catch(FileSystemMainImpl._handleError); } private static _handleError(err: any): never { if (err instanceof FileOperationError) { switch (err.fileOperationResult) { case FileOperationResult.FILE_NOT_FOUND: err.name = FileSystemProviderErrorCode.FileNotFound; break; case FileOperationResult.FILE_IS_DIRECTORY: err.name = FileSystemProviderErrorCode.FileIsADirectory; break; case FileOperationResult.FILE_PERMISSION_DENIED: err.name = FileSystemProviderErrorCode.NoPermissions; break; case FileOperationResult.FILE_MOVE_CONFLICT: err.name = FileSystemProviderErrorCode.FileExists; break; } } throw err; } } class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, ReadOnlyMessageFileSystemProvider { private readonly _onDidChange = new Emitter(); private readonly _registration: IDisposable; readonly onDidChangeFile: Event = this._onDidChange.event; readonly onFileWatchError: Event = new Emitter().event; // dummy, never fired readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event = Event.None; readonly onDidChangeReadOnlyMessage: Event = Event.None; constructor( fileService: FileService, scheme: string, capabilities: FileSystemProviderCapabilities, private readonly _handle: number, private readonly _proxy: FileSystemExt, public readonly readOnlyMessage: MarkdownString | undefined = undefined ) { this.capabilities = capabilities; this._registration = fileService.registerProvider(scheme, this); } dispose(): void { this._registration.dispose(); this._onDidChange.dispose(); } watch(resource: CoreURI, opts: WatchOptions) { const session = Math.random(); this._proxy.$watch(this._handle, session, resource['codeUri'], opts); return Disposable.create(() => { this._proxy.$unwatch(this._handle, session); }); } $onFileSystemChange(changes: IFileChangeDto[]): void { this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange)); } private static _createFileChange(dto: IFileChangeDto): FileChange { return { resource: new CoreURI(URI.revive(dto.resource)), type: dto.type }; } // --- forwarding calls stat(resource: CoreURI): Promise { return this._proxy.$stat(this._handle, resource['codeUri']).then(undefined, err => { throw err; }); } readFile(resource: CoreURI): Promise { return this._proxy.$readFile(this._handle, resource['codeUri']).then(buffer => buffer.buffer); } writeFile(resource: CoreURI, content: Uint8Array, opts: FileWriteOptions): Promise { return this._proxy.$writeFile(this._handle, resource['codeUri'], BinaryBuffer.wrap(content), opts); } delete(resource: CoreURI, opts: FileDeleteOptions): Promise { return this._proxy.$delete(this._handle, resource['codeUri'], opts); } mkdir(resource: CoreURI): Promise { return this._proxy.$mkdir(this._handle, resource['codeUri']); } readdir(resource: CoreURI): Promise<[string, FileType][]> { return this._proxy.$readdir(this._handle, resource['codeUri']); } rename(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise { return this._proxy.$rename(this._handle, resource['codeUri'], target['codeUri'], opts); } copy(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise { return this._proxy.$copy(this._handle, resource['codeUri'], target['codeUri'], opts); } open(resource: CoreURI, opts: FileOpenOptions): Promise { return this._proxy.$open(this._handle, resource['codeUri'], opts); } close(fd: number): Promise { return this._proxy.$close(this._handle, fd); } read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return this._proxy.$read(this._handle, fd, pos, length).then(readData => { data.set(readData.buffer, offset); return readData.byteLength; }); } write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return this._proxy.$write(this._handle, fd, pos, BinaryBuffer.wrap(data).slice(offset, offset + length)); } }