diff --git a/package.json b/package.json index 3df93d97389..7b1684bb88b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "https-proxy-agent": "0.3.6", "iconv-lite": "0.4.15", "jschardet": "^1.5.1", + "jsftp": "^2.0.0", "keytar": "^4.0.3", "minimist": "1.2.0", "native-keymap": "1.2.5", @@ -132,4 +133,4 @@ "windows-mutex": "^0.2.0", "fsevents": "0.3.8" } -} \ No newline at end of file +} diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index ad58576214d..befe9744331 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -135,12 +135,14 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { const emitter = new Emitter(); const provider = { onDidChange: emitter.event, - resolve: (resource: URI) => { + read: (resource: URI) => { return this._proxy.$resolveFile(handle, resource); }, - update: (resource: URI, value: string) => { + write: (resource: URI, value: string) => { return this._proxy.$storeFile(handle, resource, value); - } + }, + stat: () => null, + readdir: () => null }; const searchProvider = { search: (query: ISearchQuery) => { diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index 3c609e62030..b5613afc437 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -1370,7 +1370,8 @@ export abstract class BaseSaveOneFileAction extends BaseSaveFileAction { if (this.resource) { source = this.resource; } else { - source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: ['file', 'untitled'] }); + // source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: ['file', 'untitled'] }); + source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true }); } if (source) { diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index e99e1d220a8..96db2a72d02 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -778,7 +778,7 @@ export class ExplorerView extends CollapsibleView { // Subsequent refresh: Merge stat into our local model and refresh tree modelStats.forEach((modelStat, index) => FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index])); - const input = this.contextService.hasFolderWorkspace() ? this.model.roots[0] : this.model; + const input = /* this.contextService.hasFolderWorkspace() ? this.model.roots[0] : */ this.model; if (input === this.explorerViewer.getInput()) { return this.explorerViewer.refresh(); } diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index e3d97b1d97b..4f87e2acc53 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -25,7 +25,10 @@ export class Model { private _roots: FileStat[]; constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) { - const setRoots = () => this._roots = this.contextService.getWorkspace().roots.map(uri => new FileStat(uri, undefined)); + const setRoots = () => { + this._roots = this.contextService.getWorkspace().roots.map(uri => new FileStat(uri, undefined)); + this._roots.push(new FileStat(URI.parse('ftp://waws-prod-db3-029.ftp.azurewebsites.windows.net/'), undefined)); + }; this.contextService.onDidChangeWorkspaceRoots(() => setRoots()); setRoots(); } @@ -262,7 +265,8 @@ export class FileStat implements IFileStat { } private updateResource(recursive: boolean): void { - this.resource = URI.file(paths.join(this.parent.resource.fsPath, this.name)); + this.resource = this.parent.resource.with({ path: paths.join(this.parent.resource.path, this.name) }); + // this.resource = URI.file(paths.join(this.parent.resource.fsPath, this.name)); if (recursive) { if (this.isDirectory && this.hasChildren && this.children) { @@ -423,4 +427,4 @@ export class OpenEditor { public getResource(): URI { return toResource(this.editor, { supportSideBySide: true, filter: ['file', 'untitled'] }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index f2ac5bbf146..4d9e1fdf586 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -273,7 +273,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { } let input: ICachedEditorInput; - if (resource.scheme === network.Schemas.file) { + if (resource.scheme === network.Schemas.file || resource.scheme === 'ftp') { input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService); } else { input = instantiationService.createInstance(ResourceEditorInput, label, description, resource); @@ -359,4 +359,4 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { return super.doCloseEditor(position, input); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/electron-browser/ftpFileSystemProvider.ts b/src/vs/workbench/services/files/electron-browser/ftpFileSystemProvider.ts new file mode 100644 index 00000000000..26ac9949f53 --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/ftpFileSystemProvider.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Event from 'vs/base/common/event'; +import * as JSFtp from 'jsftp'; +import { ninvoke } from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Readable } from 'stream'; +import { join } from 'path'; +import { IStat } from 'vs/workbench/services/files/electron-browser/remoteFileService'; + +export class FtpFileSystemProvider { + + private _connection: JSFtp; + + onDidChange = Event.None; + + constructor() { + this._connection = new JSFtp({ + host: 'waws-prod-db3-029.ftp.azurewebsites.windows.net', + user: 'performanto-slack-updater\\riejo-test', + pass: 'Z0llikon' + }); + this._connection.keepAlive(1000 * 5); + } + + dispose(): void { + // + } + + stat(resource: URI): TPromise { + + return ninvoke(this._connection, this._connection.ls, resource.path).then(entries => { + + if (entries.length === 1) { + // stat one file + const [entry] = entries; + return { + resource, + mtime: entry.time, + size: entry.size, + isDirectory: false + }; + } + + // stat directory + return { + resource, + isDirectory: true, + mtime: 0, + size: 0 + }; + }); + } + + readdir(resource: URI): TPromise { + return ninvoke(this._connection, this._connection.ls, resource.path).then(ret => { + const promises: TPromise[] = []; + for (let entry of ret) { + promises.push(this.stat(resource.with({ path: join(resource.path, entry.name) }))); + } + return TPromise.join(promises); + }); + } + + write(resource: URI, content: string): TPromise { + return ninvoke(this._connection, this._connection.put, Buffer.from(content, 'utf8'), resource.path); + } + + read(resource: URI): TPromise { + return ninvoke(this._connection, this._connection.get, resource.path).then(stream => { + return new TPromise((resolve, reject) => { + let str = ''; + stream.on('data', function (d) { + str += d.toString(); + }); + stream.on('close', function (hadErr) { + if (hadErr) { + reject(hadErr); + } else { + resolve(str); + } + }); + stream.resume(); + }); + }); + } +} diff --git a/src/vs/workbench/services/files/electron-browser/jsftp.d.ts b/src/vs/workbench/services/files/electron-browser/jsftp.d.ts new file mode 100644 index 00000000000..767a28e7abd --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/jsftp.d.ts @@ -0,0 +1,42 @@ + + +import { Readable } from 'stream'; + +declare namespace JSFtp { + + + interface JSFtpOptions { + host: string; + port?: number | 21; + user?: string | 'anonymous'; + pass?: string | '@anonymous'; + } + + interface Callback { + (err: any, result: T): void; + } + + + interface Entry { + name: string; + size: number; + time: number; + type: 0 | 1; + } +} + +interface JSFtp { + keepAlive(wait?: number): void; + ls(path: string, callback: JSFtp.Callback): void; + put(buffer: Buffer, path: string, callback: JSFtp.Callback): void; + get(path: string, callback: JSFtp.Callback): void; + raw(command: string, args: any[], callback: JSFtp.Callback): void +} + +interface JSFtpConstructor { + new(options: JSFtp.JSFtpOptions): JSFtp; +} + +declare const JSFtp: JSFtpConstructor; + +export = JSFtp; diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 990dfba275b..2a658c72965 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -6,23 +6,128 @@ import URI from 'vs/base/common/uri'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType, IResolveFileOptions, IResolveFileResult } 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 * as Ftp from './ftpFileSystemProvider'; +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 { 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 { IMessageService } from 'vs/platform/message/common/message'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; + + +export interface IStat { + resource: URI; + mtime: number; + size: number; + isDirectory: boolean; +} + +function toIFileStat(provider: IRemoteFileSystemProvider, stat: IStat, recurse: boolean): TPromise { + const ret: IFileStat = { + isDirectory: false, + hasChildren: false, + resource: stat.resource, + name: basename(stat.resource.path), + mtime: stat.mtime, + size: stat.size, + etag: stat.mtime.toString(3) + stat.size.toString(7), + }; + + if (!stat.isDirectory) { + // done + return TPromise.as(ret); + + } else { + // dir -> resolve + return provider.readdir(stat.resource).then(items => { + ret.isDirectory = true; + ret.hasChildren = items.length > 0; + + if (recurse) { + // resolve children if requested + return TPromise.join(items.map(item => toIFileStat(provider, item, false))).then(children => { + ret.children = children; + return ret; + }); + } else { + return ret; + } + + }); + } +} export interface IRemoteFileSystemProvider { - onDidChange: Event; - resolve(resource: URI): TPromise; - update(resource: URI, content: string): TPromise; + onDidChange?: Event; + stat(resource: URI): TPromise; + readdir(resource: URI): TPromise; + write(resource: URI, content: string): TPromise; + read(resource: URI): TPromise; } export class RemoteFileService extends FileService { + // public existsFile(resource: URI): TPromise { + // throw new Error("Method not implemented."); + // } + // public moveFile(source: URI, target: URI, overwrite?: boolean): TPromise { + // throw new Error("Method not implemented."); + // } + // public copyFile(source: URI, target: URI, overwrite?: boolean): TPromise { + // throw new Error("Method not implemented."); + // } + // public createFile(resource: URI, content?: string): TPromise { + // throw new Error("Method not implemented."); + // } + // public createFolder(resource: URI): TPromise { + // throw new Error("Method not implemented."); + // } + // public touchFile(resource: URI): TPromise { + // throw new Error("Method not implemented."); + // } + // public rename(resource: URI, newName: string): TPromise { + // throw new Error("Method not implemented."); + // } + // public del(resource: URI, useTrash?: boolean): TPromise { + // throw new Error("Method not implemented."); + // } + + private readonly _provider = new Map(); + 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 + ) { + super( + configurationService, + contextService, + editorService, + environmentService, + editorGroupService, + lifecycleService, + messageService, + storageService, + ); + this.registerProvider('ftp', new Ftp.FtpFileSystemProvider()); + } + registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { if (this._provider.has(authority)) { throw new Error(); @@ -41,59 +146,96 @@ export class RemoteFileService extends FileService { }; } + resolveFile(resource: URI, options?: IResolveFileOptions): TPromise { + const provider = this._provider.get(resource.scheme); + if (provider) { + return this._doResolveFiles(provider, [{ resource, options }]).then(data => { + if (isFalsyOrEmpty(data)) { + throw new Error('NotFound'); + } + return data[0].stat; + }); + } + return super.resolveFile(resource, options); + } + + resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + const groups = groupBy(toResolve, (a, b) => compare(a.resource.scheme, b.resource.scheme)); + const promises: TPromise[] = []; + + for (const group of groups) { + const provider = this._provider.get(group[0].resource.scheme); + if (!provider) { + promises.push(super.resolveFiles(group)); + } else { + promises.push(this._doResolveFiles(provider, group)); + } + } + + return TPromise.join(promises).then(data => { + return [].concat(...data); + }); + } + + private _doResolveFiles(provider: IRemoteFileSystemProvider, toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + let result: IResolveFileResult[] = []; + let promises: TPromise[] = []; + for (const item of toResolve) { + promises.push(provider.stat(item.resource) + .then(stat => toIFileStat(provider, stat, true)) + .then(stat => result.push({ stat, success: true }))); + } + return TPromise.join(promises).then(() => result); + } + // --- resolve resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doResolveContent(resource); + const provider = this._provider.get(resource.scheme); + if (provider) { + return this._doResolveContent(provider, resource); } return super.resolveContent(resource, options); } resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doResolveContent(resource).then(RemoteFileService._asStreamContent); + + const provider = this._provider.get(resource.scheme); + if (provider) { + return this._doResolveContent(provider, resource).then(RemoteFileService._asStreamContent); } return super.resolveStreamContent(resource, options); } - private async _doResolveContent(resource: URI): TPromise { + private _doResolveContent(provider: IRemoteFileSystemProvider, resource: URI): TPromise { - const stat = RemoteFileService._createFakeStat(resource); - const value = await this._provider.get(resource.authority).resolve(resource); - return { ...stat, value }; + return provider.stat(resource).then(stat => { + return provider.read(resource).then(value => { + return { + ...stat, + value, + }; + }); + }); } // --- saving updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doUpdateContent(resource, value).then(RemoteFileService._createFakeStat); + const provider = this._provider.get(resource.scheme); + if (provider) { + return this._doUpdateContent(provider, resource, value); } - return super.updateContent(resource, value, options); } - private async _doUpdateContent(resource: URI, content: string): TPromise { - await this._provider.get(resource.authority).update(resource, content); - return resource; - } - - // --- util - - private static _createFakeStat(resource: URI): IFileStat { - - return { - resource, - name: basename(resource.path), - encoding: 'utf8', - mtime: Date.now(), - etag: Date.now().toString(16), - isDirectory: false, - hasChildren: false - }; + private async _doUpdateContent(provider: IRemoteFileSystemProvider, resource: URI, content: string): TPromise { + await provider.write(resource, content); + const stat = await provider.stat(resource); + const fileStat = await toIFileStat(provider, stat, false); + return fileStat; } private static _asStreamContent(content: IContent): IStreamContent { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index bdfaaa0b9ba..68cd75dc7b5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -12,7 +12,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { guessMimeTypes } from 'vs/base/common/mime'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import URI from 'vs/base/common/uri'; -import * as assert from 'vs/base/common/assert'; +// import * as assert from 'vs/base/common/assert'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import paths = require('vs/base/common/paths'); import diagnostics = require('vs/base/common/diagnostics'); @@ -90,7 +90,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil ) { super(modelService, modeService); - assert.ok(resource.scheme === 'file', 'TextFileEditorModel can only handle file:// resources.'); + // assert.ok(resource.scheme === 'file', 'TextFileEditorModel can only handle file:// resources.'); this.resource = resource; this.toDispose = []; diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 70e7a582514..746c09ba4b5 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -407,10 +407,13 @@ export abstract class TextFileService implements ITextFileService { const filesToSave: URI[] = []; const untitledToSave: URI[] = []; toSave.forEach(s => { - if (s.scheme === Schemas.file) { - filesToSave.push(s); - } else if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) { + // if (s.scheme === Schemas.file) { + // filesToSave.push(s); + // } else + if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) { untitledToSave.push(s); + } else { + filesToSave.push(s); } }); @@ -712,4 +715,4 @@ export abstract class TextFileService implements ITextFileService { // Clear all caches this._models.clear(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index c7b7668b869..9ed257d66b1 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -35,7 +35,10 @@ class ResourceModelCollection extends ReferenceCollection this.instantiationService.createInstance(ResourceEditorModel, resource)); }