mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 1b314ab317fbff7d799b21754326b7d849889ceb
This commit is contained in:
@@ -9,7 +9,8 @@ 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, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
|
||||
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
@@ -52,9 +53,20 @@ function isSyncData(thing: any): thing is ISyncData {
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface IMergableResourcePreview extends IBaseResourcePreview {
|
||||
readonly remoteContent: string | null;
|
||||
readonly localContent: string | null;
|
||||
readonly previewContent: string | null;
|
||||
readonly hasConflicts: boolean;
|
||||
merged: boolean;
|
||||
}
|
||||
|
||||
export type IResourcePreview = Omit<IMergableResourcePreview, 'merged'>;
|
||||
|
||||
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly resourcePreviews: IMergableResourcePreview[];
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
@@ -70,10 +82,10 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _conflicts: Conflict[] = [];
|
||||
get conflicts(): Conflict[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
|
||||
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.event;
|
||||
private _conflicts: IMergableResourcePreview[] = [];
|
||||
get conflicts(): IMergableResourcePreview[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IMergableResourcePreview[]> = this._register(new Emitter<IMergableResourcePreview[]>());
|
||||
readonly onDidChangeConflicts: Event<IMergableResourcePreview[]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
|
||||
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
@@ -119,7 +131,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
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);
|
||||
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true);
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
@@ -127,7 +139,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
|
||||
if (hasRemoteChanged) {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
@@ -137,8 +149,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(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 });
|
||||
@@ -147,16 +157,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
|
||||
}
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
this.setConflicts([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setConflicts(conflicts: Conflict[]) {
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.local, b.local) && isEqual(a.remote, b.remote))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(this._conflicts);
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -199,7 +201,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(preview, true);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
|
||||
} finally {
|
||||
@@ -208,38 +210,49 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
|
||||
await this._sync(manifest, true, headers);
|
||||
}
|
||||
|
||||
async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
return this._sync(manifest, false, headers);
|
||||
}
|
||||
|
||||
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
|
||||
return;
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
|
||||
return;
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
|
||||
let status: SyncStatus = SyncStatus.Idle;
|
||||
try {
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData);
|
||||
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);
|
||||
}
|
||||
@@ -267,7 +280,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
|
||||
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
|
||||
await this.applyPreview(preview, false);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -276,7 +289,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
if (lastSyncUserData) {
|
||||
|
||||
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
|
||||
@@ -294,24 +307,15 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return this.getRemoteUserData(lastSyncUserData);
|
||||
}
|
||||
|
||||
async generateSyncPreview(): Promise<ISyncResourcePreview | null> {
|
||||
if (this.isEnabled()) {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
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.Incompatible, 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);
|
||||
return await this.doSync(remoteUserData, lastSyncUserData, apply);
|
||||
} catch (e) {
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
@@ -319,7 +323,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
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);
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
|
||||
case UserDataSyncErrorCode.PreconditionFailed:
|
||||
// Rejected as there is a new remote version. Syncing again...
|
||||
@@ -332,32 +336,28 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
// and one of them successfully updated remote and last sync state.
|
||||
lastSyncUserData = await this.getLastSyncUserData();
|
||||
|
||||
return this.performSync(remoteUserData, lastSyncUserData);
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
try {
|
||||
// generate or use existing preview
|
||||
if (!this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
if (preview.hasConflicts) {
|
||||
return SyncStatus.HasConflicts;
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
if (apply) {
|
||||
const preview = await this.syncPreviewPromise;
|
||||
const newConflicts = preview.resourcePreviews.filter(({ hasConflicts }) => hasConflicts);
|
||||
return await this.updateConflictsAndApply(newConflicts, false);
|
||||
} else {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
return SyncStatus.Idle;
|
||||
} catch (error) {
|
||||
|
||||
// reset preview on error
|
||||
@@ -367,32 +367,128 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getSyncPreviewInProgress(): Promise<ISyncResourcePreview | null> {
|
||||
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
|
||||
async acceptPreviewContent(resource: URI, content: string, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
const preview = await this.syncPreviewPromise;
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.updateSyncResourcePreviewContent(preview, resource, content, token));
|
||||
return this.merge(resource, force, headers);
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
|
||||
let preview = await this.getSyncPreviewInProgress();
|
||||
|
||||
if (!preview || !preview.hasConflicts) {
|
||||
return;
|
||||
async merge(resource: URI, force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
const preview = await this.syncPreviewPromise;
|
||||
const resourcePreview = preview.resourcePreviews.find(({ localResource, remoteResource, previewResource }) =>
|
||||
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (!resourcePreview) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
|
||||
preview = await this.syncPreviewPromise;
|
||||
/* mark merged */
|
||||
resourcePreview.merged = true;
|
||||
|
||||
if (!preview.hasConflicts) {
|
||||
/* Add or remove the preview from conflicts */
|
||||
const newConflicts = [...this._conflicts];
|
||||
const index = newConflicts.findIndex(({ localResource, remoteResource, previewResource }) =>
|
||||
isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (resourcePreview.hasConflicts) {
|
||||
if (newConflicts.indexOf(resourcePreview) === -1) {
|
||||
newConflicts.push(resourcePreview);
|
||||
}
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
newConflicts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview, false);
|
||||
const status = await this.updateConflictsAndApply(newConflicts, force);
|
||||
this.setStatus(status);
|
||||
return this.syncPreviewPromise;
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
private async updateConflictsAndApply(conflicts: IMergableResourcePreview[], force: boolean): Promise<SyncStatus> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
// update conflicts
|
||||
this.updateConflicts(conflicts);
|
||||
if (this._conflicts.length) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
|
||||
// check if all are merged
|
||||
if (preview.resourcePreviews.some(r => !r.merged)) {
|
||||
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 updateSyncResourcePreviewContent(preview: ISyncResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource, localChange, remoteChange }) =>
|
||||
(localChange !== Change.None || remoteChange !== Change.None)
|
||||
&& (isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource)));
|
||||
if (index !== -1) {
|
||||
const resourcePreviews = [...preview.resourcePreviews];
|
||||
const resourcePreview = await this.updateResourcePreviewContent(resourcePreviews[index], resource, previewContent, token);
|
||||
resourcePreviews[index] = { ...resourcePreview, merged: resourcePreviews[index].merged };
|
||||
preview = {
|
||||
...preview,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
protected async updateResourcePreviewContent(resourcePreview: IResourcePreview, resource: URI, previewContent: string, token: CancellationToken): Promise<IResourcePreview> {
|
||||
return {
|
||||
...resourcePreview,
|
||||
previewContent,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async clearPreviewFolder(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.syncPreviewFolder, { recursive: true });
|
||||
} catch (error) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private updateConflicts(conflicts: IMergableResourcePreview[]): void {
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
@@ -400,11 +496,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise<boolean> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
}
|
||||
|
||||
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
|
||||
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
|
||||
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
|
||||
@@ -447,12 +538,47 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(uri: URI): Promise<string | null> {
|
||||
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
|
||||
if (syncPreview) {
|
||||
for (const resourcePreview of syncPreview.resourcePreviews) {
|
||||
if (resourcePreview.previewResource && isEqual(resourcePreview.previewResource, uri)) {
|
||||
return resourcePreview.previewContent || '';
|
||||
}
|
||||
if (resourcePreview.remoteResource && isEqual(resourcePreview.remoteResource, uri)) {
|
||||
return resourcePreview.remoteContent || '';
|
||||
}
|
||||
if (resourcePreview.localResource && isEqual(resourcePreview.localResource, uri)) {
|
||||
return resourcePreview.localContent || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, merge: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
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 resourcePreviews = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
|
||||
// Mark merge
|
||||
const mergableResourcePreviews = resourcePreviews.map(r => ({
|
||||
...r,
|
||||
merged: merge || (r.localChange === Change.None && r.remoteChange === Change.None) /* Mark previews with no changes as merged */
|
||||
}));
|
||||
|
||||
return { remoteUserData, lastSyncUserData, resourcePreviews: mergableResourcePreviews, isLastSyncFromCurrentMachine };
|
||||
}
|
||||
|
||||
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncResource);
|
||||
@@ -500,7 +626,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
} 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.Incompatible, this.resource);
|
||||
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<IUserData> {
|
||||
@@ -526,26 +652,33 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
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<ISyncResourcePreview>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise<void>;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]>;
|
||||
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreview extends ISyncResourcePreview {
|
||||
export interface IFileResourcePreview extends IResourcePreview {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly content: string | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
@@ -568,28 +701,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await super.stop();
|
||||
try {
|
||||
await this.fileService.del(this.localPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource)) {
|
||||
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
|
||||
}
|
||||
if (isEqual(this.localPreviewResource, conflictResource)) {
|
||||
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.file);
|
||||
@@ -598,14 +709,14 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise<void> {
|
||||
try {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
|
||||
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: false });
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force });
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
|
||||
@@ -624,8 +735,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected abstract readonly localPreviewResource: URI;
|
||||
protected abstract readonly remotePreviewResource: URI;
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
||||
Reference in New Issue
Block a user