diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index f8bb0950acfa1..f5bd9adc5a709 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -70,7 +70,7 @@ import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; -import { IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration as registerUserDataSyncConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration as registerUserDataSyncConfiguration, IUserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataAutoSyncChannel, UserDataSyncAccountServiceChannel, UserDataSyncMachinesServiceChannel, UserDataSyncStoreManagementServiceChannel, UserDataSyncUtilServiceClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -112,6 +112,7 @@ import { UserDataProfilesCleaner } from 'vs/code/electron-browser/sharedProcess/ import { RemoteTunnelService } from 'vs/platform/remoteTunnel/electron-browser/remoteTunnelService'; import { IRemoteTunnelService } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { ISharedProcessLifecycleService, SharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService'; +import { UserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSyncResourceProvider'; class SharedProcessMain extends Disposable { @@ -361,6 +362,7 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); services.set(IUserDataSyncProfilesStorageService, new SyncDescriptor(UserDataSyncProfilesStorageService, undefined, true)); + services.set(IUserDataSyncResourceProviderService, new SyncDescriptor(UserDataSyncResourceProviderService, undefined, true)); // Terminal diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 1f03fd4edb9b9..94633b4613b9f 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -14,7 +14,7 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtUri } from 'vs/base/common/resources'; import { uppercaseFirstLetter } from 'vs/base/common/strings'; -import { isString, isUndefined } from 'vs/base/common/types'; +import { isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IHeaders } from 'vs/base/parts/request/common/request'; import { localize } from 'vs/nls'; @@ -26,7 +26,7 @@ import { getServiceMachineId } from 'vs/platform/externalServices/common/service import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, ISyncResourceHandle, IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; type IncompatibleSyncSourceClassification = { @@ -242,12 +242,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa } } - async replace(uri: URI): Promise { - const content = await this.resolveContent(uri); - if (!content) { - return false; - } - + async replace(content: string): Promise { const syncData = this.parseSyncData(content); if (!syncData) { return false; @@ -490,48 +485,6 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa return !!lastSyncData && lastSyncData.syncData !== null /* `null` sync data implies resource is not synced */; } - async getRemoteSyncResourceHandles(): Promise { - const handles = await this.userDataSyncStoreService.getAllResourceRefs(this.resource, this.collection); - return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); - } - - async getLocalSyncResourceHandles(): Promise { - const handles = await this.userDataSyncBackupStoreService.getAllRefs(this.syncResource.profile, this.resource); - return handles.map(({ created, ref }) => ({ created, uri: this.toLocalBackupResource(ref) })); - } - - private toRemoteBackupResource(ref: string): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${this.syncResource.profile.isDefault ? '' : `${this.syncResource.profile.id}/`}${this.resource}/${ref}` }); - } - - private toLocalBackupResource(ref: string): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.syncResource.profile.id}/${this.resource}/${ref}` }); - } - - async getMachineId({ uri }: ISyncResourceHandle): Promise { - const ref = this.extUri.basename(uri); - if (this.extUri.isEqual(uri, this.toRemoteBackupResource(ref))) { - const { content } = await this.getUserData(ref); - if (content) { - const syncData = this.parseSyncData(content); - return syncData?.machineId; - } - } - return undefined; - } - - async resolveContent(uri: URI): Promise { - const ref = this.extUri.basename(uri); - if (this.extUri.isEqual(uri, this.toRemoteBackupResource(ref))) { - const { content } = await this.getUserData(ref); - return content; - } - if (this.extUri.isEqual(uri, this.toLocalBackupResource(ref))) { - return this.userDataSyncBackupStoreService.resolveContent(this.syncResource.profile, this.resource, ref); - } - return null; - } - protected async resolvePreviewContent(uri: URI): Promise { const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null; if (syncPreview) { @@ -670,14 +623,9 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource); } - private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { - if (isString(refOrLastSyncData)) { - const content = await this.userDataSyncStoreService.resolveResourceContent(this.resource, refOrLastSyncData, this.collection); - return { ref: refOrLastSyncData, content }; - } else { - const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null; - return this.userDataSyncStoreService.readResource(this.resource, lastSyncUserData, this.collection, this.syncHeaders); - } + private async getUserData(lastSyncData: IRemoteUserData | null): Promise { + const lastSyncUserData: IUserData | null = lastSyncData ? { ref: lastSyncData.ref, content: lastSyncData.syncData ? JSON.stringify(lastSyncData.syncData) : null } : null; + return this.userDataSyncStoreService.readResource(this.resource, lastSyncUserData, this.collection, this.syncHeaders); } protected async updateRemoteUserData(content: string, ref: string | null): Promise { @@ -729,7 +677,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa protected abstract hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise; abstract hasLocalData(): Promise; - abstract getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]>; + abstract resolveContent(uri: URI): Promise; } export interface IFileResourcePreview extends IResourcePreview { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index ff63fd4b7cab8..87098ee8e7135 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -30,7 +30,7 @@ import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userData import { AbstractInitializer, AbstractSynchroniser, getSyncResourceLogLabel, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { IMergeResult as IExtensionMergeResult, merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; -import { Change, IRemoteUserData, ISyncData, ISyncExtension, ISyncExtensionWithVersion, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, ISyncData, ISyncExtension, ISyncExtensionWithVersion, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; type IExtensionResourceMergeResult = IAcceptResult & IExtensionMergeResult; @@ -76,9 +76,24 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen return extensions; } -export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { +export function parseExtensions(syncData: ISyncData): ISyncExtension[] { + return JSON.parse(syncData.content); +} + +export function stringify(extensions: ISyncExtension[], format: boolean): string { + extensions.sort((e1, e2) => { + if (!e1.identifier.uuid && e2.identifier.uuid) { + return -1; + } + if (e1.identifier.uuid && !e2.identifier.uuid) { + return 1; + } + return compare(e1.identifier.id, e2.identifier.id); + }); + return format ? toFormattedString(extensions, {}) : JSON.stringify(extensions); +} - private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); +export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { /* Version 3 - Introduce installed property to skip installing built in extensions @@ -292,16 +307,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - return [{ resource: this.extUri.joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }]; - } - - override async resolveContent(uri: URI): Promise { - if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { - const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.profile); - return this.stringify(localExtensions.filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))), true); - } - + async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) || this.extUri.isEqual(this.localResource, uri) @@ -310,37 +316,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const content = await this.resolvePreviewContent(uri); return content ? this.stringify(JSON.parse(content), true) : content; } - - let content = await super.resolveContent(uri); - if (content) { - return content; - } - - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (this.extUri.basename(uri)) { - case 'extensions.json': - return this.stringify(this.parseExtensions(syncData), true); - } - } - } - return null; } private stringify(extensions: ISyncExtension[], format: boolean): string { - extensions.sort((e1, e2) => { - if (!e1.identifier.uuid && e2.identifier.uuid) { - return -1; - } - if (e1.identifier.uuid && !e2.identifier.uuid) { - return 1; - } - return compare(e1.identifier.id, e2.identifier.id); - }); - return format ? toFormattedString(extensions, {}) : JSON.stringify(extensions); + return stringify(extensions, format); } async hasLocalData(): Promise { @@ -355,10 +335,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return false; } - private parseExtensions(syncData: ISyncData): ISyncExtension[] { - return JSON.parse(syncData.content); - } - } export class LocalExtensionsProvider { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index d6a09a80247e9..bb98c1821b027 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -25,7 +25,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { AbstractInitializer, AbstractSynchroniser, getSyncResourceLogLabel, IAcceptResult, IMergeResult, IResourcePreview, isSyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; -import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, getEnablementKey, IGlobalState, IRemoteUserData, IStorageValue, ISyncData, ISyncResourceHandle, IUserData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, SYNC_SERVICE_URL_TYPE, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreType, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, getEnablementKey, IGlobalState, IRemoteUserData, IStorageValue, ISyncData, IUserData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, SYNC_SERVICE_URL_TYPE, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreType, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; @@ -47,7 +47,7 @@ export interface IGlobalStateResourcePreview extends IResourcePreview { readonly storageKeys: StorageKeys; } -function stringify(globalState: IGlobalState, format: boolean): string { +export function stringify(globalState: IGlobalState, format: boolean): string { const storageKeys = globalState.storage ? Object.keys(globalState.storage).sort() : []; const storage: IStringDictionary = {}; storageKeys.forEach(key => storage[key] = globalState.storage[key]); @@ -68,7 +68,6 @@ const GLOBAL_STATE_DATA_VERSION = 1; */ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { - private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/globalState.json` }); protected readonly version: number = GLOBAL_STATE_DATA_VERSION; private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'globalState.json'); private readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' }); @@ -258,16 +257,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - return [{ resource: this.extUri.joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }]; - } - - override async resolveContent(uri: URI): Promise { - if (this.extUri.isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) { - const localGlobalState = await this.localGlobalStateProvider.getLocalGlobalState(this.syncResource.profile); - return stringify(localGlobalState, true); - } - + async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) || this.extUri.isEqual(this.localResource, uri) @@ -276,23 +266,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const content = await this.resolvePreviewContent(uri); return content ? stringify(JSON.parse(content), true) : content; } - - let content = await super.resolveContent(uri); - if (content) { - return content; - } - - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (this.extUri.basename(uri)) { - case 'globalState.json': - return stringify(JSON.parse(syncData.content), true); - } - } - } - return null; } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 01fa5d9339c4d..02c4382cf459a 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -22,7 +22,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; -import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM } from 'vs/platform/userDataSync/common/userDataSync'; interface ISyncContent { mac?: string; @@ -282,12 +282,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return false; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; - return [{ resource: this.extUri.joinPath(uri, 'keybindings.json'), comparableResource }]; - } - - override async resolveContent(uri: URI): Promise { + async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) || this.extUri.isEqual(this.localResource, uri) @@ -295,20 +290,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem ) { return this.resolvePreviewContent(uri); } - let content = await super.resolveContent(uri); - if (content) { - return content; - } - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (this.extUri.basename(uri)) { - case 'keybindings.json': - return getKeybindingsContentFromSyncContent(syncData.content, this.syncKeybindingsPerPlatform(), this.logService); - } - } - } return null; } @@ -352,7 +333,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } private syncKeybindingsPerPlatform(): boolean { - return !!this.configurationService.getValue('settingsSync.keybindingsPerPlatform'); + return !!this.configurationService.getValue(CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM); } } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 8dda49b2082e0..da07e78c29bea 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -20,7 +20,7 @@ import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userData import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { edit } from 'vs/platform/userDataSync/common/content'; import { getIgnoredSettings, isEmpty, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { Change, CONFIGURATION_SYNC_STORE_KEY, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, CONFIGURATION_SYNC_STORE_KEY, IRemoteUserData, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; interface ISettingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; @@ -279,12 +279,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return false; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; - return [{ resource: this.extUri.joinPath(uri, 'settings.json'), comparableResource }]; - } - - override async resolveContent(uri: URI): Promise { + async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri) @@ -292,23 +287,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement ) { return this.resolvePreviewContent(uri); } - let content = await super.resolveContent(uri); - if (content) { - return content; - } - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); - if (settingsSyncContent) { - switch (this.extUri.basename(uri)) { - case 'settings.json': - return settingsSyncContent.settings; - } - } - } - } return null; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index ea95789acb1f8..48555eb486d96 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -18,7 +18,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { areSame, IMergeResult as ISnippetsMergeResult, merge } from 'vs/platform/userDataSync/common/snippetsMerge'; -import { Change, IRemoteUserData, ISyncData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, ISyncData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; interface ISnippetsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; @@ -28,6 +28,10 @@ interface ISnippetsAcceptedResourcePreview extends IFileResourcePreview { acceptResult: IAcceptResult; } +export function parseSnippets(syncData: ISyncData): IStringDictionary { + return JSON.parse(syncData.content); +} + export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { protected readonly version: number = 1; @@ -374,25 +378,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return [...resourcePreviews.values()]; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const content = await super.resolveContent(uri); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - const snippets = this.parseSnippets(syncData); - const result = []; - for (const snippet of Object.keys(snippets)) { - const resource = this.extUri.joinPath(uri, snippet); - const comparableResource = this.extUri.joinPath(this.snippetsFolder, snippet); - const exists = await this.fileService.exists(comparableResource); - result.push({ resource, comparableResource: exists ? comparableResource : this.extUri.joinPath(this.syncPreviewFolder, snippet).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }) }); - } - return result; - } - } - return []; - } - override async resolveContent(uri: URI): Promise { if (this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' })) || this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' })) @@ -400,21 +385,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD || this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }))) { return this.resolvePreviewContent(uri); } - - let content = await super.resolveContent(uri); - if (content) { - return content; - } - - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - const snippets = this.parseSnippets(syncData); - return snippets[this.extUri.basename(uri)] || null; - } - } - return null; } @@ -495,7 +465,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } private parseSnippets(syncData: ISyncData): IStringDictionary { - return JSON.parse(syncData.content); + return parseSnippets(syncData); } private toSnippetsContents(snippetsFileContents: IStringDictionary): IStringDictionary { diff --git a/src/vs/platform/userDataSync/common/tasksSync.ts b/src/vs/platform/userDataSync/common/tasksSync.ts index 55d25380021f6..544a2ea5726b5 100644 --- a/src/vs/platform/userDataSync/common/tasksSync.ts +++ b/src/vs/platform/userDataSync/common/tasksSync.ts @@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractFileSynchroniser, AbstractInitializer, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; -import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; interface ITasksSyncContent { tasks: string; @@ -224,11 +224,6 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser return this.fileService.exists(this.file); } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource; - return [{ resource: this.extUri.joinPath(uri, 'tasks.json'), comparableResource }]; - } - override async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) @@ -237,20 +232,6 @@ export class TasksSynchroniser extends AbstractFileSynchroniser implements IUser ) { return this.resolvePreviewContent(uri); } - let content = await super.resolveContent(uri); - if (content) { - return content; - } - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (this.extUri.basename(uri)) { - case 'tasks.json': - return getTasksContentFromSyncContent(syncData.content, this.logService); - } - } - } return null; } diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index a0f0af035d215..4d3767d988d43 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -15,7 +15,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { merge } from 'vs/platform/userDataSync/common/userDataProfilesManifestMerge'; -import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME, ISyncUserDataProfile, ISyncData, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME, ISyncUserDataProfile, ISyncData, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; export interface IUserDataProfileManifestResourceMergeResult extends IAcceptResult { readonly local: { added: ISyncUserDataProfile[]; removed: IUserDataProfile[]; updated: ISyncUserDataProfile[] }; @@ -29,8 +29,6 @@ export interface IUserDataProfilesManifestResourcePreview extends IResourcePrevi export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { - private static readonly PROFILES_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'profiles', path: `/profiles.json` }); - protected readonly version: number = 1; readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'profiles.json'); readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' }); @@ -80,7 +78,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i remoteChange: remote !== null ? Change.Modified : Change.None, }; - const localContent = this.stringifyLocalProfiles(localProfiles, false); + const localContent = stringifyLocalProfiles(localProfiles, false); return [{ baseResource: this.baseResource, baseContent: lastSyncProfiles ? this.stringifyRemoteProfiles(lastSyncProfiles) : null, @@ -179,7 +177,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i } if (localChange !== Change.None) { - await this.backupLocal(this.stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); + await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); const promises: Promise[] = []; for (const profile of local.added) { promises.push(this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName })); @@ -234,15 +232,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i return this.getLocalUserDataProfiles().length > 0; } - async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - return [{ resource: this.extUri.joinPath(uri, 'profiles.json'), comparableResource: UserDataProfilesManifestSynchroniser.PROFILES_DATA_URI }]; - } - - override async resolveContent(uri: URI): Promise { - if (this.extUri.isEqual(uri, UserDataProfilesManifestSynchroniser.PROFILES_DATA_URI)) { - return this.stringifyLocalProfiles(this.getLocalUserDataProfiles(), true); - } - + async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.baseResource, uri) || this.extUri.isEqual(this.localResource, uri) @@ -251,23 +241,6 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i const content = await this.resolvePreviewContent(uri); return content ? toFormattedString(JSON.parse(content), {}) : content; } - - let content = await super.resolveContent(uri); - if (content) { - return content; - } - - content = await super.resolveContent(this.extUri.dirname(uri)); - if (content) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (this.extUri.basename(uri)) { - case 'profiles.json': - return toFormattedString(parseUserDataProfilesManifest(syncData), {}); - } - } - } - return null; } @@ -279,14 +252,14 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i return JSON.stringify([...profiles].sort((a, b) => a.name.localeCompare(b.name))); } - private stringifyLocalProfiles(profiles: IUserDataProfile[], format: boolean): string { - const result = [...profiles].sort((a, b) => a.name.localeCompare(b.name)).map(p => ({ id: p.id, name: p.name })); - return format ? toFormattedString(result, {}) : JSON.stringify(result); - } +} +export function stringifyLocalProfiles(profiles: IUserDataProfile[], format: boolean): string { + const result = [...profiles].sort((a, b) => a.name.localeCompare(b.name)).map(p => ({ id: p.id, name: p.name })); + return format ? toFormattedString(result, {}) : JSON.stringify(result); } -function parseUserDataProfilesManifest(syncData: ISyncData): ISyncUserDataProfile[] { +export function parseUserDataProfilesManifest(syncData: ISyncData): ISyncUserDataProfile[] { return JSON.parse(syncData.content); } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index c3afbb7056356..c1d0e4508c3e9 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -46,6 +46,8 @@ export interface IUserDataSyncConfiguration { ignoredSettings?: string[]; } +export const CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM = 'settingsSync.keybindingsPerPlatform'; + export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -55,7 +57,7 @@ export function registerConfiguration(): IDisposable { title: localize('settings sync', "Settings Sync"), type: 'object', properties: { - 'settingsSync.keybindingsPerPlatform': { + [CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM]: { type: 'boolean', description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."), default: true, @@ -212,7 +214,7 @@ export interface IUserDataSyncBackupStoreService { readonly _serviceBrand: undefined; backup(profile: IUserDataProfile, resource: SyncResource, content: string): Promise; getAllRefs(profile: IUserDataProfile, resource: SyncResource): Promise; - resolveContent(profile: IUserDataProfile, resource: SyncResource, ref?: string): Promise; + resolveContent(profile: IUserDataProfile, resource: SyncResource, ref: string): Promise; } //#endregion @@ -426,7 +428,6 @@ export interface IUserDataSynchroniser { readonly onDidChangeLocal: Event; sync(manifest: IUserDataResourceManifest | null, headers: IHeaders): Promise; - replace(uri: URI): Promise; stop(): Promise; preview(manifest: IUserDataResourceManifest | null, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise; @@ -440,10 +441,7 @@ export interface IUserDataSynchroniser { resetLocal(): Promise; resolveContent(resource: URI): Promise; - getRemoteSyncResourceHandles(): Promise; - getLocalSyncResourceHandles(): Promise; - getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]>; - getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; + replace(content: string): Promise; } //#endregion @@ -506,6 +504,7 @@ export interface IUserDataSyncService { createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise; createManualSyncTask(): Promise; + resolveContent(resource: URI): Promise; accept(syncResource: IUserDataSyncResource, resource: URI, content: string | null | undefined, apply: boolean | { force: boolean }): Promise; reset(): Promise; @@ -514,12 +513,24 @@ export interface IUserDataSyncService { hasLocalData(): Promise; hasPreviouslySynced(): Promise; + getRemoteProfiles(): Promise; + getRemoteSyncResourceHandles(syncResource: SyncResource, profile?: ISyncUserDataProfile): Promise; + getLocalSyncResourceHandles(syncResource: SyncResource, profile?: IUserDataProfile): Promise; + getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]>; + getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; + replace(syncResourceHandle: ISyncResourceHandle): Promise; +} + +export const IUserDataSyncResourceProviderService = createDecorator('IUserDataSyncResourceProviderService'); +export interface IUserDataSyncResourceProviderService { + _serviceBrand: any; + getRemoteSyncedProfiles(): Promise; + getLocalSyncResourceHandles(syncResource: SyncResource, profile: IUserDataProfile): Promise; + getRemoteSyncResourceHandles(syncResource: SyncResource, profile: ISyncUserDataProfile | undefined): Promise; + getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]>; + getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; resolveContent(resource: URI): Promise; - replace(resource: IUserDataSyncResource, uri: URI): Promise; - getLocalSyncResourceHandles(resource: IUserDataSyncResource): Promise; - getRemoteSyncResourceHandles(resource: IUserDataSyncResource): Promise; - getAssociatedResources(resource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]>; - getMachineId(resource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise; + resolveUserDataSyncResource(syncResourceHandle: ISyncResourceHandle): IUserDataSyncResource | undefined; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts index ac16c67688e96..bdfaffc77bdf6 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts @@ -47,19 +47,16 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD return []; } - async resolveContent(profile: IUserDataProfile, resource: SyncResource, ref?: string): Promise { - if (!ref) { - const refs = await this.getAllRefs(profile, resource); - if (refs.length) { - ref = refs[refs.length - 1].ref; - } - } - if (ref) { - const file = joinPath(this.environmentService.userDataSyncHome, resource, ref); + async resolveContent(profile: IUserDataProfile, resourceKey: SyncResource, ref: string): Promise { + const folder = this.getResourceBackupHome(profile, resourceKey); + const file = joinPath(folder, ref); + try { const content = await this.fileService.readFile(file); return content.value.toString(); + } catch (error) { + this.logService.error(error); + return null; } - return null; } async backup(profile: IUserDataProfile, resourceKey: SyncResource, content: string): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts new file mode 100644 index 0000000000000..8daa8147b1134 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts @@ -0,0 +1,430 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtUri } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ISyncData, ISyncResourceHandle, IUserData, IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncStoreService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncResourceProviderService, ISyncUserDataProfile, CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { isSyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { parseSnippets } from 'vs/platform/userDataSync/common/snippetsSync'; +import { parseSettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; +import { getKeybindingsContentFromSyncContent } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { getTasksContentFromSyncContent } from 'vs/platform/userDataSync/common/tasksSync'; +import { LocalExtensionsProvider, parseExtensions, stringify as stringifyExtensions } from 'vs/platform/userDataSync/common/extensionsSync'; +import { LocalGlobalStateProvider, stringify as stringifyGlobalState } from 'vs/platform/userDataSync/common/globalStateSync'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { parseUserDataProfilesManifest, stringifyLocalProfiles } from 'vs/platform/userDataSync/common/userDataProfilesManifestSync'; +import { toFormattedString } from 'vs/base/common/jsonFormatter'; + +interface ISyncResourceUriInfo { + readonly remote: boolean; + readonly syncResource: SyncResource; + readonly profile: string; + readonly collection: string | undefined; + readonly ref: string | undefined; + readonly node: string | undefined; +} + +export class UserDataSyncResourceProviderService implements IUserDataSyncResourceProviderService { + + _serviceBrand: any; + + private static readonly NOT_EXISTING_RESOURCE = 'not-existing-resource'; + private static readonly REMOTE_BACKUP_AUTHORITY = 'remote-backup'; + private static readonly LOCAL_BACKUP_AUTHORITY = 'local-backup'; + + private readonly extUri: IExtUri; + + constructor( + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService private readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IStorageService private readonly storageService: IStorageService, + @IFileService private readonly fileService: IFileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + this.extUri = uriIdentityService.extUri; + } + + async getRemoteSyncedProfiles(): Promise { + const userData = await this.userDataSyncStoreService.readResource(SyncResource.Profiles, null, undefined); + if (userData.content) { + const syncData = this.parseSyncData(userData.content, SyncResource.Profiles); + return parseUserDataProfilesManifest(syncData); + } + return []; + } + + async getRemoteSyncResourceHandles(syncResource: SyncResource, profile: ISyncUserDataProfile | undefined): Promise { + const handles = await this.userDataSyncStoreService.getAllResourceRefs(syncResource, profile?.collection); + return handles.map(({ created, ref }) => ({ + created, + uri: this.toUri({ + remote: true, + syncResource, + profile: profile?.id ?? this.userDataProfilesService.defaultProfile.id, + collection: profile?.collection, + ref, + node: undefined, + }) + })); + } + + async getLocalSyncResourceHandles(syncResource: SyncResource, profile: IUserDataProfile): Promise { + const handles = await this.userDataSyncBackupStoreService.getAllRefs(profile, syncResource); + return handles.map(({ created, ref }) => ({ + created, + uri: this.toUri({ + remote: false, + syncResource, + profile: profile.id, + collection: undefined, + ref, + node: undefined, + }) + })); + } + + resolveUserDataSyncResource({ uri }: ISyncResourceHandle): IUserDataSyncResource | undefined { + const resolved = this.resolveUri(uri); + const profile = resolved ? this.userDataProfilesService.profiles.find(p => p.id === resolved.profile) : undefined; + return resolved && profile ? { profile, syncResource: resolved?.syncResource } : undefined; + } + + async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { + const resolved = this.resolveUri(uri); + if (!resolved) { + return []; + } + + const profile = this.userDataProfilesService.profiles.find(p => p.id === resolved.profile); + switch (resolved.syncResource) { + case SyncResource.Settings: return this.getSettingsAssociatedResources(uri, profile); + case SyncResource.Keybindings: return this.getKeybindingsAssociatedResources(uri, profile); + case SyncResource.Tasks: return this.getTasksAssociatedResources(uri, profile); + case SyncResource.Snippets: return this.getSnippetsAssociatedResources(uri, profile); + case SyncResource.GlobalState: return this.getGlobalStateAssociatedResources(uri, profile); + case SyncResource.Extensions: return this.getExtensionsAssociatedResources(uri, profile); + case SyncResource.Profiles: return this.getProfilesAssociatedResources(uri, profile); + } + } + + async getMachineId({ uri }: ISyncResourceHandle): Promise { + const resolved = this.resolveUri(uri); + if (!resolved) { + return undefined; + } + if (resolved.remote) { + if (resolved.ref) { + const { content } = await this.getUserData(resolved.syncResource, resolved.ref, resolved.collection); + if (content) { + const syncData = this.parseSyncData(content, resolved.syncResource); + return syncData?.machineId; + } + } + return undefined; + } + return getServiceMachineId(this.environmentService, this.fileService, this.storageService); + } + + async resolveContent(uri: URI): Promise { + const resolved = this.resolveUri(uri); + if (!resolved) { + return null; + } + + if (resolved.node === UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE) { + return null; + } + + if (resolved.ref) { + const content = await this.getContentFromStore(resolved.remote, resolved.syncResource, resolved.profile, resolved.collection, resolved.ref); + if (resolved.node && content) { + return this.resolveNodeContent(resolved.syncResource, content, resolved.node); + } + return content; + } + + if (!resolved.remote && !resolved.node) { + return this.resolveLatestContent(resolved.syncResource, resolved.profile); + } + + return null; + } + + private async getContentFromStore(remote: boolean, syncResource: SyncResource, profileId: string, collection: string | undefined, ref: string): Promise { + if (remote) { + const { content } = await this.getUserData(syncResource, ref, collection); + return content; + } + const profile = this.userDataProfilesService.profiles.find(p => p.id === profileId); + if (profile) { + return this.userDataSyncBackupStoreService.resolveContent(profile, syncResource, ref); + } + return null; + } + + private resolveNodeContent(syncResource: SyncResource, content: string, node: string): string | null { + const syncData = this.parseSyncData(content, syncResource); + switch (syncResource) { + case SyncResource.Settings: return this.resolveSettingsNodeContent(syncData, node); + case SyncResource.Keybindings: return this.resolveKeybindingsNodeContent(syncData, node); + case SyncResource.Tasks: return this.resolveTasksNodeContent(syncData, node); + case SyncResource.Snippets: return this.resolveSnippetsNodeContent(syncData, node); + case SyncResource.GlobalState: return this.resolveGlobalStateNodeContent(syncData, node); + case SyncResource.Extensions: return this.resolveExtensionsNodeContent(syncData, node); + case SyncResource.Profiles: return this.resolveProfileNodeContent(syncData, node); + } + } + + private async resolveLatestContent(syncResource: SyncResource, profileId: string): Promise { + const profile = this.userDataProfilesService.profiles.find(p => p.id === profileId); + if (!profile) { + return null; + } + switch (syncResource) { + case SyncResource.GlobalState: return this.resolveLatestGlobalStateContent(profile); + case SyncResource.Extensions: return this.resolveLatestExtensionsContent(profile); + case SyncResource.Profiles: return this.resolveLatestProfilesContent(profile); + case SyncResource.Settings: return null; + case SyncResource.Keybindings: return null; + case SyncResource.Tasks: return null; + case SyncResource.Snippets: return null; + } + } + + private getSettingsAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'settings.json'); + const comparableResource = profile ? profile.settingsResource : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + return [{ resource, comparableResource }]; + } + + private resolveSettingsNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'settings.json': + return parseSettingsSyncContent(syncData.content).settings; + } + return null; + } + + private getKeybindingsAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'keybindings.json'); + const comparableResource = profile ? profile.keybindingsResource : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + return [{ resource, comparableResource }]; + } + + private resolveKeybindingsNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'keybindings.json': + return getKeybindingsContentFromSyncContent(syncData.content, !!this.configurationService.getValue(CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM), this.logService); + } + return null; + } + + private getTasksAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'tasks.json'); + const comparableResource = profile ? profile.tasksResource : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + return [{ resource, comparableResource }]; + } + + private resolveTasksNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'tasks.json': + return getTasksContentFromSyncContent(syncData.content, this.logService); + } + return null; + } + + private async getSnippetsAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): Promise<{ resource: URI; comparableResource: URI }[]> { + const content = await this.resolveContent(uri); + if (content) { + const syncData = this.parseSyncData(content, SyncResource.Snippets); + if (syncData) { + const snippets = parseSnippets(syncData); + const result = []; + for (const snippet of Object.keys(snippets)) { + const resource = this.extUri.joinPath(uri, snippet); + const comparableResource = profile ? this.extUri.joinPath(profile.snippetsHome, snippet) : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + result.push({ resource, comparableResource }); + } + return result; + } + } + return []; + } + + private resolveSnippetsNodeContent(syncData: ISyncData, node: string): string | null { + return parseSnippets(syncData)[node] || null; + } + + private getExtensionsAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'extensions.json'); + const comparableResource = profile + ? this.toUri({ + remote: false, + syncResource: SyncResource.Extensions, + profile: profile.id, + collection: undefined, + ref: undefined, + node: undefined, + }) + : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + return [{ resource, comparableResource }]; + } + + private resolveExtensionsNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'extensions.json': + return stringifyExtensions(parseExtensions(syncData), true); + } + return null; + } + + private async resolveLatestExtensionsContent(profile: IUserDataProfile): Promise { + const { localExtensions } = await this.instantiationService.createInstance(LocalExtensionsProvider).getLocalExtensions(profile); + return stringifyExtensions(localExtensions, true); + } + + private getGlobalStateAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'globalState.json'); + const comparableResource = profile + ? this.toUri({ + remote: false, + syncResource: SyncResource.GlobalState, + profile: profile.id, + collection: undefined, + ref: undefined, + node: undefined, + }) + : this.extUri.joinPath(uri, UserDataSyncResourceProviderService.NOT_EXISTING_RESOURCE); + return [{ resource, comparableResource }]; + } + + private resolveGlobalStateNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'globalState.json': + return stringifyGlobalState(JSON.parse(syncData.content), true); + } + return null; + } + + private async resolveLatestGlobalStateContent(profile: IUserDataProfile): Promise { + const localGlobalState = await this.instantiationService.createInstance(LocalGlobalStateProvider).getLocalGlobalState(profile); + return stringifyGlobalState(localGlobalState, true); + } + + private getProfilesAssociatedResources(uri: URI, profile: IUserDataProfile | undefined): { resource: URI; comparableResource: URI }[] { + const resource = this.extUri.joinPath(uri, 'profiles.json'); + const comparableResource = this.toUri({ + remote: false, + syncResource: SyncResource.Profiles, + profile: this.userDataProfilesService.defaultProfile.id, + collection: undefined, + ref: undefined, + node: undefined, + }); + return [{ resource, comparableResource }]; + } + + private resolveProfileNodeContent(syncData: ISyncData, node: string): string | null { + switch (node) { + case 'profiles.json': + return toFormattedString(JSON.parse(syncData.content), {}); + } + return null; + } + + private async resolveLatestProfilesContent(profile: IUserDataProfile): Promise { + return stringifyLocalProfiles(this.userDataProfilesService.profiles.filter(p => !p.isDefault && !p.isTransient), true); + } + + private toUri(syncResourceUriInfo: ISyncResourceUriInfo): URI { + const authority = syncResourceUriInfo.remote ? UserDataSyncResourceProviderService.REMOTE_BACKUP_AUTHORITY : UserDataSyncResourceProviderService.LOCAL_BACKUP_AUTHORITY; + const paths = [ + syncResourceUriInfo.syncResource, + syncResourceUriInfo.profile, + ]; + if (syncResourceUriInfo.collection) { + paths.push(`collection:${syncResourceUriInfo.collection}`); + } + if (syncResourceUriInfo.ref) { + paths.push(`ref:${syncResourceUriInfo.ref}`); + } + if (syncResourceUriInfo.node) { + paths.push(syncResourceUriInfo.node); + } + return this.extUri.joinPath(URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority, path: `/` }), ...paths); + } + + private resolveUri(uri: URI): ISyncResourceUriInfo | undefined { + if (uri.scheme !== USER_DATA_SYNC_SCHEME) { + return undefined; + } + if (uri.authority !== UserDataSyncResourceProviderService.LOCAL_BACKUP_AUTHORITY && uri.authority !== UserDataSyncResourceProviderService.REMOTE_BACKUP_AUTHORITY) { + return undefined; + } + const paths: string[] = []; + while (uri.path !== '/') { + paths.unshift(this.extUri.basename(uri)); + uri = this.extUri.dirname(uri); + } + if (paths.length < 2) { + return undefined; + } + const remote = uri.authority === UserDataSyncResourceProviderService.REMOTE_BACKUP_AUTHORITY; + const syncResource = paths.shift()! as SyncResource; + const profile = paths.shift()!; + let collection: string | undefined; + let ref: string | undefined; + let node: string | undefined; + while (paths.length) { + const path = paths.shift()!; + if (path.startsWith('collection:')) { + collection = path.substring('collection:'.length); + } else if (path.startsWith('ref:')) { + ref = path.substring('ref:'.length); + } else { + node = path; + } + } + return { + remote, + syncResource, + profile, + collection, + ref, + node, + }; + } + + private parseSyncData(content: string, syncResource: SyncResource): ISyncData { + try { + const syncData: ISyncData = JSON.parse(content); + if (isSyncData(syncData)) { + return syncData; + } + } catch (error) { + this.logService.error(error); + } + throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, syncResource); + } + + private async getUserData(syncResource: SyncResource, ref: string, collection?: string): Promise { + const content = await this.userDataSyncStoreService.resolveResourceContent(syncResource, ref, collection); + return { ref, content }; + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index fbf1f1316816d..ab5261c5cc38e 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -10,7 +10,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; -import { isBoolean, isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; +import { isBoolean, isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -32,7 +32,7 @@ import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError, IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, ISyncUserDataProfile, IUserDataManifest, IUserDataResourceManifest, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, - MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE + MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSync'; type SyncErrorClassification = { @@ -93,6 +93,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IProductService private readonly productService: IProductService, @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncResourceProviderService private readonly userDataSyncResourceProviderService: IUserDataSyncResourceProviderService, ) { super(); this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; @@ -265,27 +266,37 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } async resolveContent(resource: URI): Promise { - for (const profile of this.userDataProfilesService.profiles) { - const result = await this.performAction(profile, async synchronizer => { + const content = await this.userDataSyncResourceProviderService.resolveContent(resource); + if (content) { + return content; + } + for (const profileSynchronizer of this.getActiveProfileSynchronizers()) { + for (const synchronizer of profileSynchronizer.enabled) { const content = await synchronizer.resolveContent(resource); if (content) { return content; } - return undefined; - }); - if (!isUndefinedOrNull(result)) { - return result; } } return null; } - async replace(profileSyncResource: IUserDataSyncResource, uri: URI): Promise { + async replace(syncResourceHandle: ISyncResourceHandle): Promise { this.checkEnablement(); + const profileSyncResource = this.userDataSyncResourceProviderService.resolveUserDataSyncResource(syncResourceHandle); + if (!profileSyncResource) { + return; + } + + const content = await this.resolveContent(syncResourceHandle.uri); + if (!content) { + return; + } + await this.performAction(profileSyncResource.profile, async synchronizer => { if (profileSyncResource.syncResource === synchronizer.resource) { - await synchronizer.replace(uri); + await synchronizer.replace(content); return true; } return undefined; @@ -309,45 +320,24 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ }); } - async getRemoteSyncResourceHandles(syncResource: IUserDataSyncResource): Promise { - const result = await this.performAction(syncResource.profile, async synchronizer => { - if (synchronizer.resource === syncResource.syncResource) { - return synchronizer.getRemoteSyncResourceHandles(); - } - return undefined; - }); - return result || []; + getRemoteProfiles(): Promise { + return this.userDataSyncResourceProviderService.getRemoteSyncedProfiles(); } - async getLocalSyncResourceHandles(syncResource: IUserDataSyncResource): Promise { - const result = await this.performAction(syncResource.profile, async synchronizer => { - if (synchronizer.resource === syncResource.syncResource) { - return synchronizer.getLocalSyncResourceHandles(); - } - return undefined; - }); - return result || []; + getRemoteSyncResourceHandles(syncResource: SyncResource, profile?: ISyncUserDataProfile): Promise { + return this.userDataSyncResourceProviderService.getRemoteSyncResourceHandles(syncResource, profile); } - async getAssociatedResources(syncResource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const result = await this.performAction(syncResource.profile, async synchronizer => { - if (synchronizer.resource === syncResource.syncResource) { - return synchronizer.getAssociatedResources(syncResourceHandle); - } - return undefined; - }); - return result || []; + async getLocalSyncResourceHandles(syncResource: SyncResource, profile?: IUserDataProfile): Promise { + return this.userDataSyncResourceProviderService.getLocalSyncResourceHandles(syncResource, profile ?? this.userDataProfilesService.defaultProfile); } - async getMachineId(syncResource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise { - const result = await this.performAction(syncResource.profile, async synchronizer => { - if (synchronizer.resource === syncResource.syncResource) { - const result = await synchronizer.getMachineId(syncResourceHandle); - return result || null; - } - return undefined; - }); - return result || undefined; + async getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { + return this.userDataSyncResourceProviderService.getAssociatedResources(syncResourceHandle); + } + + async getMachineId(syncResourceHandle: ISyncResourceHandle): Promise { + return this.userDataSyncResourceProviderService.getMachineId(syncResourceHandle); } async hasLocalData(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts index c6605c7ad7c67..edd7ba8159a29 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts @@ -9,10 +9,10 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { ILogService } from 'vs/platform/log/common/log'; -import { IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError, IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, IUserDataSyncService, - SyncResource, SyncStatus, UserDataSyncError + SyncResource, SyncStatus, UserDataSyncError, ISyncUserDataProfile } from 'vs/platform/userDataSync/common/userDataSync'; type ManualSyncTaskEvent = { manualSyncTaskId: string; data: T }; @@ -21,6 +21,10 @@ function reviewSyncResource(syncResource: IUserDataSyncResource, userDataProfile return { ...syncResource, profile: reviveProfile(syncResource.profile, userDataProfilesService.profilesHome.scheme) }; } +function reviewSyncResourceHandle(syncResourceHandle: ISyncResourceHandle): ISyncResourceHandle { + return { created: syncResourceHandle.created, uri: URI.revive(syncResourceHandle.uri) }; +} + export class UserDataSyncChannel implements IServerChannel { private readonly manualSyncTasks = new Map(); @@ -72,11 +76,12 @@ export class UserDataSyncChannel implements IServerChannel { case 'hasLocalData': return this.service.hasLocalData(); case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); case 'accept': return this.service.accept(reviewSyncResource(args[0], this.userDataProfilesService), URI.revive(args[1]), args[2], args[3]); - case 'replace': return this.service.replace(reviewSyncResource(args[0], this.userDataProfilesService), URI.revive(args[1])); - case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(reviewSyncResource(args[0], this.userDataProfilesService)); - case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(reviewSyncResource(args[0], this.userDataProfilesService)); - case 'getAssociatedResources': return this.service.getAssociatedResources(reviewSyncResource(args[0], this.userDataProfilesService), { created: args[1].created, uri: URI.revive(args[1].uri) }); - case 'getMachineId': return this.service.getMachineId(reviewSyncResource(args[0], this.userDataProfilesService), { created: args[1].created, uri: URI.revive(args[1].uri) }); + case 'getRemoteProfiles': return this.service.getRemoteProfiles(); + case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0], reviveProfile(args[1], this.userDataProfilesService.profilesHome.scheme)); + case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0], args[1]); + case 'replace': return this.service.replace(reviewSyncResourceHandle(args[0])); + case 'getAssociatedResources': return this.service.getAssociatedResources(reviewSyncResourceHandle(args[0])); + case 'getMachineId': return this.service.getMachineId(reviewSyncResourceHandle(args[0])); case 'createManualSyncTask': return this.createManualSyncTask(); } @@ -199,10 +204,6 @@ export class UserDataSyncChannelClient extends Disposable implements IUserDataSy return manualSyncTaskChannelClient; } - replace(profileSyncResource: IUserDataSyncResource, uri: URI): Promise { - return this.channel.call('replace', [profileSyncResource, uri]); - } - reset(): Promise { return this.channel.call('reset'); } @@ -231,23 +232,31 @@ export class UserDataSyncChannelClient extends Disposable implements IUserDataSy return this.channel.call('resolveContent', [resource]); } - async getLocalSyncResourceHandles(resource: IUserDataSyncResource): Promise { - const handles = await this.channel.call('getLocalSyncResourceHandles', [resource]); + getRemoteProfiles(): Promise { + return this.channel.call('getRemoteProfiles'); + } + + async getLocalSyncResourceHandles(syncResource: SyncResource, profile?: IUserDataProfile): Promise { + const handles = await this.channel.call('getLocalSyncResourceHandles', [syncResource, profile]); return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) })); } - async getRemoteSyncResourceHandles(resource: IUserDataSyncResource): Promise { - const handles = await this.channel.call('getRemoteSyncResourceHandles', [resource]); + async getRemoteSyncResourceHandles(syncResource: SyncResource, profile?: ISyncUserDataProfile): Promise { + const handles = await this.channel.call('getRemoteSyncResourceHandles', [syncResource, profile]); return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) })); } - async getAssociatedResources(resource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { - const result = await this.channel.call<{ resource: URI; comparableResource: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]); + async getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI }[]> { + const result = await this.channel.call<{ resource: URI; comparableResource: URI }[]>('getAssociatedResources', [syncResourceHandle]); return result.map(({ resource, comparableResource }) => ({ resource: URI.revive(resource), comparableResource: URI.revive(comparableResource) })); } - async getMachineId(resource: IUserDataSyncResource, syncResourceHandle: ISyncResourceHandle): Promise { - return this.channel.call('getMachineId', [resource, syncResourceHandle]); + getMachineId(syncResourceHandle: ISyncResourceHandle): Promise { + return this.channel.call('getMachineId', [syncResourceHandle]); + } + + replace(syncResourceHandle: ISyncResourceHandle): Promise { + return this.channel.call('replace', [syncResourceHandle]); } private async updateStatus(status: SyncStatus): Promise { diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index a034c18411591..3386bda9ba57e 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -169,7 +169,7 @@ class TestSynchroniser extends AbstractSynchroniser { } hasLocalData(): Promise { throw new Error('not implemented'); } - getAssociatedResources(): Promise<{ resource: URI; comparableResource: URI }[]> { throw new Error('not implemented'); } + async resolveContent(uri: URI): Promise { return null; } } suite('TestSynchronizer - Auto Sync', () => { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 6ba7ca6f86de2..5f85b242afc71 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -9,10 +9,10 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncEnablementService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, getLastSyncResourceUri, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncEnablementService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, getLastSyncResourceUri, SyncResource, ISyncUserDataProfile } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriDto } 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'; @@ -25,14 +25,13 @@ import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CO import { IUserDataSyncMachinesService, IUserDataSyncMachine, isWebPlatform } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { flatten } from 'vs/base/common/arrays'; import { basename } from 'vs/base/common/resources'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataSyncConflictsViewPane } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView'; export class UserDataSyncDataViews extends Disposable { @@ -236,14 +235,14 @@ export class UserDataSyncDataViews extends Disposable { async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { const dialogService = accessor.get(IDialogService); const userDataSyncService = accessor.get(IUserDataSyncService); - const { resource, syncResource } = <{ resource: string; syncResource: IUserDataSyncResource }>JSON.parse(handle.$treeItemHandle); + const { syncResourceHandle, syncResource } = <{ syncResourceHandle: UriDto; syncResource: SyncResource }>JSON.parse(handle.$treeItemHandle); const result = await dialogService.confirm({ - message: localize({ key: 'confirm replace', comment: ['A confirmation message to replace current user data (settings, extensions, keybindings, snippets) with selected version'] }, "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource.syncResource)), + message: localize({ key: 'confirm replace', comment: ['A confirmation message to replace current user data (settings, extensions, keybindings, snippets) with selected version'] }, "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource)), type: 'info', title: SYNC_TITLE }); if (result.confirmed) { - return userDataSyncService.replace(syncResource, URI.parse(resource)); + return userDataSyncService.replace({ created: syncResourceHandle.created, uri: URI.revive(syncResourceHandle.uri) }); } } }); @@ -280,8 +279,11 @@ export class UserDataSyncDataViews extends Disposable { } +type Profile = IUserDataProfile | ISyncUserDataProfile; + interface ISyncResourceHandle extends IResourceHandle { - syncResource: IUserDataSyncResource; + profileId?: string; + syncResource: SyncResource; previous?: IResourceHandle; } @@ -289,16 +291,20 @@ interface SyncResourceHandleTreeItem extends ITreeItem { syncResourceHandle: ISyncResourceHandle; } -abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProvider { +interface ProfileTreeItem extends ITreeItem { + profile: Profile; +} + +abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProvider { - private syncResourceHandlesPromise: Promise | undefined; + private readonly syncResourceHandlesByProfile = new Map>(); constructor( @IUserDataSyncService protected readonly userDataSyncService: IUserDataSyncService, @IUserDataAutoSyncService protected readonly userDataAutoSyncService: IUserDataAutoSyncService, @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, @INotificationService private readonly notificationService: INotificationService, - @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, ) { } async getChildren(element?: ITreeItem): Promise { @@ -306,6 +312,13 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv if (!element) { return await this.getRoots(); } + if ((element).profile || element.handle === this.userDataProfilesService.defaultProfile.id) { + let promise = this.syncResourceHandlesByProfile.get(element.handle); + if (!promise) { + this.syncResourceHandlesByProfile.set(element.handle, promise = this.getSyncResourceHandles((element).profile)); + } + return await promise; + } if ((element).syncResourceHandle) { return await this.getChildrenForSyncResourceTreeItem(element); } @@ -331,29 +344,41 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv } } - private async getRoots(): Promise { - this.syncResourceHandlesPromise = undefined; + private async getRoots(): Promise { + this.syncResourceHandlesByProfile.clear(); - const syncResourceHandles = await this.getSyncResourceHandles(); + const roots: ITreeItem[] = []; - return syncResourceHandles.map(syncResourceHandle => { - const handle = JSON.stringify({ resource: syncResourceHandle.uri.toString(), syncResource: syncResourceHandle.syncResource }); - return { - handle, + const profiles = await this.getProfiles(); + if (profiles.length) { + const profileTreeItem = { + handle: this.userDataProfilesService.defaultProfile.id, + label: { label: this.userDataProfilesService.defaultProfile.name }, + collapsibleState: TreeItemCollapsibleState.Expanded, + }; + roots.push(profileTreeItem); + } else { + const defaultSyncResourceHandles = await this.getSyncResourceHandles(); + roots.push(...defaultSyncResourceHandles); + } + + for (const profile of profiles) { + const profileTreeItem: ProfileTreeItem = { + handle: profile.id, + label: { label: profile.name }, collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: getSyncAreaLabel(syncResourceHandle.syncResource.syncResource) }, - description: fromNow(syncResourceHandle.created, true), - themeIcon: FolderThemeIcon, - syncResourceHandle, - contextValue: `sync-resource-${syncResourceHandle.syncResource.syncResource}` + profile, }; - }); + roots.push(profileTreeItem); + } + + return roots; } protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { const syncResourceHandle = (element).syncResourceHandle; - const associatedResources = await this.userDataSyncService.getAssociatedResources(syncResourceHandle.syncResource, syncResourceHandle); - const previousAssociatedResources = syncResourceHandle.previous ? await this.userDataSyncService.getAssociatedResources(syncResourceHandle.syncResource, syncResourceHandle.previous) : []; + const associatedResources = await this.userDataSyncService.getAssociatedResources(syncResourceHandle); + const previousAssociatedResources = syncResourceHandle.previous ? await this.userDataSyncService.getAssociatedResources(syncResourceHandle.previous) : []; return associatedResources.map(({ resource, comparableResource }) => { const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource.toString() }); const previousResource = previousAssociatedResources.find(previous => basename(previous.resource) === basename(resource))?.resource; @@ -380,29 +405,44 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv }); } - private getSyncResourceHandles(): Promise { - if (this.syncResourceHandlesPromise === undefined) { - this.syncResourceHandlesPromise = Promise.all(ALL_SYNC_RESOURCES.map(async syncResource => { - const profileSyncResource = { syncResource, profile: this.userDataProfilesService.defaultProfile }; - const resourceHandles = await this.getResourceHandles(profileSyncResource); - resourceHandles.sort((a, b) => b.created - a.created); - return resourceHandles.map((resourceHandle, index) => ({ ...resourceHandle, syncResource: profileSyncResource, previous: resourceHandles[index + 1] })); - })).then(result => flatten(result).sort((a, b) => b.created - a.created)); + private async getSyncResourceHandles(profile?: T): Promise { + const treeItems: SyncResourceHandleTreeItem[] = []; + const result = await Promise.all(ALL_SYNC_RESOURCES.map(async syncResource => { + const resourceHandles = await this.getResourceHandles(syncResource, profile); + return resourceHandles.map((resourceHandle, index) => ({ ...resourceHandle, syncResource, previous: resourceHandles[index + 1] })); + })); + const syncResourceHandles = result.flat().sort((a, b) => b.created - a.created); + for (const syncResourceHandle of syncResourceHandles) { + const handle = JSON.stringify({ syncResourceHandle, syncResource: syncResourceHandle.syncResource }); + treeItems.push({ + handle, + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: getSyncAreaLabel(syncResourceHandle.syncResource) }, + description: fromNow(syncResourceHandle.created, true), + themeIcon: FolderThemeIcon, + syncResourceHandle, + contextValue: `sync-resource-${syncResourceHandle.syncResource}` + }); } - return this.syncResourceHandlesPromise; + return treeItems; } - protected abstract getResourceHandles(syncResource: IUserDataSyncResource): Promise; + protected abstract getProfiles(): Promise; + protected abstract getResourceHandles(syncResource: SyncResource, profile?: T): Promise; } -class LocalUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { +class LocalUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { + + protected getResourceHandles(syncResource: SyncResource, profile: IUserDataProfile | undefined): Promise { + return this.userDataSyncService.getLocalSyncResourceHandles(syncResource, profile); + } - protected getResourceHandles(syncResource: IUserDataSyncResource): Promise { - return this.userDataSyncService.getLocalSyncResourceHandles(syncResource); + protected async getProfiles(): Promise { + return this.userDataProfilesService.profiles.filter(p => !p.isDefault); } } -class RemoteUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { +class RemoteUserDataSyncActivityViewDataProvider extends UserDataSyncActivityViewDataProvider { private machinesPromise: Promise | undefined; @@ -431,14 +471,18 @@ class RemoteUserDataSyncActivityViewDataProvider extends UserDataSyncActivityVie return this.machinesPromise; } - protected getResourceHandles(syncResource: IUserDataSyncResource): Promise { - return this.userDataSyncService.getRemoteSyncResourceHandles(syncResource); + protected getResourceHandles(syncResource: SyncResource, profile?: ISyncUserDataProfile): Promise { + return this.userDataSyncService.getRemoteSyncResourceHandles(syncResource, profile); + } + + protected getProfiles(): Promise { + return this.userDataSyncService.getRemoteProfiles(); } protected override async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { const children = await super.getChildrenForSyncResourceTreeItem(element); if (children.length) { - const machineId = await this.userDataSyncService.getMachineId(element.syncResourceHandle.syncResource, element.syncResourceHandle); + const machineId = await this.userDataSyncService.getMachineId(element.syncResourceHandle); if (machineId) { const machines = await this.getMachines(); const machine = machines.find(({ id }) => id === machineId); diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 23b6577a3ccf5..39abd11494106 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -75,7 +75,7 @@ import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/exte import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { LogLevel } from 'vs/platform/log/common/log'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService, IUserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -99,6 +99,7 @@ registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService, Ins registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService, InstantiationType.Delayed); registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService, InstantiationType.Delayed); registerSingleton(IUserDataSyncService, UserDataSyncService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncResourceProviderService, UserDataSyncResourceProviderService, InstantiationType.Delayed); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService, InstantiationType.Eager /* Eager to start auto sync */); registerSingleton(ITitleService, TitlebarPart, InstantiationType.Eager); registerSingleton(IExtensionTipsService, ExtensionTipsService, InstantiationType.Delayed); @@ -178,6 +179,7 @@ import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { UserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSyncResourceProvider'; export {