Added Unified connection support (#3785)

* Added Unified connection support

* Use generic way to do expandNode.
Cleanup the ported code and removed unreference code. Added as needed later.
Resolved PR comments.

* Minor fixes and removed timer for all expanders for now. If any providers can't response, the tree node will spin and wait. We may improve later.

* Change handSessionClose to not thenable.
Added a node to OE to show error message instead of reject. So we could show partial expanded result if get any.
Resolve PR comments

* Minor fixes of PR comments
This commit is contained in:
Yurong He
2019-01-29 14:37:14 -08:00
committed by GitHub
parent b439ea45ec
commit 3ddc5e7846
37 changed files with 3087 additions and 80 deletions

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Transform } from 'stream';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class CancelableStream extends Transform {
constructor(private cancelationToken: vscode.CancellationTokenSource) {
super();
}
public _transform(chunk: any, encoding: string, callback: Function): void {
if (this.cancelationToken && this.cancelationToken.token.isCancellationRequested) {
callback(new Error(localize('streamCanceled', 'Stream operation canceled by the user')));
} else {
this.push(chunk);
callback();
}
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import { TreeNode } from './treeNodes';
import { QuestionTypes, IPrompter, IQuestion } from '../prompts/question';
import * as utils from '../utils';
import * as constants from '../constants';
import { AppContext } from '../appContext';
export interface ICommandContextParsingOptions {
editor: boolean;
uri: boolean;
}
export interface ICommandBaseContext {
command: string;
editor?: vscode.TextEditor;
uri?: vscode.Uri;
}
export interface ICommandUnknownContext extends ICommandBaseContext {
type: 'unknown';
}
export interface ICommandUriContext extends ICommandBaseContext {
type: 'uri';
}
export interface ICommandViewContext extends ICommandBaseContext {
type: 'view';
node: TreeNode;
}
export interface ICommandObjectExplorerContext extends ICommandBaseContext {
type: 'objectexplorer';
explorerContext: sqlops.ObjectExplorerContext;
}
export type CommandContext = ICommandObjectExplorerContext | ICommandViewContext | ICommandUriContext | ICommandUnknownContext;
function isTextEditor(editor: any): editor is vscode.TextEditor {
if (editor === undefined) { return false; }
return editor.id !== undefined && ((editor as vscode.TextEditor).edit !== undefined || (editor as vscode.TextEditor).document !== undefined);
}
export abstract class Command extends vscode.Disposable {
protected readonly contextParsingOptions: ICommandContextParsingOptions = { editor: false, uri: false };
private disposable: vscode.Disposable;
constructor(command: string | string[], protected appContext: AppContext) {
super(() => this.dispose());
if (typeof command === 'string') {
this.disposable = this.apiWrapper.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this);
return;
}
const subscriptions = command.map(cmd => this.apiWrapper.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this));
this.disposable = vscode.Disposable.from(...subscriptions);
}
dispose(): void {
this.disposable && this.disposable.dispose();
}
protected get apiWrapper(): ApiWrapper {
return this.appContext.apiWrapper;
}
protected async preExecute(...args: any[]): Promise<any> {
return this.execute(...args);
}
abstract execute(...args: any[]): any;
protected _execute(command: string, ...args: any[]): any {
// TODO consider using Telemetry.trackEvent(command);
const [context, rest] = Command.parseContext(command, this.contextParsingOptions, ...args);
return this.preExecute(context, ...rest);
}
private static parseContext(command: string, options: ICommandContextParsingOptions, ...args: any[]): [CommandContext, any[]] {
let editor: vscode.TextEditor | undefined = undefined;
let firstArg = args[0];
if (options.editor && (firstArg === undefined || isTextEditor(firstArg))) {
editor = firstArg;
args = args.slice(1);
firstArg = args[0];
}
if (options.uri && (firstArg === undefined || firstArg instanceof vscode.Uri)) {
const [uri, ...rest] = args as [vscode.Uri, any];
return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest];
}
if (firstArg instanceof TreeNode) {
const [node, ...rest] = args as [TreeNode, any];
return [{ command: command, type: constants.ViewType, node: node }, rest];
}
if (firstArg && utils.isObjectExplorerContext(firstArg)) {
const [explorerContext, ...rest] = args as [sqlops.ObjectExplorerContext, any];
return [{ command: command, type: constants.ObjectExplorerService, explorerContext: explorerContext }, rest];
}
return [{ command: command, type: 'unknown', editor: editor }, args];
}
}
export abstract class ProgressCommand extends Command {
static progressId = 0;
constructor(private command: string, protected prompter: IPrompter, appContext: AppContext) {
super(command, appContext);
}
protected async executeWithProgress(
execution: (cancelToken: vscode.CancellationTokenSource) => Promise<void>,
label: string,
isCancelable: boolean = false,
onCanceled?: () => void
): Promise<void> {
let disposables: vscode.Disposable[] = [];
const tokenSource = new vscode.CancellationTokenSource();
const statusBarItem = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left);
disposables.push(vscode.Disposable.from(statusBarItem));
statusBarItem.text = localize('progress', '$(sync~spin) {0}...', label);
if (isCancelable) {
const cancelCommandId = `cancelProgress${ProgressCommand.progressId++}`;
disposables.push(this.apiWrapper.registerCommand(cancelCommandId, async () => {
if (await this.confirmCancel()) {
tokenSource.cancel();
}
}));
statusBarItem.tooltip = localize('cancelTooltip', 'Cancel');
statusBarItem.command = cancelCommandId;
}
statusBarItem.show();
try {
await execution(tokenSource);
} catch (error) {
if (isCancelable && onCanceled && tokenSource.token.isCancellationRequested) {
// The error can be assumed to be due to cancelation occurring. Do the callback
onCanceled();
} else {
throw error;
}
} finally {
disposables.forEach(d => d.dispose());
}
}
private async confirmCancel(): Promise<boolean> {
return await this.prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: localize('cancel', 'Cancel operation?'),
default: true
});
}
}

View File

@@ -0,0 +1,222 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as constants from '../constants';
import * as LocalizedConstants from '../localizedConstants';
import * as utils from '../utils';
import { IFileSource, HdfsFileSource, IHdfsOptions, IRequestParams, FileSourceFactory } from './fileSources';
function appendIfExists(uri: string, propName: string, propValue: string): string {
if (propValue) {
uri = `${uri};${propName}=${propValue}`;
}
return uri;
}
interface IValidationResult {
isValid: boolean;
errors: string;
}
export class Connection {
private _host: string;
private _knoxPort: string;
constructor(private connectionInfo: sqlops.ConnectionInfo, private connectionUri?: string, private _connectionId?: string) {
if (!this.connectionInfo) {
throw new Error(localize('connectionInfoMissing', 'connectionInfo is required'));
}
if (!this._connectionId) {
this._connectionId = UUID.generateUuid();
}
}
public get uri(): string {
return this.connectionUri;
}
public saveUriWithPrefix(prefix: string): string {
let uri = `${prefix}${this.host}`;
uri = appendIfExists(uri, constants.knoxPortPropName, this.knoxport);
uri = appendIfExists(uri, constants.userPropName, this.user);
uri = appendIfExists(uri, constants.groupIdPropName, this.connectionInfo.options[constants.groupIdPropName]);
this.connectionUri = uri;
return this.connectionUri;
}
public async tryConnect(factory?: FileSourceFactory): Promise<sqlops.ConnectionInfoSummary> {
let fileSource = this.createHdfsFileSource(factory, {
timeout: this.connecttimeout
});
let summary: sqlops.ConnectionInfoSummary = undefined;
try {
await fileSource.enumerateFiles(constants.hdfsRootPath);
summary = {
ownerUri: this.connectionUri,
connectionId: this.connectionId,
connectionSummary: {
serverName: this.host,
databaseName: undefined,
userName: this.user
},
errorMessage: undefined,
errorNumber: undefined,
messages: undefined,
serverInfo: this.getEmptyServerInfo()
};
} catch (error) {
summary = {
ownerUri: this.connectionUri,
connectionId: undefined,
connectionSummary: undefined,
errorMessage: this.getConnectError(error),
errorNumber: undefined,
messages: undefined,
serverInfo: undefined
};
}
return summary;
}
private getConnectError(error: string | Error): string {
let errorMsg = utils.getErrorMessage(error);
if (errorMsg.indexOf('ETIMEDOUT') > -1) {
errorMsg = LocalizedConstants.msgTimeout;
} else if (errorMsg.indexOf('ENOTFOUND') > -1) {
errorMsg = LocalizedConstants.msgTimeout;
}
return localize('connectError', 'Connection failed with error: {0}', errorMsg);
}
private getEmptyServerInfo(): sqlops.ServerInfo {
let info: sqlops.ServerInfo = {
serverMajorVersion: 0,
serverMinorVersion: 0,
serverReleaseVersion: 0,
engineEditionId: 0,
serverVersion: '',
serverLevel: '',
serverEdition: '',
isCloud: false,
azureVersion: 0,
osVersion: '',
options: { isBigDataCluster: false, clusterEndpoints: []}
};
return info;
}
public get connectionId(): string {
return this._connectionId;
}
public get host(): string {
if (!this._host) {
this.ensureHostAndPort();
}
return this._host;
}
/**
* Sets host and port values, using any ',' or ':' delimited port in the hostname in
* preference to the built in port.
*/
private ensureHostAndPort(): void {
this._host = this.connectionInfo.options[constants.hostPropName];
this._knoxPort = Connection.getKnoxPortOrDefault(this.connectionInfo);
// determine whether the host has either a ',' or ':' in it
this.setHostAndPort(',');
this.setHostAndPort(':');
}
// set port and host correctly after we've identified that a delimiter exists in the host name
private setHostAndPort(delimeter: string): void {
let originalHost = this._host;
let index = originalHost.indexOf(delimeter);
if (index > -1) {
this._host = originalHost.slice(0, index);
this._knoxPort = originalHost.slice(index + 1);
}
}
public get user(): string {
return this.connectionInfo.options[constants.userPropName];
}
public get password(): string {
return this.connectionInfo.options[constants.passwordPropName];
}
public get knoxport(): string {
if (!this._knoxPort) {
this.ensureHostAndPort();
}
return this._knoxPort;
}
private static getKnoxPortOrDefault(connInfo: sqlops.ConnectionInfo): string {
let port = connInfo.options[constants.knoxPortPropName];
if (!port) {
port = constants.defaultKnoxPort;
}
return port;
}
public get connecttimeout(): number {
let timeoutSeconds: number = this.connectionInfo.options['connecttimeout'];
if (!timeoutSeconds) {
timeoutSeconds = constants.hadoopConnectionTimeoutSeconds;
}
// connect timeout is in milliseconds
return timeoutSeconds * 1000;
}
public get sslverification(): string {
return this.connectionInfo.options['sslverification'];
}
public get groupId(): string {
return this.connectionInfo.options[constants.groupIdName];
}
public isMatch(connectionInfo: sqlops.ConnectionInfo): boolean {
if (!connectionInfo) {
return false;
}
let otherConnection = new Connection(connectionInfo);
return otherConnection.groupId === this.groupId
&& otherConnection.host === this.host
&& otherConnection.knoxport === this.knoxport
&& otherConnection.user === this.user;
}
public createHdfsFileSource(factory?: FileSourceFactory, additionalRequestParams?: IRequestParams): IFileSource {
factory = factory || FileSourceFactory.instance;
let options: IHdfsOptions = {
protocol: 'https',
host: this.host,
port: this.knoxport,
user: this.user,
path: 'gateway/default/webhdfs/v1',
requestParams: {
auth: {
user: this.user,
pass: this.password
}
}
};
if (additionalRequestParams) {
options.requestParams = Object.assign(options.requestParams, additionalRequestParams);
}
return factory.createHdfsFileSource(options);
}
}

View File

