diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 79e8473bcc679c33460b77908e01c58ad8944098..0e6c2358199c6a4db3861db6a2a15595ac211183 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -52,7 +52,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel, UserDataSyncBackupStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -225,6 +225,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const userDataSyncStoreServiceChannel = new UserDataSyncStoreServiceChannel(userDataSyncStoreService); server.registerChannel('userDataSyncStoreService', userDataSyncStoreServiceChannel); + const userDataSyncBackupStoreService = accessor.get(IUserDataSyncBackupStoreService); + const userDataSyncBackupStoreServiceChannel = new UserDataSyncBackupStoreServiceChannel(userDataSyncBackupStoreService); + server.registerChannel('userDataSyncBackupStoreService', userDataSyncBackupStoreServiceChannel); + const settingsSyncService = accessor.get(ISettingsSyncService); const settingsSyncChannel = new SettingsSyncChannel(settingsSyncService); server.registerChannel('settingsSync', settingsSyncChannel); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 5d8e7648c1d3547c8dc2378e66e0d55a77fe60fb..8f79415fe07a69849d2c9299641fdd7c8fdd273b 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -162,6 +162,10 @@ export abstract class AbstractSynchroniser extends Disposable { return content; } + async getLocalBackupContent(ref?: string): Promise { + return this.userDataSyncBackupStoreService.resolveContent(this.resourceKey, ref); + } + async resetLocal(): Promise { try { await this.fileService.del(this.lastSyncResource); diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 4a108b0814e26e6d803a5bd70082b064b15b793f..cdc3d6faab5aae49b74b4dbebda2efe2f356acc1 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -123,18 +123,30 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async getRemoteContent(ref?: string, fragment?: string): Promise { const content = await super.getRemoteContent(ref); if (content !== null && fragment) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'extensions': - return syncData.content; - } - } - return null; + return this.getFragment(content, fragment); } return content; } + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'extensions': + return syncData.content; + } + } + return null; + } + accept(content: string): Promise { throw new Error('Extensions: Conflicts should not occur'); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index d125cd05e4e5a26e18a94391e753f80d77f5160b..a095a09b482f82682089c19519735d01782568ff 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -107,18 +107,30 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async getRemoteContent(ref?: string, fragment?: string): Promise { let content = await super.getRemoteContent(ref); if (content !== null && fragment) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'globalState': - return syncData.content; - } - } - return null; + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); } return content; } + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'globalState': + return syncData.content; + } + } + return null; + } + accept(content: string): Promise { throw new Error('UI State: Conflicts should not occur'); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 33f5eb92dd7cd98ae99056ef72b01f3a9633a59b..c3f02bac0dada7f42b8cc9a322844e23aa1a026d 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -164,18 +164,30 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem async getRemoteContent(ref?: string, fragment?: string): Promise { const content = await super.getRemoteContent(ref); if (content !== null && fragment) { - const syncData = this.parseSyncData(content); - if (syncData) { - switch (fragment) { - case 'keybindings': - return this.getKeybindingsContentFromSyncContent(syncData.content); - } - } - return null; + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); } return content; } + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'keybindings': + return this.getKeybindingsContentFromSyncContent(syncData.content); + } + } + return null; + } + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 79d0b2494e8d697bcc6ceb0dd578f9738c15b445..219f2c2a698065d69203a5568ee78bd57392f796 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -205,19 +205,31 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement async getRemoteContent(ref?: string, fragment?: string): Promise { let content = await super.getRemoteContent(ref); if (content !== null && fragment) { - const syncData = this.parseSyncData(content); - if (syncData) { - const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); - if (settingsSyncContent) { - switch (fragment) { - case 'settings': - return settingsSyncContent.settings; - } + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (settingsSyncContent) { + switch (fragment) { + case 'settings': + return settingsSyncContent.settings; } } - return null; } - return content; + return null; } async accept(content: string): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 2518c3e365b33852a0b155ac5517e0fcd421d2ec..0744514d2f5191f77487dc0c3c40e114e7c157e1 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -165,6 +165,8 @@ export const IUserDataSyncBackupStoreService = createDecorator; + getAllRefs(key: ResourceKey): Promise; + resolveContent(key: ResourceKey, ref?: string): Promise; } //#endregion @@ -261,6 +263,7 @@ export interface IUserDataSynchroniser { getRemoteContentFromPreview(): Promise; getRemoteContent(ref?: string, fragment?: string): Promise; + getLocalBackupContent(ref?: string, fragment?: string): Promise; accept(content: string): Promise; } @@ -354,6 +357,9 @@ export function toRemoteSyncResourceFromSource(source: SyncSource, ref?: string) export function toRemoteSyncResource(resourceKey: ResourceKey, ref?: string): URI { return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); } +export function toLocalBackupSyncResource(resourceKey: ResourceKey, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); +} export function resolveSyncResource(resource: URI): { remote: boolean, resourceKey: ResourceKey, ref?: string } | null { const remote = resource.authority === 'remote'; diff --git a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts index 8875ec93336cc5f03e45b9d908cf7401ee50dcf8..b439de0bc0242930c66eb4651bd37e1b5c0cb273 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, ResourceKey, ALL_RESOURCE_KEYS, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, ResourceKey, ALL_RESOURCE_KEYS, IUserDataSyncBackupStoreService, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { joinPath } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { toLocalISOString } from 'vs/base/common/date'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -26,6 +26,34 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD ALL_RESOURCE_KEYS.forEach(resourceKey => this.cleanUpBackup(resourceKey)); } + async getAllRefs(resourceKey: ResourceKey): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + const stat = await this.fileService.resolve(folder); + if (stat.children) { + const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort().reverse(); + return all.map(stat => ({ + ref: stat.name, + created: this.getCreationTime(stat) + })); + } + return []; + } + + async resolveContent(resourceKey: ResourceKey, ref?: string): Promise { + if (!ref) { + const refs = await this.getAllRefs(resourceKey); + if (refs.length) { + ref = refs[refs.length - 1].ref; + } + } + if (ref) { + const file = joinPath(this.environmentService.userDataSyncHome, resourceKey, ref); + const content = await this.fileService.readFile(file); + return content.value.toString(); + } + return null; + } + async backup(resourceKey: ResourceKey, content: string): Promise { const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); const resource = joinPath(folder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); @@ -53,17 +81,7 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD if (stat.children) { const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort(); const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue('sync.localBackupDuration') || 30 /* Default 30 days */); - let toDelete = all.filter(stat => { - const ctime = stat.ctime || new Date( - parseInt(stat.name.substring(0, 4)), - parseInt(stat.name.substring(4, 6)) - 1, - parseInt(stat.name.substring(6, 8)), - parseInt(stat.name.substring(9, 11)), - parseInt(stat.name.substring(11, 13)), - parseInt(stat.name.substring(13, 15)) - ).getTime(); - return Date.now() - ctime > backUpMaxAge; - }); + let toDelete = all.filter(stat => Date.now() - this.getCreationTime(stat) > backUpMaxAge); const remaining = all.length - toDelete.length; if (remaining < 10) { toDelete = toDelete.slice(10 - remaining); @@ -77,4 +95,15 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD this.logService.error(e); } } + + private getCreationTime(stat: IFileStat) { + return stat.ctime || new Date( + parseInt(stat.name.substring(0, 4)), + parseInt(stat.name.substring(4, 6)) - 1, + parseInt(stat.name.substring(6, 8)), + parseInt(stat.name.substring(9, 11)), + parseInt(stat.name.substring(11, 13)), + parseInt(stat.name.substring(13, 15)) + ).getTime(); + } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 08e0a4c9aa363b6a4c5aa39973efae3365513954..de2736a2fe39e3b3e04877e809a037eb009f3a05 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncStoreService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -69,6 +69,7 @@ export class SettingsSyncChannel implements IServerChannel { case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]); case 'getRemoteContentFromPreview': return this.service.getRemoteContentFromPreview(); case 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]); + case 'getLocalBackupContent': return this.service.getLocalBackupContent(args[0], args[1]); } throw new Error('Invalid call'); } @@ -149,3 +150,20 @@ export class UserDataSyncStoreServiceChannel implements IServerChannel { throw new Error('Invalid call'); } } + +export class UserDataSyncBackupStoreServiceChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncBackupStoreService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'getAllRefs': return this.service.getAllRefs(args[0]); + case 'resolveContent': return this.service.resolveContent(args[0], args[1]); + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 5c7f3950cfe1c23757e0b6514df405380a2cd3f2..4be50d8387aa19bfa3ca4b8d23ecedfcb61ce6f1 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -185,7 +185,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (PREVIEW_QUERY === resource.query) { return result.remote ? synchronizer.getRemoteContentFromPreview() : null; } - return result.remote ? synchronizer.getRemoteContent(result.ref, resource.fragment) : null; + return result.remote ? synchronizer.getRemoteContent(result.ref, resource.fragment) : synchronizer.getLocalBackupContent(result.ref, resource.fragment); } } return null; diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts index 8fe78318d4cb0df533302cd93c952447ed96abf1..7d48e03e84ca78c4f2925f879028a5bc2d747a40 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts @@ -88,6 +88,10 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ return this.channel.call('getRemoteContent', [ref, fragment]); } + getLocalBackupContent(ref?: string, fragment?: string): Promise { + return this.channel.call('getLocalBackupContent', [ref, fragment]); + } + getRemoteContentFromPreview(): Promise { return this.channel.call('getRemoteContentFromPreview', []); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a395b1c5b4cb27a16b67940fecc5caac7fc5b0a --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceKey, IResourceRefHandle, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserDataSyncBackupStoreService implements IUserDataSyncBackupStoreService { + + _serviceBrand: undefined; + private readonly channel: IChannel; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + this.channel = sharedProcessService.getChannel('userDataSyncBackupStoreService'); + } + + backup(key: ResourceKey, content: string): Promise { + return this.channel.call('backup', [key, content]); + } + + + getAllRefs(key: ResourceKey): Promise { + return this.channel.call('getAllRefs', [key]); + } + + resolveContent(key: ResourceKey, ref: string): Promise { + return this.channel.call('resolveContent', [key, ref]); + } + +} + +registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 33ba47dadd6910cb957d8c20a3bad3eb8f336a04..a8f926566d42eb2c287b6acc6a37c47dbdc0891b 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -53,6 +53,7 @@ import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService' import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService';