/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ParseError, parse } from 'vs/base/common/json'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isString } from 'vs/base/common/types'; import { uppercaseFirstLetter } from 'vs/base/common/strings'; import { equals } from 'vs/base/common/arrays'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders } from 'vs/base/parts/request/common/request'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; function isSyncData(thing: any): thing is ISyncData { if (thing && (thing.version !== undefined && typeof thing.version === 'number') && (thing.content !== undefined && typeof thing.content === 'string')) { // backward compatibility if (Object.keys(thing).length === 2) { return true; } if (Object.keys(thing).length === 3 && (thing.machineId !== undefined && typeof thing.machineId === 'string')) { return true; } } return false; } export interface IMergableResourcePreview extends IBaseResourcePreview { readonly remoteContent: string | null; readonly localContent: string | null; readonly previewContent: string | null; readonly acceptedContent: string | null; readonly hasConflicts: boolean; } export type IResourcePreview = Omit; export interface ISyncResourcePreview extends IBaseSyncResourcePreview { readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; readonly resourcePreviews: IMergableResourcePreview[]; } export abstract class AbstractSynchroniser extends Disposable { private syncPreviewPromise: CancelablePromise | null = null; protected readonly syncFolder: URI; protected readonly syncPreviewFolder: URI; private readonly currentMachineIdPromise: Promise; private _status: SyncStatus = SyncStatus.Idle; get status(): SyncStatus { return this._status; } private _onDidChangStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; private _conflicts: IMergableResourcePreview[] = []; get conflicts(): IMergableResourcePreview[] { return this._conflicts; } private _onDidChangeConflicts: Emitter = this._register(new Emitter()); readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50); private readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; protected readonly lastSyncResource: URI; protected readonly syncResourceLogLabel: string; private syncHeaders: IHeaders = {}; constructor( readonly resource: SyncResource, @IFileService protected readonly fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IStorageService storageService: IStorageService, @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService protected readonly telemetryService: ITelemetryService, @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, @IConfigurationService protected readonly configurationService: IConfigurationService, ) { super(); this.syncResourceLogLabel = uppercaseFirstLetter(this.resource); this.syncFolder = joinPath(environmentService.userDataSyncHome, resource); this.syncPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`); this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); } protected isEnabled(): boolean { return this.userDataSyncResourceEnablementService.isResourceEnabled(this.resource); } protected async triggerLocalChange(): Promise { if (this.isEnabled()) { this.localChangeTriggerScheduler.schedule(); } } protected async doTriggerLocalChange(): Promise { // Sync again if current status is in conflicts if (this.status === SyncStatus.HasConflicts) { this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`); const preview = await this.syncPreviewPromise!; this.syncPreviewPromise = null; const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true); this.setStatus(status); } // Check if local change causes remote change else { this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`); const lastSyncUserData = await this.getLastSyncUserData(); const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true; if (hasRemoteChanged) { this._onDidChangeLocal.fire(); } } } protected setStatus(status: SyncStatus): void { if (this._status !== status) { const oldStatus = this._status; if (status === SyncStatus.HasConflicts) { // Log to telemetry when there is a sync conflict this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource }); } if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) { // Log to telemetry when conflicts are resolved this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource }); } this._status = status; this._onDidChangStatus.fire(status); } } async pull(): Promise { if (!this.isEnabled()) { this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`); return; } await this.stop(); try { this.logService.info(`${this.syncResourceLogLabel}: Started pulling ${this.syncResourceLogLabel.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None); await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); } } async push(): Promise { if (!this.isEnabled()) { this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`); return; } this.stop(); try { this.logService.info(`${this.syncResourceLogLabel}: Started pushing ${this.syncResourceLogLabel.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None); await this.applyPreview(remoteUserData, lastSyncUserData, preview, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); } } async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise { await this._sync(manifest, true, headers); } async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise { return this._sync(manifest, false, headers); } async apply(force: boolean, headers: IHeaders = {}): Promise { try { this.syncHeaders = { ...headers }; const status = await this.doApply(force); this.setStatus(status); return this.syncPreviewPromise; } finally { this.syncHeaders = {}; } } private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise { try { this.syncHeaders = { ...headers }; if (!this.isEnabled()) { if (this.status !== SyncStatus.Idle) { await this.stop(); } this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`); return null; } if (this.status === SyncStatus.HasConflicts) { this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`); return this.syncPreviewPromise; } if (this.status === SyncStatus.Syncing) { this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`); return this.syncPreviewPromise; } this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); let status: SyncStatus = SyncStatus.Idle; try { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); status = await this.performSync(remoteUserData, lastSyncUserData, apply); if (status === SyncStatus.HasConflicts) { this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`); } else if (status === SyncStatus.Idle) { this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`); } return this.syncPreviewPromise || null; } finally { this.setStatus(status); } } finally { this.syncHeaders = {}; } } async replace(uri: URI): Promise { const content = await this.resolveContent(uri); if (!content) { return false; } const syncData = this.parseSyncData(content); if (!syncData) { return false; } await this.stop(); try { this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData); await this.applyPreview(remoteUserData, lastSyncUserData, preview, false); this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`); } finally { this.setStatus(SyncStatus.Idle); } return true; } protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { if (lastSyncUserData) { const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined; // Last time synced resource and latest resource on server are same if (lastSyncUserData.ref === latestRef) { return lastSyncUserData; } // There is no resource on server and last time it was synced with no resource if (latestRef === undefined && lastSyncUserData.syncData === null) { return lastSyncUserData; } } return this.getRemoteUserData(lastSyncUserData); } private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource }); throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource); } try { return await this.doSync(remoteUserData, lastSyncUserData, apply); } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`); return this.performSync(remoteUserData, lastSyncUserData, apply); case UserDataSyncErrorCode.PreconditionFailed: // Rejected as there is a new remote version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); // Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624 remoteUserData = await this.getRemoteUserData(null); // Get the latest last sync user data. Because multiples parallel syncs (in Web) could share same last sync data // and one of them successfully updated remote and last sync state. lastSyncUserData = await this.getLastSyncUserData(); return this.performSync(remoteUserData, lastSyncUserData, apply); } } throw e; } } protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise { try { // generate or use existing preview if (!this.syncPreviewPromise) { this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token)); } const preview = await this.syncPreviewPromise; this.updateConflicts(preview.resourcePreviews); if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) { return SyncStatus.HasConflicts; } if (apply) { return await this.doApply(false); } return SyncStatus.Syncing; } catch (error) { // reset preview on error this.syncPreviewPromise = null; throw error; } } async accept(resource: URI, content: string | null): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resource, content); return { ...updatedResourcePreview, mergeState: MergeState.Accepted }; }); return this.syncPreviewPromise; } async merge(resource: URI): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent); return { ...updatedResourcePreview, mergeState: resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted }; }); return this.syncPreviewPromise; } async discard(resource: URI): Promise { await this.updateSyncResourcePreview(resource, async (resourcePreview) => { await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || '')); const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent); return { ...updatedResourcePreview, mergeState: MergeState.Preview }; }); return this.syncPreviewPromise; } private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IMergableResourcePreview) => Promise): Promise { if (!this.syncPreviewPromise) { return; } let preview = await this.syncPreviewPromise; const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource)); if (index === -1) { return; } this.syncPreviewPromise = createCancelablePromise(async token => { const resourcePreviews = [...preview.resourcePreviews]; resourcePreviews[index] = await updateResourcePreview(resourcePreviews[index]); return { ...preview, resourcePreviews }; }); preview = await this.syncPreviewPromise; this.updateConflicts(preview.resourcePreviews); if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) { this.setStatus(SyncStatus.HasConflicts); } else { this.setStatus(SyncStatus.Syncing); } } protected async updateResourcePreview(resourcePreview: IResourcePreview, resource: URI, acceptedContent: string | null): Promise { return { ...resourcePreview, acceptedContent }; } private async doApply(force: boolean): Promise { if (!this.syncPreviewPromise) { return SyncStatus.Idle; } const preview = await this.syncPreviewPromise; // check for conflicts if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) { return SyncStatus.HasConflicts; } // check if all are accepted if (preview.resourcePreviews.some(({ mergeState }) => mergeState !== MergeState.Accepted)) { return SyncStatus.Syncing; } // apply preview await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force); // reset preview this.syncPreviewPromise = null; // reset preview folder await this.clearPreviewFolder(); return SyncStatus.Idle; } private async clearPreviewFolder(): Promise { try { await this.fileService.del(this.syncPreviewFolder, { recursive: true }); } catch (error) { /* Ignore */ } } private updateConflicts(previews: IMergableResourcePreview[]): void { const conflicts = previews.filter(p => p.mergeState === MergeState.Conflict); if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) { this._conflicts = conflicts; this._onDidChangeConflicts.fire(conflicts); } } async hasPreviouslySynced(): Promise { const lastSyncData = await this.getLastSyncUserData(); return !!lastSyncData; } async getRemoteSyncResourceHandles(): Promise { const handles = await this.userDataSyncStoreService.getAllRefs(this.resource); return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); } async getLocalSyncResourceHandles(): Promise { const handles = await this.userDataSyncBackupStoreService.getAllRefs(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.resource}/${ref}` }); } private toLocalBackupResource(ref: string): URI { return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` }); } async getMachineId({ uri }: ISyncResourceHandle): Promise { const ref = basename(uri); if (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 = basename(uri); if (isEqual(uri, this.toRemoteBackupResource(ref))) { const { content } = await this.getUserData(ref); return content; } if (isEqual(uri, this.toLocalBackupResource(ref))) { return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref); } return null; } protected async resolvePreviewContent(uri: URI): Promise { const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null; if (syncPreview) { for (const resourcePreview of syncPreview.resourcePreviews) { if (isEqual(resourcePreview.acceptedResource, uri)) { return resourcePreview.acceptedContent; } if (isEqual(resourcePreview.remoteResource, uri)) { return resourcePreview.remoteContent; } if (isEqual(resourcePreview.localResource, uri)) { return resourcePreview.localContent; } } } return null; } async resetLocal(): Promise { try { await this.fileService.del(this.lastSyncResource); } catch (e) { /* ignore */ } } private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise { const machineId = await this.currentMachineIdPromise; const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; // For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData; const result = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token); const resourcePreviews: IMergableResourcePreview[] = []; for (const resourcePreview of result) { if (token.isCancellationRequested) { break; } if (!apply) { await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || '')); } resourcePreviews.push({ ...resourcePreview, mergeState: resourcePreview.localChange === Change.None && resourcePreview.remoteChange === Change.None ? MergeState.Accepted /* Mark previews with no changes as merged */ : apply ? (resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted) : MergeState.Preview }); } return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine }; } async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); const parsed = JSON.parse(content.value.toString()); const userData: IUserData = parsed as IUserData; if (userData.content === null) { return { ref: parsed.ref, syncData: null } as T; } const syncData: ISyncData = JSON.parse(userData.content); /* Check if syncData is of expected type. Return only if matches */ if (isSyncData(syncData)) { return { ...parsed, ...{ syncData, content: undefined } }; } } catch (error) { if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { // log error always except when file does not exist this.logService.error(error); } } return null; } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { const { ref, content } = await this.getUserData(lastSyncData); let syncData: ISyncData | null = null; if (content !== null) { syncData = this.parseSyncData(content); } return { ref, syncData }; } protected parseSyncData(content: string): 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 current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource); } private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { if (isString(refOrLastSyncData)) { const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData); 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.read(this.resource, lastSyncUserData, this.syncHeaders); } } protected async updateRemoteUserData(content: string, ref: string | null): Promise { const machineId = await this.currentMachineIdPromise; const syncData: ISyncData = { version: this.version, machineId, content }; ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, this.syncHeaders); return { ref, syncData }; } protected async backupLocal(content: string): Promise { const syncData: ISyncData = { version: this.version, content }; return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData)); } async stop(): Promise { if (this.status === SyncStatus.Idle) { return; } this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`); if (this.syncPreviewPromise) { this.syncPreviewPromise.cancel(); this.syncPreviewPromise = null; } this.updateConflicts([]); await this.clearPreviewFolder(); this.setStatus(SyncStatus.Idle); this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); } protected abstract readonly version: number; protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise; protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise; } export interface IFileResourcePreview extends IResourcePreview { readonly fileContent: IFileContent | null; } export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { constructor( protected readonly file: URI, resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IConfigurationService configurationService: IConfigurationService, ) { super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(file))); this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } protected async getLocalFileContent(): Promise { try { return await this.fileService.readFile(this.file); } catch (error) { return null; } } protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise { try { if (oldContent) { // file exists already await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent); } else { // file does not exist await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force }); } } catch (e) { if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) || (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) { throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed); } else { throw e; } } } private onFileChanges(e: FileChangesEvent): void { if (!e.contains(this.file)) { return; } this.triggerLocalChange(); } } export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser { constructor( file: URI, resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService configurationService: IConfigurationService, ) { super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService); } protected hasErrors(content: string): boolean { const parseErrors: ParseError[] = []; parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true }); return parseErrors.length > 0; } private _formattingOptions: Promise | undefined = undefined; protected getFormattingOptions(): Promise { if (!this._formattingOptions) { this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file); } return this._formattingOptions; } }