@@ -0,0 +1,371 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fspath from 'path';
import * as webhdfs from 'webhdfs';
import * as fs from 'fs';
import * as meter from 'stream-meter';
import * as bytes from 'bytes';
import * as https from 'https';
import * as readline from 'readline';
import * as os from 'os';
import * as constants from '../constants';
import * as utils from '../utils';
export function joinHdfsPath(parent: string, child: string): string {
if (parent === constants.hdfsRootPath) {
return `/${child}`;
}
return `${parent}/${child}`;
}
export interface IFile {
path: string;
isDirectory: boolean;
}
export class File implements IFile {
constructor(public path: string, public isDirectory: boolean) {
}
public static createPath(path: string, fileName: string): string {
return joinHdfsPath(path, fileName);
}
public static createChild(parent: IFile, fileName: string, isDirectory: boolean): IFile {
return new File(File.createPath(parent.path, fileName), isDirectory);
}
public static createFile(parent: IFile, fileName: string): File {
return File.createChild(parent, fileName, false);
}
public static createDirectory(parent: IFile, fileName: string): IFile {
return File.createChild(parent, fileName, true);
}
public static getBasename(file: IFile): string {
return fspath.basename(file.path);
}
}
export interface IFileSource {
enumerateFiles(path: string): Promise<IFile[]>;
mkdir(dirName: string, remoteBasePath: string): Promise<void>;
createReadStream(path: string): fs.ReadStream;
readFile(path: string, maxBytes?: number): Promise<Buffer>;
readFileLines(path: string, maxLines: number): Promise<Buffer>;
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
delete(path: string, recursive?: boolean): Promise<void>;
exists(path: string): Promise<boolean>;
}
export interface IHttpAuthentication {
user: string;
pass: string;
}
export interface IHdfsOptions {
host?: string;
port?: string;
protocol?: string;
user?: string;
path?: string;
requestParams?: IRequestParams;
}
export interface IRequestParams {
auth?: IHttpAuthentication;
/**
* Timeout in milliseconds to wait for response
*/
timeout?: number;
agent?: https.Agent;
}
export interface IHdfsFileStatus {
type: 'FILE' | 'DIRECTORY';
pathSuffix: string;
}
export interface IHdfsClient {
readdir(path: string, callback: (err: Error, files: any[]) => void): void;
/**
* Create readable stream for given path
*
* @method createReadStream
* @fires Request#data
* @fires WebHDFS#finish
*
* @param {String} path
* @param {Object} [opts]
*
* @returns {Object}
*/
createReadStream (path: string, opts?: object): fs.ReadStream;
/**
* Create writable stream for given path
*
* @example
*
* var WebHDFS = require('webhdfs');
* var hdfs = WebHDFS.createClient();
*
* var localFileStream = fs.createReadStream('/path/to/local/file');
* var remoteFileStream = hdfs.createWriteStream('/path/to/remote/file');
*
* localFileStream.pipe(remoteFileStream);
*
* remoteFileStream.on('error', function onError (err) {
* // Do something with the error
* });
*
* remoteFileStream.on('finish', function onFinish () {
* // Upload is done
* });
*
* @method createWriteStream
* @fires WebHDFS#finish
*
* @param {String} path
* @param {Boolean} [append] If set to true then append data to the file
* @param {Object} [opts]
*
* @returns {Object}
*/
createWriteStream(path: string, append?: boolean, opts?: object): fs.WriteStream;
/**
* Make new directory
*
* @method mkdir
*
* @param {String} path
* @param {String} [mode=0777]
* @param {Function} callback
*
* @returns {Object}
*/
mkdir (path: string, callback: Function): void;
mkdir (path: string, mode: string, callback: Function): void;
/**
* Delete directory or file path
*
* @method unlink
*
* @param {String} path
* @param {Boolean} [recursive=false]
* @param {Function} callback
*
* @returns {Object}
*/
rmdir (path: string, recursive: boolean, callback: Function): void;
/**
* Check file existence
* Wraps stat method
*
* @method stat
* @see WebHDFS.stat
*
* @param {String} path
* @param {Function} callback
*
* @returns {Object}
*/
exists (path: string, callback: Function): boolean;
}
export class FileSourceFactory {
private static _instance: FileSourceFactory;
public static get instance(): FileSourceFactory {
if (!FileSourceFactory._instance) {
FileSourceFactory._instance = new FileSourceFactory();
}
return FileSourceFactory._instance;
}
public createHdfsFileSource(options: IHdfsOptions): IFileSource {
options = options && options.host ? FileSourceFactory.removePortFromHost(options) : options;
let requestParams: IRequestParams = options.requestParams ? options.requestParams : {};
if (requestParams.auth) {
// TODO Remove handling of unsigned cert once we have real certs in our Knox service
let agentOptions = {
host: options.host,
port: options.port,
path: constants.hdfsRootPath,
rejectUnauthorized: false
};
let agent = new https.Agent(agentOptions);
requestParams['agent'] = agent;
}
return new HdfsFileSource(webhdfs.createClient(options, requestParams));
}
// remove port from host when port is specified after a comma or colon
private static removePortFromHost(options: IHdfsOptions): IHdfsOptions {
// determine whether the host has either a ',' or ':' in it
options = this.setHostAndPort(options, ',');
options = this.setHostAndPort(options, ':');
return options;
}
// set port and host correctly after we've identified that a delimiter exists in the host name
private static setHostAndPort(options: IHdfsOptions, delimeter: string): IHdfsOptions {
let optionsHost: string = options.host;
if (options.host.indexOf(delimeter) > -1) {
options.host = options.host.slice(0, options.host.indexOf(delimeter));
options.port = optionsHost.replace(options.host + delimeter, '');
}
return options;
}
}
export class HdfsFileSource implements IFileSource {
constructor(private client: IHdfsClient) {
}
public enumerateFiles(path: string): Promise<IFile[]> {
return new Promise((resolve, reject) => {
this.client.readdir(path, (error, files) => {
if (error) {
reject(error.message);
} else {
let hdfsFiles: IFile[] = files.map(file => {
let hdfsFile = <IHdfsFileStatus> file;
return new File(File.createPath(path, hdfsFile.pathSuffix), hdfsFile.type === 'DIRECTORY');
});
resolve(hdfsFiles);
}
});
});
}
public mkdir(dirName: string, remoteBasePath: string): Promise<void> {
return new Promise((resolve, reject) => {
let remotePath = joinHdfsPath(remoteBasePath, dirName);
this.client.mkdir(remotePath, (err) => {
if (err) {
reject(err);
} else {
resolve(undefined);
}
});
});
}
public createReadStream(path: string): fs.ReadStream {
return this.client.createReadStream(path);
}
public readFile(path: string, maxBytes?: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
let remoteFileStream = this.client.createReadStream(path);
if (maxBytes) {
remoteFileStream = remoteFileStream.pipe(meter(maxBytes));
}
let data = [];
let error = undefined;
remoteFileStream.on('error', (err) => {
error = err.toString();
if (error.includes('Stream exceeded specified max')) {
error = `File exceeds max size of ${bytes(maxBytes)}`;
}
reject(error);
});
remoteFileStream.on('data', (chunk) => {
data.push(chunk);
});
remoteFileStream.once('finish', () => {
if (!error) {
resolve(Buffer.concat(data));
}
});
});
}
public readFileLines(path: string, maxLines: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
let lineReader = readline.createInterface({
input: this.client.createReadStream(path)
});
let lineCount = 0;
let lineData: string[] = [];
let errorMsg = undefined;
lineReader.on('line', (line: string) => {
lineCount++;
lineData.push(line);
if (lineCount >= maxLines) {
resolve(Buffer.from(lineData.join(os.EOL)));
lineReader.close();
}
})
.on('error', (err) => {
errorMsg = utils.getErrorMessage(err);
reject(errorMsg);
})
.on('close', () => {
if (!errorMsg) {
resolve(Buffer.from(lineData.join(os.EOL)));
}
});
});
}
public writeFile(localFile: IFile, remoteDirPath: string): Promise<string> {
return new Promise((resolve, reject) => {
let fileName = fspath.basename(localFile.path);
let remotePath = joinHdfsPath(remoteDirPath, fileName);
let writeStream = this.client.createWriteStream(remotePath);
let readStream = fs.createReadStream(localFile.path);
readStream.pipe(writeStream);
let error: string | Error = undefined;
// API always calls finish, so catch error then handle exit in the finish event
writeStream.on('error', (err => {
error = err;
reject(error);
}));
writeStream.on('finish', (location) => {
if (!error) {
resolve(location);
}
});
});
}
public delete(path: string, recursive: boolean = false): Promise<void> {
return new Promise((resolve, reject) => {
this.client.rmdir(path, recursive, (error) => {
if (error) {
reject(error);
} else {
resolve(undefined);
}
});
});
}
public exists(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
this.client.exists(path, (result) => {
resolve(result);
});
});
}
}

