Skip to content

Instantly share code, notes, and snippets.

@sqs
Created July 26, 2017 08:17
Show Gist options
  • Save sqs/3c8dc223d5d418f09cec4a68020d9038 to your computer and use it in GitHub Desktop.
Save sqs/3c8dc223d5d418f09cec4a68020d9038 to your computer and use it in GitHub Desktop.
RemoteFileService with lazy-loaded extensions
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IUpdateContentOptions, FileChangesEvent, FileChangeType, IImportResult } from 'vs/platform/files/common/files';
import { TPromise } from "vs/base/common/winjs.base";
import Event from "vs/base/common/event";
import { EventEmitter } from "events";
import { basename } from "path";
import { IDisposable } from "vs/base/common/lifecycle";
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { Schemas } from 'vs/base/common/network';
// FileService constructor injected types
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IMessageService } from 'vs/platform/message/common/message';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
export interface IRemoteFileSystemProvider {
onDidChange: Event<URI>;
resolveFile?(resource: URI, options?: IResolveRemoteFileOptions): TPromise<IFileStat>;
resolve(resource: URI): TPromise<string>;
update(resource: URI, content: string): TPromise<any>;
}
export interface IResolveRemoteFileOptions extends IResolveFileOptions {
/**
* Return all descendants in a flat array in the FileStat's children property. This is
* an optimization for file system providers where roundtrips are expensive.
*/
resolveAllDescendants?: boolean;
}
export class RemoteFileService extends FileService {
private readonly _provider = new Map<string, IRemoteFileSystemProvider>();
constructor(
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
@IEnvironmentService environmentService: IEnvironmentService,
@IEditorGroupService editorGroupService: IEditorGroupService,
@ILifecycleService lifecycleService: ILifecycleService,
@IMessageService messageService: IMessageService,
@IStorageService storageService: IStorageService,
@IExtensionService private extensionService: IExtensionService,
) {
super(
configurationService,
contextService,
editorService,
environmentService,
editorGroupService,
lifecycleService,
messageService,
storageService,
);
}
registerProvider(scheme: string, provider: IRemoteFileSystemProvider): IDisposable {
if (this._provider.has(scheme)) {
throw new Error();
}
this._provider.set(scheme, provider);
const reg = provider.onDidChange(e => {
// forward change events
this._onFileChanges.fire(new FileChangesEvent([{ resource: e, type: FileChangeType.UPDATED }]));
});
return {
dispose: () => {
this._provider.delete(scheme);
reg.dispose();
}
};
}
private getProvider(scheme: string): TPromise<IRemoteFileSystemProvider | null> {
if (!scheme || scheme === Schemas.file) {
return TPromise.as(null);
}
if (this._provider.has(scheme)) {
return TPromise.as(this._provider.get(scheme));
}
// Try to use remote file system from extension.
return this.extensionService.activateByEvent(`resource:${scheme}`).then(() => {
return this._provider.get(scheme) || null;
});
}
// --- resolve file
resolveFile(resource: URI, options?: IResolveRemoteFileOptions): TPromise<IFileStat> {
return this.getProvider(resource.scheme).then(provider => {
if (provider) {
return this._doResolveFile(provider, resource);
}
return super.resolveFile(resource, options);
});
}
resolveFiles(toResolve: { resource: URI, options?: IResolveRemoteFileOptions }[]): TPromise<IResolveFileResult[]> {
return TPromise.join(toResolve.map(({ resource }) => this.getProvider(resource.scheme))).then(providers => {
if (providers.some(provider => !!provider)) {
return TPromise.join(toResolve.map(resourceAndOptions => this.resolveFile(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
}
return super.resolveFiles(toResolve);
});
}
existsFile(resource: URI): TPromise<boolean> {
return this.getProvider(resource.scheme).then(provider => {
if (provider) {
return this._doResolveFile(provider, resource).then(() => true, () => false);
}
return super.existsFile(resource);
});
}
private _doResolveFile(provider: IRemoteFileSystemProvider, resource: URI, options?: IResolveRemoteFileOptions): TPromise<IFileStat> {
if (!provider.resolveFile) {
return TPromise.wrapError(new Error('not implemented: stat'));
}
return provider.resolveFile(resource, options);
}
// --- resolve content
resolveContent(resource: URI, options?: IResolveContentOptions): TPromise<IContent> {
return this.getProvider(resource.scheme).then(provider => {
if (provider) {
return this._doResolveContent(provider, resource);
}
return super.resolveContent(resource, options);
});
}
resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise<IStreamContent> {
return this.getProvider(resource.scheme).then(provider => {
if (provider) {
return this._doResolveContent(provider, resource).then(RemoteFileService._asStreamContent);;
}
return super.resolveStreamContent(resource, options);
});
}
resolveContents(resources: URI[]): TPromise<IContent[]> {
return TPromise.join(resources.map(resource => this.getProvider(resource.scheme))).then(providers => {
if (providers.some(provider => !!provider)) {
return TPromise.join(resources.map(resource => this.resolveContent(resource)));
}
return super.resolveContents(resources);
});
}
private _doResolveContent(provider: IRemoteFileSystemProvider, resource: URI): TPromise<IContent> {
return provider.resolve(resource).then(
value => ({ ...RemoteFileService._createFakeStat(resource), value }) as any,
);
}
// --- saving
updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise<IFileStat> {
return this.getProvider(resource.scheme).then(provider => {
if (provider) {
return this._doUpdateContent(provider, resource, value).then(RemoteFileService._createFakeStat);
}
return super.updateContent(resource, value, options);
});
}
private async _doUpdateContent(provider: IRemoteFileSystemProvider, resource: URI, content: string): TPromise<URI> {
await provider.update(resource, content);
return resource;
}
// --- operations
moveFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
if (this._provider.has(source.scheme) || this._provider.has(target.scheme)) {
throw new Error(`not implemented: move ${source.toString()} -> ${target.toString()}`);
}
return super.moveFile(source, target, overwrite);
}
copyFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
if (this._provider.has(source.scheme) || this._provider.has(target.scheme)) {
throw new Error(`not implemented: copy ${source.toString()} -> ${target.toString()}`);
}
return super.copyFile(source, target, overwrite);
}
createFile(resource: URI, content?: string): TPromise<IFileStat> {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: create ${resource.toString()}`);
}
return super.createFile(resource, content);
}
createFolder(resource: URI): TPromise<IFileStat> {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: create folder ${resource.toString()}`);
}
return super.createFolder(resource);
}
rename(resource: URI, newName: string): TPromise<IFileStat> {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: rename ${resource.toString()} -> $[newName}`);
}
return super.rename(resource, newName);
}
touchFile(resource: URI): TPromise<IFileStat> {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: touch file ${resource.toString()}`);
}
return super.touchFile(resource);
}
del(resource: URI, useTrash?: boolean): TPromise<void> {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: del ${resource.toString()}`);
}
return super.del(resource, useTrash);
}
importFile(source: URI, targetFolder: URI): TPromise<IImportResult> {
if (this._provider.has(source.scheme)) {
throw new Error(`not implemented: import file ${source.toString()} into ${targetFolder.toString()}`);
}
return super.importFile(source, targetFolder);
}
watchFileChanges(resource: URI): void {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: watch file changes ${resource.toString()}`);
}
return super.watchFileChanges(resource);
}
unwatchFileChanges(resource: URI): void;
unwatchFileChanges(fsPath: string): void;
unwatchFileChanges(arg: any): void {
const resource = typeof arg === 'string' ? URI.file(arg) : arg;
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: unwatch file changes ${resource.toString()}`);
}
return super.unwatchFileChanges(arg);
}
getEncoding(resource: URI, preferredEncoding?: string): string {
if (this._provider.has(resource.scheme)) {
throw new Error(`not implemented: get encoding for ${resource.toString()}`);
}
return super.getEncoding(resource, preferredEncoding);
}
// --- util
private static _createFakeStat(resource: URI): IFileStat {
return <IFileStat>{
resource,
name: basename(resource.path),
encoding: 'utf8',
mtime: Date.now(),
etag: Date.now().toString(16),
isDirectory: false,
hasChildren: false
};
}
private static _asStreamContent(content: IContent): IStreamContent {
const emitter = new EventEmitter();
const { value } = content;
const result = <IStreamContent><any>content;
result.value = emitter;
setTimeout(() => {
emitter.emit('data', value);
emitter.emit('end');
}, 0);
return result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment