@theia/filesystem
Version:
Theia - FileSystem Extension
435 lines • 19.4 kB
JavaScript
"use strict";
// *****************************************************************************
// 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
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileSystemProviderServer = exports.RemoteFileSystemProvider = exports.RemoteFileSystemProxyFactory = exports.RemoteFileSystemProviderError = exports.RemoteFileSystemServer = exports.remoteFileSystemPath = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("@theia/core/shared/inversify");
const uri_1 = require("@theia/core/lib/common/uri");
const event_1 = require("@theia/core/lib/common/event");
const disposable_1 = require("@theia/core/lib/common/disposable");
const buffer_1 = require("@theia/core/lib/common/buffer");
const files_1 = require("./files");
const proxy_factory_1 = require("@theia/core/lib/common/messaging/proxy-factory");
const application_error_1 = require("@theia/core/lib/common/application-error");
const promise_util_1 = require("@theia/core/lib/common/promise-util");
const stream_1 = require("@theia/core/lib/common/stream");
const cancellation_1 = require("@theia/core/lib/common/cancellation");
exports.remoteFileSystemPath = '/services/remote-filesystem';
exports.RemoteFileSystemServer = Symbol('RemoteFileSystemServer');
exports.RemoteFileSystemProviderError = application_error_1.ApplicationError.declare(-33005, (message, data, stack) => ({ message, data, stack }));
class RemoteFileSystemProxyFactory extends proxy_factory_1.RpcProxyFactory {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serializeError(e) {
if (e instanceof files_1.FileSystemProviderError) {
const { code, name } = e;
return super.serializeError((0, exports.RemoteFileSystemProviderError)(e.message, { code, name }, e.stack));
}
return super.serializeError(e);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deserializeError(capturedError, e) {
const error = super.deserializeError(capturedError, e);
if (exports.RemoteFileSystemProviderError.is(error)) {
const fileOperationError = new files_1.FileSystemProviderError(error.message, error.data.code);
fileOperationError.name = error.data.name;
fileOperationError.stack = error.stack;
return fileOperationError;
}
return e;
}
}
exports.RemoteFileSystemProxyFactory = RemoteFileSystemProxyFactory;
/**
* Frontend component.
*
* Wraps the remote filesystem provider living on the backend.
*/
let RemoteFileSystemProvider = class RemoteFileSystemProvider {
constructor() {
this.onDidChangeFileEmitter = new event_1.Emitter();
this.onDidChangeFile = this.onDidChangeFileEmitter.event;
this.onFileWatchErrorEmitter = new event_1.Emitter();
this.onFileWatchError = this.onFileWatchErrorEmitter.event;
this.onDidChangeCapabilitiesEmitter = new event_1.Emitter();
this.onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event;
this.onDidChangeReadOnlyMessageEmitter = new event_1.Emitter();
this.onDidChangeReadOnlyMessage = this.onDidChangeReadOnlyMessageEmitter.event;
this.onFileStreamDataEmitter = new event_1.Emitter();
this.onFileStreamData = this.onFileStreamDataEmitter.event;
this.onFileStreamEndEmitter = new event_1.Emitter();
this.onFileStreamEnd = this.onFileStreamEndEmitter.event;
this.toDispose = new disposable_1.DisposableCollection(this.onDidChangeFileEmitter, this.onDidChangeCapabilitiesEmitter, this.onDidChangeReadOnlyMessageEmitter, this.onFileStreamDataEmitter, this.onFileStreamEndEmitter);
this.watcherSequence = 0;
/**
* We'll track the currently allocated watchers, in order to re-allocate them
* with the same options once we reconnect to the backend after a disconnection.
*/
this.watchOptions = new Map();
this._capabilities = 0 /* FileSystemProviderCapabilities.None */;
this._readOnlyMessage = undefined;
this.readyDeferred = new promise_util_1.Deferred();
this.ready = this.readyDeferred.promise;
this.streamHandleSeq = 0;
}
get capabilities() { return this._capabilities; }
get readOnlyMessage() {
return this._readOnlyMessage;
}
init() {
this.server.getCapabilities().then(capabilities => {
this._capabilities = capabilities;
this.readyDeferred.resolve();
}, this.readyDeferred.reject);
this.server.getReadOnlyMessage().then(readOnlyMessage => {
this._readOnlyMessage = readOnlyMessage;
});
this.server.setClient({
notifyDidChangeFile: ({ changes }) => {
this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new uri_1.default(event.resource), type: event.type })));
},
notifyFileWatchError: () => {
this.onFileWatchErrorEmitter.fire();
},
notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities),
notifyDidChangeReadOnlyMessage: readOnlyMessage => this.setReadOnlyMessage(readOnlyMessage),
onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, data]),
onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error])
});
const onInitialized = this.server.onDidOpenConnection(() => {
// skip reconnection on the first connection
onInitialized.dispose();
this.toDispose.push(this.server.onDidOpenConnection(() => this.reconnect()));
});
}
dispose() {
this.toDispose.dispose();
}
setCapabilities(capabilities) {
this._capabilities = capabilities;
this.onDidChangeCapabilitiesEmitter.fire(undefined);
}
setReadOnlyMessage(readOnlyMessage) {
this._readOnlyMessage = readOnlyMessage;
this.onDidChangeReadOnlyMessageEmitter.fire(readOnlyMessage);
}
// --- forwarding calls
stat(resource) {
return this.server.stat(resource.toString());
}
access(resource, mode) {
return this.server.access(resource.toString(), mode);
}
fsPath(resource) {
return this.server.fsPath(resource.toString());
}
open(resource, opts) {
return this.server.open(resource.toString(), opts);
}
close(fd) {
return this.server.close(fd);
}
async read(fd, pos, data, offset, length) {
const { bytes, bytesRead } = await this.server.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.slice(0, bytesRead), offset);
return bytesRead;
}
async readFile(resource) {
const bytes = await this.server.readFile(resource.toString());
return bytes;
}
readFileStream(resource, opts, token) {
const capturedError = new Error();
const stream = (0, stream_1.newWriteableStream)(data => buffer_1.BinaryBuffer.concat(data.map(item => buffer_1.BinaryBuffer.wrap(item))).buffer);
const streamHandle = this.streamHandleSeq++;
const toDispose = new disposable_1.DisposableCollection(token.onCancellationRequested(() => stream.end((0, cancellation_1.cancelled)())), this.onFileStreamData(([handle, data]) => {
if (streamHandle === handle) {
stream.write(data);
}
}), this.onFileStreamEnd(([handle, error]) => {
if (streamHandle === handle) {
if (error) {
const code = ('code' in error && error.code) || files_1.FileSystemProviderErrorCode.Unknown;
const fileOperationError = new files_1.FileSystemProviderError(error.message, code);
fileOperationError.name = error.name;
const capturedStack = capturedError.stack || '';
fileOperationError.stack = `${capturedStack}\nCaused by: ${error.stack}`;
stream.end(fileOperationError);
}
else {
stream.end();
}
}
}));
stream.on('end', () => toDispose.dispose());
this.server.readFileStream(resource.toString(), streamHandle, opts, token).then(() => {
if (token.isCancellationRequested) {
stream.end((0, cancellation_1.cancelled)());
}
}, error => stream.end(error));
return stream;
}
write(fd, pos, data, offset, length) {
return this.server.write(fd, pos, data, offset, length);
}
writeFile(resource, content, opts) {
return this.server.writeFile(resource.toString(), content, opts);
}
delete(resource, opts) {
return this.server.delete(resource.toString(), opts);
}
mkdir(resource) {
return this.server.mkdir(resource.toString());
}
readdir(resource) {
return this.server.readdir(resource.toString());
}
rename(resource, target, opts) {
return this.server.rename(resource.toString(), target.toString(), opts);
}
copy(resource, target, opts) {
return this.server.copy(resource.toString(), target.toString(), opts);
}
updateFile(resource, changes, opts) {
return this.server.updateFile(resource.toString(), changes, opts);
}
watch(resource, options) {
const watcherId = this.watcherSequence++;
const uri = resource.toString();
this.watchOptions.set(watcherId, { uri, options });
this.server.watch(watcherId, uri, options);
const toUnwatch = disposable_1.Disposable.create(() => {
this.watchOptions.delete(watcherId);
this.server.unwatch(watcherId);
});
this.toDispose.push(toUnwatch);
return toUnwatch;
}
/**
* When a frontend disconnects (e.g. bad connection) the backend resources will be cleared.
*
* This means that we need to re-allocate the watchers when a frontend reconnects.
*/
reconnect() {
for (const [watcher, { uri, options }] of this.watchOptions.entries()) {
this.server.watch(watcher, uri, options);
}
}
};
exports.RemoteFileSystemProvider = RemoteFileSystemProvider;
tslib_1.__decorate([
(0, inversify_1.inject)(exports.RemoteFileSystemServer),
tslib_1.__metadata("design:type", Object)
], RemoteFileSystemProvider.prototype, "server", void 0);
tslib_1.__decorate([
(0, inversify_1.postConstruct)(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], RemoteFileSystemProvider.prototype, "init", null);
exports.RemoteFileSystemProvider = RemoteFileSystemProvider = tslib_1.__decorate([
(0, inversify_1.injectable)()
], RemoteFileSystemProvider);
/**
* Backend component.
*
* JSON-RPC server exposing a wrapped file system provider remotely.
*/
let FileSystemProviderServer = class FileSystemProviderServer {
constructor() {
this.BUFFER_SIZE = 64 * 1024;
/**
* Mapping of `watcherId` to a disposable watcher handle.
*/
this.watchers = new Map();
this.toDispose = new disposable_1.DisposableCollection();
}
dispose() {
this.toDispose.dispose();
}
setClient(client) {
this.client = client;
}
init() {
if (this.provider.dispose) {
this.toDispose.push(disposable_1.Disposable.create(() => this.provider.dispose()));
}
this.toDispose.push(this.provider.onDidChangeCapabilities(() => {
if (this.client) {
this.client.notifyDidChangeCapabilities(this.provider.capabilities);
}
}));
if (files_1.ReadOnlyMessageFileSystemProvider.is(this.provider)) {
const providerWithReadOnlyMessage = this.provider;
this.toDispose.push(this.provider.onDidChangeReadOnlyMessage(() => {
if (this.client) {
this.client.notifyDidChangeReadOnlyMessage(providerWithReadOnlyMessage.readOnlyMessage);
}
}));
}
this.toDispose.push(this.provider.onDidChangeFile(changes => {
if (this.client) {
this.client.notifyDidChangeFile({
changes: changes.map(({ resource, type }) => ({ resource: resource.toString(), type }))
});
}
}));
this.toDispose.push(this.provider.onFileWatchError(() => {
if (this.client) {
this.client.notifyFileWatchError();
}
}));
}
async getCapabilities() {
return this.provider.capabilities;
}
async getReadOnlyMessage() {
if (files_1.ReadOnlyMessageFileSystemProvider.is(this.provider)) {
return this.provider.readOnlyMessage;
}
else {
return undefined;
}
}
stat(resource) {
return this.provider.stat(new uri_1.default(resource));
}
access(resource, mode) {
if ((0, files_1.hasAccessCapability)(this.provider)) {
return this.provider.access(new uri_1.default(resource), mode);
}
throw new Error('not supported');
}
async fsPath(resource) {
if ((0, files_1.hasAccessCapability)(this.provider)) {
return this.provider.fsPath(new uri_1.default(resource));
}
throw new Error('not supported');
}
open(resource, opts) {
if ((0, files_1.hasOpenReadWriteCloseCapability)(this.provider)) {
return this.provider.open(new uri_1.default(resource), opts);
}
throw new Error('not supported');
}
close(fd) {
if ((0, files_1.hasOpenReadWriteCloseCapability)(this.provider)) {
return this.provider.close(fd);
}
throw new Error('not supported');
}
async read(fd, pos, length) {
if ((0, files_1.hasOpenReadWriteCloseCapability)(this.provider)) {
const buffer = buffer_1.BinaryBuffer.alloc(this.BUFFER_SIZE);
const bytes = buffer.buffer;
const bytesRead = await this.provider.read(fd, pos, bytes, 0, length);
return { bytes, bytesRead };
}
throw new Error('not supported');
}
write(fd, pos, data, offset, length) {
if ((0, files_1.hasOpenReadWriteCloseCapability)(this.provider)) {
return this.provider.write(fd, pos, data, offset, length);
}
throw new Error('not supported');
}
async readFile(resource) {
if ((0, files_1.hasReadWriteCapability)(this.provider)) {
return this.provider.readFile(new uri_1.default(resource));
}
throw new Error('not supported');
}
writeFile(resource, content, opts) {
if ((0, files_1.hasReadWriteCapability)(this.provider)) {
return this.provider.writeFile(new uri_1.default(resource), content, opts);
}
throw new Error('not supported');
}
delete(resource, opts) {
return this.provider.delete(new uri_1.default(resource), opts);
}
mkdir(resource) {
return this.provider.mkdir(new uri_1.default(resource));
}
readdir(resource) {
return this.provider.readdir(new uri_1.default(resource));
}
rename(source, target, opts) {
return this.provider.rename(new uri_1.default(source), new uri_1.default(target), opts);
}
copy(source, target, opts) {
if ((0, files_1.hasFileFolderCopyCapability)(this.provider)) {
return this.provider.copy(new uri_1.default(source), new uri_1.default(target), opts);
}
throw new Error('not supported');
}
updateFile(resource, changes, opts) {
if ((0, files_1.hasUpdateCapability)(this.provider)) {
return this.provider.updateFile(new uri_1.default(resource), changes, opts);
}
throw new Error('not supported');
}
async watch(requestedWatcherId, resource, opts) {
if (this.watchers.has(requestedWatcherId)) {
throw new Error('watcher id is already allocated!');
}
const watcher = this.provider.watch(new uri_1.default(resource), opts);
this.watchers.set(requestedWatcherId, watcher);
this.toDispose.push(disposable_1.Disposable.create(() => this.unwatch(requestedWatcherId)));
}
async unwatch(watcherId) {
const watcher = this.watchers.get(watcherId);
if (watcher) {
this.watchers.delete(watcherId);
watcher.dispose();
}
}
async readFileStream(resource, handle, opts, token) {
if ((0, files_1.hasFileReadStreamCapability)(this.provider)) {
const stream = this.provider.readFileStream(new uri_1.default(resource), opts, token);
stream.on('data', data => { var _a; return (_a = this.client) === null || _a === void 0 ? void 0 : _a.onFileStreamData(handle, data); });
stream.on('error', error => {
var _a;
const code = error instanceof files_1.FileSystemProviderError ? error.code : undefined;
const { name, message, stack } = error;
(_a = this.client) === null || _a === void 0 ? void 0 : _a.onFileStreamEnd(handle, { code, name, message, stack });
});
stream.on('end', () => { var _a; return (_a = this.client) === null || _a === void 0 ? void 0 : _a.onFileStreamEnd(handle, undefined); });
}
else {
throw new Error('not supported');
}
}
};
exports.FileSystemProviderServer = FileSystemProviderServer;
tslib_1.__decorate([
(0, inversify_1.inject)(files_1.FileSystemProvider),
tslib_1.__metadata("design:type", Object)
], FileSystemProviderServer.prototype, "provider", void 0);
tslib_1.__decorate([
(0, inversify_1.postConstruct)(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], FileSystemProviderServer.prototype, "init", null);
exports.FileSystemProviderServer = FileSystemProviderServer = tslib_1.__decorate([
(0, inversify_1.injectable)()
], FileSystemProviderServer);
//# sourceMappingURL=remote-file-system-provider.js.map