View File

@@ -0,0 +1,437 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as fs from 'fs';
import * as fspath from 'path';
import * as clipboardy from 'clipboardy';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import { Command, ICommandViewContext, ProgressCommand, ICommandObjectExplorerContext } from './command';
import { IHdfsOptions, HdfsFileSource, File, IFile, joinHdfsPath, FileSourceFactory } from './fileSources';
import { HdfsProvider, FolderNode, FileNode, HdfsFileSourceNode } from './hdfsProvider';
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import * as constants from '../constants';
import * as LocalizedConstants from '../localizedConstants';
import * as utils from '../utils';
import { Connection } from './connection';
import { AppContext } from '../appContext';
import { TreeNode } from './treeNodes';
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider';
function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): vscode.Uri {
let root = utils.getUserHome();
let workspaceFolders = apiWrapper.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
root = workspaceFolders[0].uri.fsPath;
}
// Cannot preview with a file path that already exists, so keep looking for a valid path that does not exist
if (isPreview) {
let fileNum = 1;
let fileNameWithoutExtension = fspath.parse(fileName).name;
let fileExtension = fspath.parse(fileName).ext;
while (fs.existsSync(fspath.join(root, fileName))) {
fileName = `${fileNameWithoutExtension}-${fileNum}${fileExtension}`;
fileNum++;
}
}
return vscode.Uri.file(fspath.join(root, fileName));
}
export async function getNode<T extends TreeNode>(context: ICommandViewContext |ICommandObjectExplorerContext, appContext: AppContext): Promise<T> {
let node: T = undefined;
if (context && context.type === constants.ViewType && context.node) {
node = context.node as T;
} else if (context && context.type === constants.ObjectExplorerService) {
let oeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
if (oeProvider) {
node = await oeProvider.findNodeForContext<T>(context.explorerContext);
}
} else {
throw new Error(LocalizedConstants.msgMissingNodeContext);
}
return node;
}
export class UploadFilesCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.uploadFiles', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let folderNode = await getNode<FolderNode>(context, this.appContext);
const allFilesFilter = localize('allFiles', 'All Files');
let filter = {};
filter[allFilesFilter] = '*';
if (folderNode) {
let options: vscode.OpenDialogOptions = {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: localize('lblUploadFiles', 'Upload'),
filters: filter
};
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
if (fileUris) {
let files: IFile[] = fileUris.map(uri => uri.fsPath).map(this.mapPathsToFiles());
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.writeFiles(files, folderNode, cancelToken),
localize('uploading', 'Uploading files to HDFS'), true,
() => this.apiWrapper.showInformationMessage(localize('uploadCanceled', 'Upload operation was canceled')));
if (context.type === constants.ObjectExplorerService) {
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
await objectExplorerNode.refresh();
}
}
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
}
}
private mapPathsToFiles(): (value: string, index: number, array: string[]) => File {
return (path: string) => {
let isDir = fs.lstatSync(path).isDirectory();
return new File(path, isDir);
};
}
private async writeFiles(files: IFile[], folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
for (let file of files) {
if (cancelToken.token.isCancellationRequested) {
// Throw here so that all recursion is ended
throw new Error('Upload canceled');
}
if (file.isDirectory) {
let dirName = fspath.basename(file.path);
let subFolder = await folderNode.mkdir(dirName);
let children: IFile[] = fs.readdirSync(file.path)
.map(childFileName => joinHdfsPath(file.path, childFileName))
.map(this.mapPathsToFiles());
this.writeFiles(children, subFolder, cancelToken);
} else {
await folderNode.writeFile(file);
}
}
}
}
export class MkDirCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.mkdir', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let folderNode = await getNode<FolderNode>(context, this.appContext);
if (folderNode) {
let fileName: string = await this.getDirName();
if (fileName && fileName.length > 0) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.mkDir(fileName, folderNode, cancelToken),
localize('makingDir', 'Creating directory'), true,
() => this.apiWrapper.showInformationMessage(localize('mkdirCanceled', 'Operation was canceled')));
if (context.type === constants.ObjectExplorerService) {
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
await objectExplorerNode.refresh();
}
}
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
}
}
private async getDirName(): Promise<string> {
return await this.prompter.promptSingle(<IQuestion>{
type: QuestionTypes.input,
name: 'enterDirName',
message: localize('enterDirName', 'Enter directory name'),
default: ''
}).then(confirmed => <string>confirmed);
}
private async mkDir(fileName, folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
let subFolder = await folderNode.mkdir(fileName);
}
}
export class DeleteFilesCommand extends Command {
constructor(private prompter: IPrompter, appContext: AppContext) {
super('hdfs.deleteFiles', appContext);
}
protected async preExecute(context: ICommandViewContext |ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext |ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let node = await getNode<TreeNode>(context, this.appContext);
if (node) {
// TODO ideally would let node define if it's deletable
// TODO also, would like to change this to getNodeInfo as OE is the primary use case now
let treeItem = await node.getTreeItem();
let oeNodeToRefresh: sqlops.objectexplorer.ObjectExplorerNode = undefined;
if (context.type === constants.ObjectExplorerService) {
let oeNodeToDelete = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, node.getNodeInfo().nodePath);
oeNodeToRefresh = await oeNodeToDelete.getParent();
}
switch (treeItem.contextValue) {
case constants.HdfsItems.Folder:
await this.deleteFolder(<FolderNode>node);
break;
case constants.HdfsItems.File:
await this.deleteFile(<FileNode>node);
break;
default:
return;
}
if (oeNodeToRefresh) {
await oeNodeToRefresh.refresh();
}
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('deleteError', 'Error deleting files {0}', err));
}
}
private async confirmDelete(deleteMsg: string): Promise<boolean> {
return await this.prompter.promptSingle(<IQuestion>{
type: QuestionTypes.confirm,
message: deleteMsg,
default: false
}).then(confirmed => <boolean>confirmed);
}
private async deleteFolder(node: FolderNode): Promise<void> {
if (node) {
let confirmed = await this.confirmDelete(localize('msgDeleteFolder', 'Are you sure you want to delete this folder and its contents?'));
if (confirmed) {
// TODO prompt for recursive delete if non-empty?
await node.delete(true);
}
}
}
private async deleteFile(node: FileNode): Promise<void> {
if (node) {
let confirmed = await this.confirmDelete(localize('msgDeleteFile', 'Are you sure you want to delete this file?'));
if (confirmed) {
await node.delete();
}
}
}
}
export class SaveFileCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.saveFile', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let fileNode = await getNode<FileNode>(context, this.appContext);
if (fileNode) {
let defaultUri = getSaveableUri(this.apiWrapper, fspath.basename(fileNode.hdfsPath));
let fileUri: vscode.Uri = await this.apiWrapper.showSaveDialog({
defaultUri: defaultUri
});
if (fileUri) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.doSaveAndOpen(fileUri, fileNode, cancelToken),
localize('saving', 'Saving HDFS Files'), true,
() => this.apiWrapper.showInformationMessage(localize('saveCanceled', 'Save operation was canceled')));
}
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('saveError', 'Error saving file: {0}', err));
}
}
private async doSaveAndOpen(fileUri: vscode.Uri, fileNode: FileNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
await fileNode.writeFileContentsToDisk(fileUri.fsPath, cancelToken);
await this.apiWrapper.executeCommand('vscode.open', fileUri);
}
}
export class PreviewFileCommand extends ProgressCommand {
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.previewFile', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let fileNode = await getNode<FileNode>(context, this.appContext);
if (fileNode) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => {
let contents = await fileNode.getFileContentsAsString(PreviewFileCommand.DefaultMaxSize);
let doc = await this.openTextDocument(fspath.basename(fileNode.hdfsPath));
let editor = await this.apiWrapper.showTextDocument(doc, vscode.ViewColumn.Active, false);
await editor.edit(edit => {
edit.insert(new vscode.Position(0, 0), contents);
});
},
localize('previewing', 'Generating preview'),
false);
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('previewError', 'Error previewing file: {0}', err));
}
}
private async openTextDocument(fileName: string): Promise<vscode.TextDocument> {
let docUri: vscode.Uri = getSaveableUri(this.apiWrapper, fileName, true);
if (docUri) {
docUri = docUri.with({ scheme: constants.UNTITLED_SCHEMA });
return await this.apiWrapper.openTextDocument(docUri);
} else {
// Can't reliably create a filename to save as so just use untitled
let language = fspath.extname(fileName);
if (language && language.length > 0) {
// trim the '.'
language = language.substring(1);
}
return await this.apiWrapper.openTextDocument({
language: language
});
}
}
}
export class CopyPathCommand extends Command {
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
constructor(appContext: AppContext) {
super('hdfs.copyPath', appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
if (node) {
let path = node.hdfsPath;
clipboardy.writeSync(path);
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('copyPathError', 'Error copying path: {0}', err));
}
}
}
/**
* The connect task is only expected to work in the file-tree based APIs, not Object Explorer
*/
export class ConnectTask {
constructor(private hdfsProvider: HdfsProvider, private prompter: IPrompter, private apiWrapper: ApiWrapper) {
}
async execute(profile: sqlops.IConnectionProfile, ...args: any[]): Promise<void> {
if (profile) {
return this.createFromProfile(profile);
}
return this.createHdfsConnection();
}
private createFromProfile(profile: sqlops.IConnectionProfile): Promise<void> {
let connection = new Connection(profile);
if (profile.providerName === constants.mssqlClusterProviderName && connection.host) {
// TODO need to get the actual port and auth to be used since this will be non-default
// in future versions
this.hdfsProvider.addHdfsConnection(<IHdfsOptions> {
protocol: 'https',
host: connection.host,
port: connection.knoxport,
user: connection.user,
path: 'gateway/default/webhdfs/v1',
requestParams: {
auth: {
user: connection.user,
pass: connection.password
}
}
});
}
return Promise.resolve(undefined);
}
private addConnection(options: IHdfsOptions): void {
let display: string = `${options.user}@${options.host}:${options.port}`;
this.hdfsProvider.addConnection(display, FileSourceFactory.instance.createHdfsFileSource(options));
}
private async createHdfsConnection(profile?: sqlops.IConnectionProfile): Promise<void> {
let questions: IQuestion[] = [
{
type: QuestionTypes.input,
name: constants.hdfsHost,
message: localize('msgSetWebHdfsHost', 'HDFS URL and port'),
default: 'localhost:50070'
},
{
type: QuestionTypes.input,
name: constants.hdfsUser,
message: localize('msgSetWebHdfsUser', 'User Name'),
default: 'root'
}];
let answers = await this.prompter.prompt(questions);
if (answers) {
let hostAndPort: string = answers[constants.hdfsHost];
let parts = hostAndPort.split(':');
let host: string = parts[0];
let port: string = parts.length > 1 ? parts[1] : undefined;
let user: string = answers[constants.hdfsUser];
let options: IHdfsOptions = {
host: host,
port: port,
user: user
};
this.addConnection(options);
}
}
}

View File

@@ -0,0 +1,366 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as fspath from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import * as Constants from '../constants';
import { IFileSource, IHdfsOptions, HdfsFileSource, IFile, File, FileSourceFactory } from './fileSources';
import { CancelableStream } from './cancelableStream';
import { TreeNode } from './treeNodes';
import * as utils from '../utils';
import { IFileNode } from './types';
export interface ITreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;
}
export class TreeDataContext {
constructor(public extensionContext: vscode.ExtensionContext, public changeHandler: ITreeChangeHandler) {
}
}
export class HdfsProvider implements vscode.TreeDataProvider<TreeNode>, ITreeChangeHandler {
static readonly NoConnectionsMessage = 'No connections added';
static readonly ConnectionsLabel = 'Connections';
private connections: ConnectionNode[];
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode>();
private context: TreeDataContext;
constructor(extensionContext: vscode.ExtensionContext, private vscodeApi: ApiWrapper) {
this.connections = [];
this.context = new TreeDataContext(extensionContext, this);
}
public get onDidChangeTreeData(): vscode.Event<TreeNode> {
return this._onDidChangeTreeData.event;
}
getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element.getTreeItem();
}
getChildren(element?: TreeNode): vscode.ProviderResult<TreeNode[]> {
if (element) {
return element.getChildren(false);
} else {
return this.connections.length > 0 ? this.connections : [MessageNode.create(HdfsProvider.NoConnectionsMessage, element)];
}
}
addConnection(displayName: string, fileSource: IFileSource): void {
if (!this.connections.find(c => c.getDisplayName() === displayName)) {
this.connections.push(new ConnectionNode(this.context, displayName, fileSource));
this._onDidChangeTreeData.fire();
}
}
addHdfsConnection(options: IHdfsOptions): void {
let displayName = `${options.user}@${options.host}:${options.port}`;
let fileSource = FileSourceFactory.instance.createHdfsFileSource(options);
this.addConnection(displayName, fileSource);
}
notifyNodeChanged(node: TreeNode): void {
this._onDidChangeTreeData.fire(node);
}
}
export abstract class HdfsFileSourceNode extends TreeNode {
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
super();
}
public get hdfsPath(): string {
return this._path;
}
public get nodePathValue(): string {
return this.getDisplayName();
}
getDisplayName(): string {
return fspath.basename(this._path);
}
public async delete(recursive: boolean = false): Promise<void> {
await this.fileSource.delete(this.hdfsPath, recursive);
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
(<HdfsFileSourceNode>this.parent).onChildRemoved();
this.context.changeHandler.notifyNodeChanged(this.parent);
}
public abstract onChildRemoved(): void;
}
export class FolderNode extends HdfsFileSourceNode {
private children: TreeNode[];
protected _nodeType: string;
constructor(context: TreeDataContext, path: string, fileSource: IFileSource, nodeType?: string) {
super(context, path, fileSource);
this._nodeType = nodeType ? nodeType : Constants.HdfsItems.Folder;
}
private ensureChildrenExist(): void {
if (!this.children) {
this.children = [];
}
}
public onChildRemoved(): void {
this.children = undefined;
}
async getChildren(refreshChildren: boolean): Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.ensureChildrenExist();
try {
let files: IFile[] = await this.fileSource.enumerateFiles(this._path);
if (files) {
// Note: for now, assuming HDFS-provided sorting is sufficient
this.children = files.map((file) => {
let node: TreeNode = file.isDirectory ? new FolderNode(this.context, file.path, this.fileSource)
: new FileNode(this.context, file.path, this.fileSource);
node.parent = this;
return node;
});
}
} catch (error) {
this.children = [MessageNode.create(localize('errorExpanding', 'Error: {0}', utils.getErrorMessage(error)), this)];
}
}
return this.children;
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.Collapsed);
// For now, folder always looks the same. We're using SQL icons to differentiate remote vs local files
item.iconPath = {
dark: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg'),
light: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg')
};
item.contextValue = this._nodeType;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
// TODO handle error message case by returning it in the OE API
// TODO support better mapping of node type
let nodeInfo: sqlops.NodeInfo = {
label: this.getDisplayName(),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: this._nodeType,
nodeSubType: undefined,
iconType: 'Folder'
};
return nodeInfo;
}
public async writeFile(localFile: IFile): Promise<FileNode> {
return this.runChildAddAction<FileNode>(() => this.writeFileAsync(localFile));
}
private async writeFileAsync(localFile: IFile): Promise<FileNode> {
await this.fileSource.writeFile(localFile, this._path);
let fileNode = new FileNode(this.context, File.createPath(this._path, File.getBasename(localFile)), this.fileSource);
return fileNode;
}
public async mkdir(name: string): Promise<FolderNode> {
return this.runChildAddAction<FolderNode>(() => this.mkdirAsync(name));
}
private async mkdirAsync(name: string): Promise<FolderNode> {
await this.fileSource.mkdir(name, this._path);
let subDir = new FolderNode(this.context, File.createPath(this._path, name), this.fileSource);
return subDir;
}
private async runChildAddAction<T extends TreeNode>(action: () => Promise<T>): Promise<T> {
let node = await action();
await this.getChildren(true);
if (this.children) {
// Find the child matching the node. This is necessary
// since writing can add duplicates.
node = this.children.find(n => n.nodePathValue === node.nodePathValue) as T;
this.context.changeHandler.notifyNodeChanged(this);
} else {
// Failed to retrieve children from server so something went wrong
node = undefined;
}
return node;
}
}
export class ConnectionNode extends FolderNode {
constructor(context: TreeDataContext, private displayName: string, fileSource: IFileSource) {
super(context, '/', fileSource, Constants.HdfsItems.Connection);
}
getDisplayName(): string {
return this.displayName;
}
public async delete(): Promise<void> {
throw new Error(localize('errDeleteConnectionNode', 'Cannot delete a connection. Only subfolders and files can be deleted.'));
}
async getTreeItem(): Promise<vscode.TreeItem> {
let item = await super.getTreeItem();
item.contextValue = this._nodeType;
return item;
}
}
export class FileNode extends HdfsFileSourceNode implements IFileNode {
constructor(context: TreeDataContext, path: string, fileSource: IFileSource) {
super(context, path, fileSource);
}
public onChildRemoved(): void {
// do nothing
}
getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
return [];
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.None);
item.iconPath = {
dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'),
light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg')
};
item.contextValue = Constants.HdfsItems.File;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
// TODO improve node type handling so it's not tied to SQL Server types
let nodeInfo: sqlops.NodeInfo = {
label: this.getDisplayName(),
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: Constants.HdfsItems.File,
nodeSubType: this.getSubType(),
iconType: 'FileGroupFile'
};
return nodeInfo;
}
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes);
return contents ? contents.toString('utf8') : '';
}
public async getFileLinesAsString(maxLines: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines);
return contents ? contents.toString('utf8') : '';
}
public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
return new Promise((resolve, reject) => {
let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath);
let writeStream = fs.createWriteStream(localPath, {
encoding: 'utf8'
});
let cancelable = new CancelableStream(cancelToken);
cancelable.on('error', (err) => {
reject(err);
});
readStream.pipe(cancelable).pipe(writeStream);
let error: string | Error = undefined;
writeStream.on('error', (err) => {
error = err;
reject(error);
});
writeStream.on('finish', (location) => {
if (!error) {
resolve(vscode.Uri.file(localPath));
}
});
});
}
private getSubType(): string {
if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) {
return Constants.HdfsItemsSubType.Spark;
}
return undefined;
}
}
export class MessageNode extends TreeNode {
static messageNum: number = 0;
private _nodePathValue: string;
constructor(private message: string) {
super();
}
public static create(message: string, parent: TreeNode): MessageNode {
let node = new MessageNode(message);
node.parent = parent;
return node;
}
private ensureNodePathValue(): void {
if (!this._nodePathValue) {
this._nodePathValue = `message_${MessageNode.messageNum++}`;
}
}
public get nodePathValue(): string {
this.ensureNodePathValue();
return this._nodePathValue;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
return [];
}
public getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None);
item.contextValue = Constants.HdfsItems.Message;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: this.message,
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: Constants.HdfsItems.Message,
nodeSubType: undefined,
iconType: 'MessageType'
};
return nodeInfo;
}
}

