Skip to content

Commit

Permalink
#93960 Ability to downlod and replace from cloud
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed May 12, 2020
1 parent 645f585 commit 26cbe9d
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 25 deletions.
28 changes: 28 additions & 0 deletions src/vs/platform/userDataSync/common/abstractSynchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,33 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}

async replace(uri: URI): Promise<boolean> {
const content = await this.resolveContent(uri);
if (!content) {
return false;
}

const syncData = this.parseSyncData(content);
if (!syncData) {
return false;
}

await this.stop();

try {
this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
await this.performReplace(syncData, remoteUserData, lastSyncUserData);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}

return true;
}

private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (lastSyncUserData) {

Expand Down Expand Up @@ -323,6 +350,7 @@ export abstract class AbstractSynchroniser extends Disposable {

protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
}

Expand Down
12 changes: 12 additions & 0 deletions src/vs/platform/userDataSync/common/extensionsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return SyncStatus.Idle;
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const localExtensions = await this.getLocalExtensions();
const syncExtensions = this.parseExtensions(syncData);
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], this.getIgnoredExtensions());

await this.apply({
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: true
});
}

protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
Expand Down
14 changes: 13 additions & 1 deletion src/vs/platform/userDataSync/common/globalStateSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
Expand Down Expand Up @@ -200,6 +200,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return SyncStatus.Idle;
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
await this.apply({
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: true
});
}

protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
Expand Down
20 changes: 19 additions & 1 deletion src/vs/platform/userDataSync/common/keybindingsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
Expand Down Expand Up @@ -212,6 +212,24 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
}
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const content = this.getKeybindingsContentFromSyncContent(syncData.content);

if (content !== null) {
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: true,
}));
await this.apply();
}
}

private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
Expand Down
23 changes: 22 additions & 1 deletion src/vs/platform/userDataSync/common/settingsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
Expand Down Expand Up @@ -257,6 +257,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
}
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: true,
hasRemoteChanged: true,
hasConflicts: false,
}));

await this.apply();
}
}

private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
Expand Down
13 changes: 13 additions & 0 deletions src/vs/platform/userDataSync/common/snippetsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true
}));
await this.apply();
}

protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
Expand Down
12 changes: 12 additions & 0 deletions src/vs/platform/userDataSync/common/userDataSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ export interface IUserDataSynchroniser {
pull(): Promise<void>;
push(): Promise<void>;
sync(manifest: IUserDataManifest | null): Promise<void>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;

getSyncPreview(): Promise<ISyncPreviewResult>
Expand Down Expand Up @@ -330,6 +331,7 @@ export interface IUserDataSyncService {
pull(): Promise<void>;
sync(): Promise<void>;
stop(): Promise<void>;
replace(uri: URI): Promise<void>;
reset(): Promise<void>;
resetLocal(): Promise<void>;

Expand Down Expand Up @@ -380,3 +382,13 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe
localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme });
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
}

export function getSyncAreaLabel(source: SyncResource): string {
switch (source) {
case SyncResource.Settings: return localize('settings', "Settings");
case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts");
case SyncResource.Snippets: return localize('snippets', "User Snippets");
case SyncResource.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State");
}
}
1 change: 1 addition & 0 deletions src/vs/platform/userDataSync/common/userDataSyncIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class UserDataSyncChannel implements IServerChannel {
case 'pull': return this.service.pull();
case 'sync': return this.service.sync();
case 'stop': this.service.stop(); return Promise.resolve();
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal();
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
Expand Down
9 changes: 9 additions & 0 deletions src/vs/platform/userDataSync/common/userDataSyncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}

async replace(uri: URI): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}

