This commit is contained in:
Joao Moreno 2018-03-01 15:45:56 +01:00
parent 8c938b14ee
commit b51d61e362
6 changed files with 334 additions and 21 deletions

View file

@ -10,7 +10,7 @@ import { Readable } from 'stream';
import { nfcall, ninvoke, SimpleThrottler } from 'vs/base/common/async';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { TPromise } from 'vs/base/common/winjs.base';
import { open as openZip, Entry, ZipFile } from 'yauzl';
import { open as _openZip, Entry, ZipFile } from 'yauzl';
export interface IExtractOptions {
overwrite?: boolean;
@ -26,6 +26,29 @@ interface IOptions {
sourcePathRegex: RegExp;
}
export enum ExtractErrorType {
Undefined,
CorruptZip
}
export class ExtractError extends Error {
readonly type: ExtractErrorType;
readonly cause: Error;
constructor(type: ExtractErrorType, cause: Error) {
let message = cause.message;
switch (type) {
case ExtractErrorType.CorruptZip: message = `Corrupt ZIP: ${message}`; break;
}
super(message);
this.type = type;
this.cause = cause;
}
}
function modeFromEntry(entry: Entry) {
let attr = entry.externalFileAttributes >> 16 || 33188;
@ -34,6 +57,18 @@ function modeFromEntry(entry: Entry) {
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
}
function toExtractError(err: Error): ExtractError {
let type = ExtractErrorType.CorruptZip;
console.log('WHAT');
if (/end of central directory record signature not found/.test(err.message)) {
type = ExtractErrorType.CorruptZip;
}
return new ExtractError(type, err);
}
function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions): TPromise<void> {
const dirName = path.dirname(fileName);
const targetDirName = path.join(targetPath, dirName);
@ -74,13 +109,18 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions): TP
last = throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options)));
});
});
}).then(null, err => TPromise.wrapError(toExtractError(err)));
}
function openZip(zipFile: string): TPromise<ZipFile> {
return nfcall<ZipFile>(_openZip, zipFile)
.then(null, err => TPromise.wrapError(toExtractError(err)));
}
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}): TPromise<void> {
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
let promise = nfcall<ZipFile>(openZip, zipPath);
let promise = openZip(zipPath);
if (options.overwrite) {
promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile));
@ -90,7 +130,7 @@ export function extract(zipPath: string, targetPath: string, options: IExtractOp
}
function read(zipPath: string, filePath: string): TPromise<Readable> {
return nfcall(openZip, zipPath).then((zipfile: ZipFile) => {
return openZip(zipPath).then(zipfile => {
return new TPromise<Readable>((c, e) => {
zipfile.on('entry', (entry: Entry) => {
if (entry.fileName === filePath) {

View file

@ -26,7 +26,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery';
import { IFileService, IContent } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
import { IExtensionService, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import URI from 'vs/base/common/uri';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
@ -42,7 +42,38 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
class DownloadExtensionAction extends Action {
constructor(
private extension: IExtension,
@IOpenerService private openerService: IOpenerService,
@INotificationService private notificationService: INotificationService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super('extensions.download', localize('download', "Download Manually"), '', true);
}
run(): TPromise<void> {
return this.openerService.open(URI.parse(this.extension.downloadUrl)).then(() => {
this.notificationService.notify({
severity: Severity.Info,
message: localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.id),
actions: {
primary: [
this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)
]
}
});
});
}
}
export class InstallAction extends Action {
@ -58,7 +89,9 @@ export class InstallAction extends Action {
set extension(extension: IExtension) { this._extension = extension; this.update(); }
constructor(
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private instantiationService: IInstantiationService,
@INotificationService private notificationService: INotificationService
) {
super('extensions.install', InstallAction.InstallLabel, InstallAction.Class, false);
@ -89,7 +122,27 @@ export class InstallAction extends Action {
run(): TPromise<any> {
this.extensionsWorkbenchService.open(this.extension);
return this.extensionsWorkbenchService.install(this.extension);
return this.install(this.extension);
}
private install(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.install(extension).then(null, err => {
if (!extension.downloadUrl) {
return this.notificationService.error(err);
}
console.error(err);
this.notificationService.notify({
severity: Severity.Error,
message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id),
actions: {
primary: [
this.instantiationService.createInstance(DownloadExtensionAction, extension)
]
}
});
});
}
dispose(): void {
@ -251,7 +304,9 @@ export class UpdateAction extends Action {
set extension(extension: IExtension) { this._extension = extension; this.update(); }
constructor(
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private instantiationService: IInstantiationService,
@INotificationService private notificationService: INotificationService
) {
super('extensions.update', UpdateAction.Label, UpdateAction.DisabledClass, false);
@ -283,7 +338,26 @@ export class UpdateAction extends Action {
}
run(): TPromise<any> {
return this.extensionsWorkbenchService.install(this.extension);
return this.install(this.extension);
}
private install(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.install(extension).then(null, err => {
if (!extension.downloadUrl) {
return this.notificationService.error(err);
}
console.error(err);
this.notificationService.notify({
severity: Severity.Error,
message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id),
actions: {
primary: [
this.instantiationService.createInstance(DownloadExtensionAction, extension)
]
}
});
});
}
dispose(): void {
@ -757,7 +831,9 @@ export class UpdateAllAction extends Action {
constructor(
id = UpdateAllAction.ID,
label = UpdateAllAction.LABEL,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@INotificationService private notificationService: INotificationService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super(id, label, '', false);
@ -774,7 +850,26 @@ export class UpdateAllAction extends Action {
}
run(): TPromise<any> {
return TPromise.join(this.outdated.map(e => this.extensionsWorkbenchService.install(e)));
return TPromise.join(this.outdated.map(e => this.install(e)));
}
private install(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.install(extension).then(null, err => {
if (!extension.downloadUrl) {
return this.notificationService.error(err);
}
console.error(err);
this.notificationService.notify({
severity: Severity.Error,
message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id),
actions: {
primary: [
this.instantiationService.createInstance(DownloadExtensionAction, extension)
]
}
});
});
}
dispose(): void {
@ -1109,7 +1204,8 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action {
@IViewletService private viewletService: IViewletService,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionTipsService private extensionTipsService: IExtensionTipsService,
@INotificationService private notificationService: INotificationService
@INotificationService private notificationService: INotificationService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super(id, label, 'extension-action');
this.extensionsWorkbenchService.onChange(() => this.update(), this, this.disposables);
@ -1154,18 +1250,37 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action {
extensionsWithDependencies.push(e);
return TPromise.as(null);
} else {
return this.extensionsWorkbenchService.install(e);
return this.install(e);
}
}));
}
return TPromise.join(installPromises).then(() => {
return TPromise.join(extensionsWithDependencies.map(e => this.extensionsWorkbenchService.install(e)));
return TPromise.join(extensionsWithDependencies.map(e => this.install(e)));
});
});
});
});
}
private install(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.install(extension).then(null, err => {
if (!extension.downloadUrl) {
return this.notificationService.error(err);
}
console.error(err);
this.notificationService.notify({
severity: Severity.Error,
message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id),
actions: {
primary: [
this.instantiationService.createInstance(DownloadExtensionAction, extension)
]
}
});
});
}
dispose(): void {
this.disposables = dispose(this.disposables);
super.dispose();
@ -1184,7 +1299,8 @@ export class InstallRecommendedExtensionAction extends Action {
extensionId: string,
@IViewletService private viewletService: IViewletService,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@INotificationService private notificationService: INotificationService
@INotificationService private notificationService: INotificationService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, null);
this.extensionId = extensionId;
@ -1210,11 +1326,30 @@ export class InstallRecommendedExtensionAction extends Action {
viewlet.focus();
return this.extensionsWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation' }).then(pager => {
return (pager && pager.firstPage && pager.firstPage.length) ? this.extensionsWorkbenchService.install(pager.firstPage[0]) : TPromise.as(null);
return (pager && pager.firstPage && pager.firstPage.length) ? this.install(pager.firstPage[0]) : TPromise.as(null);
});
});
}
private install(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.install(extension).then(null, err => {
if (!extension.downloadUrl) {
return this.notificationService.error(err);
}
console.error(err);
this.notificationService.notify({
severity: Severity.Error,
message: localize('failedToInstall', "Failed to install \'{0}\'.", extension.id),
actions: {
primary: [
this.instantiationService.createInstance(DownloadExtensionAction, extension)
]
}
});
});
}
dispose(): void {
this.disposables = dispose(this.disposables);
super.dispose();
@ -1706,6 +1841,137 @@ export class EnableAllWorkpsaceAction extends Action {
}
}
export class OpenExtensionsFolderAction extends Action {
static readonly ID = 'workbench.extensions.action.openExtensionsFolder';
static LABEL = localize('openExtensionsFolder', "Open Extensions Folder");
constructor(
id: string,
label: string,
@IWindowsService private windowsService: IWindowsService,
@IFileService private fileService: IFileService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
super(id, label, null, true);
}
run(): TPromise<void> {
const extensionsHome = this.environmentService.extensionsPath;
return this.fileService.resolveFile(URI.file(extensionsHome)).then(file => {
let itemToShow: string;
if (file.children && file.children.length > 0) {
itemToShow = file.children[0].resource.fsPath;
} else {
itemToShow = paths.normalize(extensionsHome, true);
}
return this.windowsService.showItemInFolder(itemToShow);
});
}
}
export class InstallVSIXAction extends Action {
static readonly ID = 'workbench.extensions.action.installVSIX';
static LABEL = localize('installVSIX', "Install from VSIX...");
constructor(
id = InstallVSIXAction.ID,
label = InstallVSIXAction.LABEL,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IChoiceService private choiceService: IChoiceService,
@IWindowService private windowsService: IWindowService
) {
super(id, label, 'extension-action install-vsix', true);
}
run(): TPromise<any> {
return this.windowsService.showOpenDialog({
title: localize('installFromVSIX', "Install from VSIX"),
filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }],
properties: ['openFile'],
buttonLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"))
}).then(result => {
if (!result) {
return TPromise.as(null);
}
return TPromise.join(result.map(vsix => this.extensionsWorkbenchService.install(vsix))).then(() => {
return this.choiceService.choose(Severity.Info, localize('InstallVSIXAction.success', "Successfully installed the extension. Reload to enable it."), [localize('InstallVSIXAction.reloadNow', "Reload Now")]).then(choice => {
if (choice === 0) {
return this.windowsService.reloadWindow();
}
return TPromise.as(undefined);
});
});
});
}
}
export class ReinstallAction extends Action {
static readonly ID = 'workbench.extensions.action.reinstall';
static LABEL = localize('reinstall', "Reinstall Extension...");
constructor(
id: string = ReinstallAction.ID, label: string = ReinstallAction.LABEL,
@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
@IQuickOpenService private quickOpenService: IQuickOpenService,
@INotificationService private notificationService: INotificationService,
@IWindowService private windowService: IWindowService
) {
super(id, label);
}
get enabled(): boolean {
return this.extensionsWorkbenchService.local.filter(l => l.type === LocalExtensionType.User && l.local).length > 0;
}
run(): TPromise<any> {
return this.quickOpenService.pick(this.getEntries(), { placeHolder: localize('selectExtension', "Select Extension to Reinstall") });
}
private getEntries(): TPromise<IPickOpenEntry[]> {
return this.extensionsWorkbenchService.queryLocal()
.then(local => {
const entries: IPickOpenEntry[] = local
.filter(extension => extension.type === LocalExtensionType.User)
.map(extension => {
return <IPickOpenEntry>{
id: extension.id,
label: extension.displayName,
description: extension.id,
run: () => this.reinstallExtension(extension),
};
});
return entries;
});
}
private reinstallExtension(extension: IExtension): TPromise<void> {
return this.extensionsWorkbenchService.reinstall(extension)
.then(() => {
this.notificationService.notify({
message: localize('ReinstallAction.success', "Successfully reinstalled the extension."),
severity: Severity.Info,
actions: {
primary: [<IAction>{
id: 'reload',
label: localize('ReinstallAction.reloadNow', "Reload Now"),
enabled: true,
run: () => this.windowService.reloadWindow(),
dispose: () => null
}]
}
});
}, error => this.notificationService.error(error));
}
}
CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForLanguage', function (accessor: ServicesAccessor, fileExtension: string) {
const viewletService = accessor.get(IViewletService);

View file

@ -36,6 +36,7 @@ export interface IExtension {
latestVersion: string;
description: string;
url: string;
downloadUrl: string;
repository: string;
iconUrl: string;
iconUrlFallback: string;

View file

@ -22,9 +22,8 @@ import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/e
import {
OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction,
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction,
EnableAllAction, EnableAllWorkpsaceAction, DisableAllAction, DisableAllWorkpsaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor
EnableAllAction, EnableAllWorkpsaceAction, DisableAllAction, DisableAllWorkpsaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, OpenExtensionsFolderAction, InstallVSIXAction, ReinstallAction
} from 'vs/workbench/parts/extensions/browser/extensionsActions';
import { OpenExtensionsFolderAction, InstallVSIXAction, ReinstallAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput';
import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { ExtensionEditor } from 'vs/workbench/parts/extensions/browser/extensionEditor';

View file

@ -29,10 +29,9 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionS
import {
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction,
ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction,
EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction
EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction
} from 'vs/workbench/parts/extensions/browser/extensionsActions';
import { LocalExtensionType, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { InstallVSIXAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput';
import { ExtensionsListView, InstalledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView } from './extensionsViews';
import { OpenGlobalSettingsAction } from 'vs/workbench/parts/preferences/browser/preferencesActions';

View file

@ -117,6 +117,14 @@ class Extension implements IExtension {
return `${product.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`;
}
get downloadUrl(): string {
if (!product.extensionsGallery) {
return null;
}
return `${product.extensionsGallery.serviceUrl}/publishers/${this.publisher}/vsextensions/${this.name}/${this.latestVersion}/vspackage`;
}
get iconUrl(): string {
return this.galleryIconUrl || this.localIconUrl || this.defaultIconUrl;
}