View File

@@ -0,0 +1,347 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import { ProviderBase } from './providerBase';
import { Connection } from './connection';
import * as utils from '../utils';
import { TreeNode } from './treeNodes';
import { ConnectionNode, TreeDataContext, ITreeChangeHandler } from './hdfsProvider';
import { IFileSource } from './fileSources';
import { AppContext } from '../appContext';
import * as constants from '../constants';
const outputChannel = vscode.window.createOutputChannel(constants.providerId);
interface IEndpoint {
serviceName: string;
ipAddress: string;
port: number;
}
export class MssqlObjectExplorerNodeProvider extends ProviderBase implements sqlops.ObjectExplorerNodeProvider, ITreeChangeHandler {
public readonly supportedProviderId: string = constants.providerId;
private sessionMap: Map<string, Session>;
private expandCompleteEmitter = new vscode.EventEmitter<sqlops.ObjectExplorerExpandInfo>();
constructor(private appContext: AppContext) {
super();
this.sessionMap = new Map();
this.appContext.registerService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
}
handleSessionOpen(session: sqlops.ObjectExplorerSession): Thenable<boolean> {
return new Promise((resolve, reject) => {
if (!session) {
reject('handleSessionOpen requires a session object to be passed');
} else {
resolve(this.doSessionOpen(session));
}
});
}
private async doSessionOpen(sessionInfo: sqlops.ObjectExplorerSession): Promise<boolean> {
let connectionProfile = await sqlops.objectexplorer.getSessionConnectionProfile(sessionInfo.sessionId);
if (!connectionProfile) {
return false;
} else {
let credentials = await sqlops.connection.getCredentials(connectionProfile.id);
let serverInfo = await sqlops.connection.getServerInfo(connectionProfile.id);
if (!serverInfo || !credentials || !serverInfo.options) {
return false;
}
let endpoints: IEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
if (!endpoints || endpoints.length === 0) {
return false;
}
let index = endpoints.findIndex(ep => ep.serviceName === constants.hadoopKnoxEndpointName);
if (index === -1) {
return false;
}
let connInfo: sqlops.connection.Connection = {
options: {
'host': endpoints[index].ipAddress,
'groupId': connectionProfile.options.groupId,
'knoxport': endpoints[index].port,
'user': 'root', //connectionProfile.options.userName cluster setup has to have the same user for master and big data cluster
'password': credentials.password,
},
providerName: constants.mssqlClusterProviderName,
connectionId: UUID.generateUuid()
};
let connection = new Connection(connInfo);
connection.saveUriWithPrefix(constants.objectExplorerPrefix);
let session = new Session(connection, sessionInfo.sessionId);
session.root = new RootNode(session, new TreeDataContext(this.appContext.extensionContext, this), sessionInfo.rootNode.nodePath);
this.sessionMap.set(sessionInfo.sessionId, session);
return true;
}
}
expandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Thenable<boolean> {
return new Promise((resolve, reject) => {
if (!nodeInfo) {
reject('expandNode requires a nodeInfo object to be passed');
} else {
resolve(this.doExpandNode(nodeInfo, isRefresh));
}
});
}
private async doExpandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
let session = this.sessionMap.get(nodeInfo.sessionId);
let response = {
sessionId: nodeInfo.sessionId,
nodePath: nodeInfo.nodePath,
errorMessage: undefined,
nodes: []
};
if (!session) {
// This is not an error case. Just fire reponse with empty nodes for example: request from standalone SQL instance
this.expandCompleteEmitter.fire(response);
return false;
} else {
setTimeout(() => {
// Running after promise resolution as we need the Ops Studio-side map to have been updated
// Intentionally not awaiting or catching errors.
// Any failure in startExpansion should be emitted in the expand complete result
// We want this to be async and ideally return true before it completes
this.startExpansion(session, nodeInfo, isRefresh);
}, 10);
}
return true;
}
private async startExpansion(session: Session, nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<void> {
let expandResult: sqlops.ObjectExplorerExpandInfo = {
sessionId: session.uri,
nodePath: nodeInfo.nodePath,
errorMessage: undefined,
nodes: []
};
try {
let node = await session.root.findNodeByPath(nodeInfo.nodePath, true);
if (node) {
expandResult.errorMessage = node.getNodeInfo().errorMessage;
let children = await node.getChildren(true);
if (children) {
expandResult.nodes = children.map(c => c.getNodeInfo());
}
}
} catch (error) {
expandResult.errorMessage = utils.getErrorMessage(error);
}
this.expandCompleteEmitter.fire(expandResult);
}
refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
// TODO #3815 implement properly
return this.expandNode(nodeInfo, true);
}
handleSessionClose(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void {
this.sessionMap.delete(closeSessionInfo.sessionId);
}
findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> {
// TODO #3814 implement
let response: sqlops.ObjectExplorerFindNodesResponse = {
nodes: []
};
return Promise.resolve(response);
}
registerOnExpandCompleted(handler: (response: sqlops.ObjectExplorerExpandInfo) => any): void {
this.expandCompleteEmitter.event(handler);
}
notifyNodeChanged(node: TreeNode): void {
this.notifyNodeChangesAsync(node);
}
private async notifyNodeChangesAsync(node: TreeNode): Promise<void> {
try {
let session = this.getSessionForNode(node);
if (!session) {
this.appContext.apiWrapper.showErrorMessage(localize('sessionNotFound', 'Session for node {0} does not exist', node.nodePathValue));
} else {
let nodeInfo = node.getNodeInfo();
let expandInfo: sqlops.ExpandNodeInfo = {
nodePath: nodeInfo.nodePath,
sessionId: session.uri
};
await this.refreshNode(expandInfo);
}
} catch (err) {
outputChannel.appendLine(localize('notifyError', 'Error notifying of node change: {0}', err));
}
}
private getSessionForNode(node: TreeNode): Session {
let rootNode: DataServicesNode = undefined;
while (rootNode === undefined && node !== undefined) {
if (node instanceof DataServicesNode) {
rootNode = node;
break;
} else {
node = node.parent;
}
}
if (rootNode) {
return rootNode.session;
}
// Not found
return undefined;
}
async findNodeForContext<T extends TreeNode>(explorerContext: sqlops.ObjectExplorerContext): Promise<T> {
let node: T = undefined;
let session = this.findSessionForConnection(explorerContext.connectionProfile);
if (session) {
if (explorerContext.isConnectionNode) {
// Note: ideally fix so we verify T matches RootNode and go from there
node = <T><any>session.root;
} else {
// Find the node under the session
node = <T><any>await session.root.findNodeByPath(explorerContext.nodeInfo.nodePath, true);
}
}
return node;
}
private findSessionForConnection(connectionProfile: sqlops.IConnectionProfile): Session {
for (let session of this.sessionMap.values()) {
if (session.connection && session.connection.isMatch(connectionProfile)) {
return session;
}
}
return undefined;
}
}
export class Session {
private _root: RootNode;
constructor(private _connection: Connection, private sessionId?: string) {
}
public get uri(): string {
return this.sessionId || this._connection.uri;
}
public get connection(): Connection {
return this._connection;
}
public set root(node: RootNode) {
this._root = node;
}
public get root(): RootNode {
return this._root;
}
}
class RootNode extends TreeNode {
private children: TreeNode[];
constructor(private _session: Session, private context: TreeDataContext, private nodePath: string) {
super();
}
public get session(): Session {
return this._session;
}
public get nodePathValue(): string {
return this.nodePath;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.children = [];
let dataServicesNode = new DataServicesNode(this._session, this.context, this.nodePath);
dataServicesNode.parent = this;
this.children.push(dataServicesNode);
}
return this.children;
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
throw new Error('Not intended for use in a file explorer view.');
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: localize('rootLabel', 'Root'),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: 'sqlCluster:root',
nodeSubType: undefined,
iconType: 'folder'
};
return nodeInfo;
}
}
class DataServicesNode extends TreeNode {
private children: TreeNode[];
constructor(private _session: Session, private context: TreeDataContext, private nodePath: string) {
super();
}
public get session(): Session {
return this._session;
}
public get nodePathValue(): string {
return this.nodePath;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.children = [];
let hdfsNode = new ConnectionNode(this.context, localize('hdfsFolder', 'HDFS'), this.createHdfsFileSource());
hdfsNode.parent = this;
this.children.push(hdfsNode);
}
return this.children;
}
private createHdfsFileSource(): IFileSource {
return this.session.connection.createHdfsFileSource();
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
throw new Error('Not intended for use in a file explorer view.');
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: localize('dataServicesLabel', 'Data Services'),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: 'dataservices',
nodeSubType: undefined,
iconType: 'folder'
};
return nodeInfo;
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as constants from '../constants';
export abstract class ProviderBase {
public readonly providerId: string = constants.mssqlClusterProviderName;
public handle: number;
}

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { ITreeNode } from './types';
type TreeNodePredicate = (node: TreeNode) => boolean;
export abstract class TreeNode implements ITreeNode {
private _parent: TreeNode = undefined;
public get parent(): TreeNode {
return this._parent;
}
public set parent(node: TreeNode) {
this._parent = node;
}
public generateNodePath(): string {
let path = undefined;
if (this.parent) {
path = this.parent.generateNodePath();
}
path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue;
return path;
}
public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise<TreeNode> {
let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path || node.getNodeInfo().nodePath.startsWith(path);
let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath);
return TreeNode.findNode(this, condition, filter, true);
}
public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise<TreeNode> {
if (!node) {
return undefined;
}
if (condition(node)) {
return node;
}
let nodeInfo = node.getNodeInfo();
if (nodeInfo.isLeaf) {
return undefined;
}
// TODO #3813 support filtering by already expanded / not yet expanded
let children = await node.getChildren(false);
if (children) {
for (let child of children) {
if (filter && filter(child)) {
let childNode = await this.findNode(child, condition, filter, expandIfNeeded);
if (childNode) {
return childNode;
}
}
}
}
return undefined;
}
/**
* The value to use for this node in the node path
*/
public abstract get nodePathValue(): string;
abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
abstract getNodeInfo(): sqlops.NodeInfo;
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
/**
* A tree node in the object explorer tree
*
* @export
* @interface ITreeNode
*/
export interface ITreeNode {
getNodeInfo(): sqlops.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
}
/**
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
* can be queried
*
* @export
* @interface IFileNode
* @extends {ITreeNode}
*/
export interface IFileNode extends ITreeNode {
getFileContentsAsString(maxBytes?: number): Promise<string>;
}