hacking my way around

This commit is contained in:
Johannes Rieken 2017-09-11 21:20:52 +02:00
parent c62644ed13
commit 689dcf546f
12 changed files with 343 additions and 51 deletions

View file

@ -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"
}
}
}

View file

@ -135,12 +135,14 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
const emitter = new Emitter<URI>();
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) => {

View file

@ -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) {

View file

@ -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();
}

View file

@ -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'] });
}
}
}

View file

@ -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);
});
}
}
}

View file

@ -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<IStat> {
return ninvoke<JSFtp.Entry[]>(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 <IStat>{
resource,
isDirectory: true,
mtime: 0,
size: 0
};
});
}
readdir(resource: URI): TPromise<IStat[]> {
return ninvoke<JSFtp.Entry[]>(this._connection, this._connection.ls, resource.path).then(ret => {
const promises: TPromise<IStat>[] = [];
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<void> {
return ninvoke(this._connection, this._connection.put, Buffer.from(content, 'utf8'), resource.path);
}
read(resource: URI): TPromise<string> {
return ninvoke<Readable>(this._connection, this._connection.get, resource.path).then(stream => {
return new TPromise<string>((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();
});
});
}
}

View file

@ -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<T> {
(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<JSFtp.Entry[]>): void;
put(buffer: Buffer, path: string, callback: JSFtp.Callback<void>): void;
get(path: string, callback: JSFtp.Callback<Readable>): void;
raw<T = any>(command: string, args: any[], callback: JSFtp.Callback<T>): void
}
interface JSFtpConstructor {
new(options: JSFtp.JSFtpOptions): JSFtp;
}
declare const JSFtp: JSFtpConstructor;
export = JSFtp;

View file

@ -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<IFileStat> {
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<URI>;
resolve(resource: URI): TPromise<string>;
update(resource: URI, content: string): TPromise<any>;
onDidChange?: Event<URI>;
stat(resource: URI): TPromise<IStat>;
readdir(resource: URI): TPromise<IStat[]>;
write(resource: URI, content: string): TPromise<void>;
read(resource: URI): TPromise<string>;
}
export class RemoteFileService extends FileService {
// public existsFile(resource: URI): TPromise<boolean, any> {
// throw new Error("Method not implemented.");
// }
// public moveFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public copyFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public createFile(resource: URI, content?: string): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public createFolder(resource: URI): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public touchFile(resource: URI): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public rename(resource: URI, newName: string): TPromise<IFileStat, any> {
// throw new Error("Method not implemented.");
// }
// public del(resource: URI, useTrash?: boolean): TPromise<void, any> {
// throw new Error("Method not implemented.");
// }
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
) {
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<IFileStat, any> {
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<IResolveFileResult[], any> {
const groups = groupBy(toResolve, (a, b) => compare(a.resource.scheme, b.resource.scheme));
const promises: TPromise<IResolveFileResult[], any>[] = [];
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<IResolveFileResult[], any> {
let result: IResolveFileResult[] = [];
let promises: TPromise<any>[] = [];
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<IContent> {
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<IStreamContent> {
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<IContent> {
private _doResolveContent(provider: IRemoteFileSystemProvider, resource: URI): TPromise<IContent> {
const stat = RemoteFileService._createFakeStat(resource);
const value = await this._provider.get(resource.authority).resolve(resource);
return <any>{ ...stat, value };
return provider.stat(resource).then(stat => {
return provider.read(resource).then(value => {
return <any>{
...stat,
value,
};
});
});
}
// --- saving
updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise<IFileStat> {
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<URI> {
await this._provider.get(resource.authority).update(resource, content);
return resource;
}
// --- 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 async _doUpdateContent(provider: IRemoteFileSystemProvider, resource: URI, content: string): TPromise<IFileStat> {
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 {

View file

@ -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 = [];

View file

@ -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();
}
}
}

View file

@ -35,7 +35,10 @@ class ResourceModelCollection extends ReferenceCollection<TPromise<ITextEditorMo
if (resource.scheme === network.Schemas.file) {
return this.textFileService.models.loadOrCreate(resource);
}
const result = this.textFileService.models.get(resource);
if (result) {
return TPromise.as(result);
}
return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource));
}