async stop(): Promise<void> {
await this.checkEnablement();
if (this.status === SyncStatus.Idle) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { Barrier } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';

Expand Down Expand Up @@ -39,6 +39,8 @@ class TestSynchroniser extends AbstractSynchroniser {
return this.syncResult.status || SyncStatus.Idle;
}

protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> { }

async apply(ref: string): Promise<void> {
ref = await this.userDataSyncStoreService.write(this.resource, '', ref);
await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } });
Expand Down
12 changes: 1 addition & 11 deletions src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
CONTEXT_SYNC_STATE, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT,
SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview
SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview, getSyncAreaLabel
} from 'vs/platform/userDataSync/common/userDataSync';
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
Expand All @@ -54,16 +54,6 @@ const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources',

type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string };

function getSyncAreaLabel(source: SyncResource): string {
switch (source) {
case SyncResource.Settings: return localize('settings', "Settings");
case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts");
case SyncResource.Snippets: return localize('snippets', "User Snippets");
case SyncResource.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State");
}
}

type SyncConflictsClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
Expand Down
51 changes: 41 additions & 10 deletions src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import { localize } from 'vs/nls';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus, getSyncAreaLabel } from 'vs/platform/userDataSync/common/userDataSync';
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { FolderThemeIcon } from 'vs/platform/theme/common/themeService';
import { fromNow } from 'vs/base/common/date';
import { pad, uppercaseFirstLetter } from 'vs/base/common/strings';
import { pad } from 'vs/base/common/strings';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Codicon } from 'vs/base/common/codicons';
import { CONTEXT_ACCOUNT_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
import { AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';

export const VIEW_CONTAINER_ID = 'workbench.view.sync';
const CONTEXT_ENABLE_VIEWS = new RawContextKey<boolean>(`showUserDataSyncViews`, false);
Expand Down Expand Up @@ -132,8 +133,37 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution {
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const { resource } = <{ resource: string }>JSON.parse(handle.$treeItemHandle);
const editorService = accessor.get(IEditorService);
await editorService.openEditor({ resource: URI.parse(handle.$treeItemHandle) });
await editorService.openEditor({ resource: URI.parse(resource) });
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.replaceCurrent`,
title: localize('workbench.actions.sync.replaceCurrent', "Download..."),
icon: { id: 'codicon/cloud-download' },
menu: {
id: MenuId.ViewItemContext,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)),
group: 'inline',
},
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const dialogService = accessor.get(IDialogService);
const userDataSyncService = accessor.get(IUserDataSyncService);
const { resource, syncResource } = <{ resource: string, syncResource: SyncResource }>JSON.parse(handle.$treeItemHandle);
const result = await dialogService.confirm({
message: localize('confirm replace', "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource)),
type: 'info',
title: localize('preferences sync', "Preferences Sync")
});
if (result.confirmed) {
return userDataSyncService.replace(URI.parse(resource));
}
}
});

Expand Down Expand Up @@ -179,23 +209,24 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider {
return ALL_SYNC_RESOURCES.map(resourceKey => ({
handle: resourceKey,
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: uppercaseFirstLetter(resourceKey) },
label: { label: getSyncAreaLabel(resourceKey) },
themeIcon: FolderThemeIcon,
}));
}
const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource;
if (resourceKey) {
const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(resourceKey) : await this.userDataSyncService.getLocalSyncResourceHandles(resourceKey);
const syncResource = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource;
if (syncResource) {
const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(syncResource) : await this.userDataSyncService.getLocalSyncResourceHandles(syncResource);
return refHandles.map(({ uri, created }) => {
const handle = JSON.stringify({ resource: uri.toString(), syncResource });
return <SyncResourceTreeItem>{
handle: uri.toString(),
handle,
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: label(new Date(created)) },
description: fromNow(created, true),
resourceUri: uri,
resource: resourceKey,
resource: syncResource,
resourceHandle: { uri, created },
contextValue: `sync-resource-${resourceKey}`
contextValue: `sync-resource-${syncResource}`
};
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('stop');
}

replace(uri: URI): Promise<void> {
return this.channel.call('replace', [uri]);
}

reset(): Promise<void> {
return this.channel.call('reset');
}
Expand Down

0 comments on commit 26cbe9d

Please sign in to comment.