* fix #162380
- Handle 405 error
- Clean up remote data when 405 occurs
- Check for too many profiles locally
- fix merging profiles
- remove created collections when updaing profiles fail

* fix tests
This commit is contained in:
Sandeep Somavarapu 2022-12-06 08:15:12 +01:00 committed by GitHub
parent 4b148a70a8
commit d253ab2adc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 285 additions and 140 deletions

View file

@ -20,7 +20,7 @@ import { IHeaders } from 'vs/base/parts/request/common/request';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileChangesEvent, FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files';
import { FileChangesEvent, FileOperationError, FileOperationResult, IFileContent, IFileService, toFileOperationResult } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
@ -540,8 +540,10 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
this.storageService.remove(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
try {
await this.fileService.del(this.lastSyncResource);
} catch (e) {
this.logService.error(e);
} catch (error) {
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
this.logService.error(error);
}
}
}

View file

@ -267,6 +267,12 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
}
// Method Not Found
else if (userDataSyncError.code === UserDataSyncErrorCode.MethodNotFound) {
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because current client is making requests to server that are not supported');
}
// Upgrade Required or Gone
else if (userDataSyncError.code === UserDataSyncErrorCode.UpgradeRequired || userDataSyncError.code === UserDataSyncErrorCode.Gone) {
await this.turnOff(false, true /* force soft turnoff on error */,
@ -458,89 +464,105 @@ class AutoSync extends Disposable {
private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise<void> {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
let error: Error | undefined;
try {
this.syncTask = await this.userDataSyncService.createSyncTask(this.manifest, disableCache);
if (token.isCancellationRequested) {
return;
}
this.manifest = this.syncTask.manifest;
// Server has no data but this machine was synced before
if (this.manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
}
const sessionId = this.storageService.get(sessionIdKey, StorageScope.APPLICATION);
// Server session is different from client session
if (sessionId && this.manifest && sessionId !== this.manifest.session) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
}
const machines = await this.userDataSyncMachinesService.getMachines(this.manifest || undefined);
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Throw TurnedOff error
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
await this.syncTask.run();
// After syncing, get the manifest if it was not available before
if (this.manifest === null) {
try {
this.manifest = await this.userDataSyncStoreService.manifest(null);
} catch (error) {
throw new UserDataAutoSyncError(toErrorMessage(error), error instanceof UserDataSyncError ? error.code : UserDataSyncErrorCode.Unknown);
}
}
// Update local session id
if (this.manifest && this.manifest.session !== sessionId) {
this.storageService.store(sessionIdKey, this.manifest.session, StorageScope.APPLICATION, StorageTarget.MACHINE);
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
// Add current machine
if (!currentMachine) {
await this.userDataSyncMachinesService.addCurrentMachine(this.manifest || undefined);
}
await this.createAndRunSyncTask(disableCache, token);
} catch (e) {
this.logService.error(e);
error = e;
if (UserDataSyncError.toUserDataSyncError(e).code === UserDataSyncErrorCode.MethodNotFound) {
try {
this.logService.info('Auto Sync: Client is making invalid requests. Cleaning up data...');
await this.userDataSyncService.cleanUpRemoteData();
this.logService.info('Auto Sync: Retrying sync...');
await this.createAndRunSyncTask(disableCache, token);
error = undefined;
} catch (e1) {
this.logService.error(e1);
error = e1;
}
}
}
this._onDidFinishSync.fire(error);
}
private async createAndRunSyncTask(disableCache: boolean, token: CancellationToken): Promise<void> {
this.syncTask = await this.userDataSyncService.createSyncTask(this.manifest, disableCache);
if (token.isCancellationRequested) {
return;
}
this.manifest = this.syncTask.manifest;
// Server has no data but this machine was synced before
if (this.manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
}
const sessionId = this.storageService.get(sessionIdKey, StorageScope.APPLICATION);
// Server session is different from client session
if (sessionId && this.manifest && sessionId !== this.manifest.session) {
if (this.hasSyncServiceChanged()) {
if (await this.hasDefaultServiceChanged()) {
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
} else {
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
}
} else {
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
}
const machines = await this.userDataSyncMachinesService.getMachines(this.manifest || undefined);
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Throw TurnedOff error
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
await this.syncTask.run();
// After syncing, get the manifest if it was not available before
if (this.manifest === null) {
try {
this.manifest = await this.userDataSyncStoreService.manifest(null);
} catch (error) {
throw new UserDataAutoSyncError(toErrorMessage(error), error instanceof UserDataSyncError ? error.code : UserDataSyncErrorCode.Unknown);
}
}
// Update local session id
if (this.manifest && this.manifest.session !== sessionId) {
this.storageService.store(sessionIdKey, this.manifest.session, StorageScope.APPLICATION, StorageTarget.MACHINE);
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
// Add current machine
if (!currentMachine) {
await this.userDataSyncMachinesService.addCurrentMachine(this.manifest || undefined);
}
}
register<T extends IDisposable>(t: T): T {
return super._register(t);
}

View file

@ -20,26 +20,20 @@ interface IUserDataProfileInfo {
}
export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[] | null, lastSync: ISyncUserDataProfile[] | null, ignored: string[]): IMergeResult {
const result: IRelaxedMergeResult = {
local: {
added: [],
removed: [],
updated: [],
}, remote: {
added: [],
removed: [],
updated: [],
}
};
const localResult: { added: ISyncUserDataProfile[]; removed: IUserDataProfile[]; updated: ISyncUserDataProfile[] } = { added: [], removed: [], updated: [] };
let remoteResult: { added: IUserDataProfile[]; removed: ISyncUserDataProfile[]; updated: IUserDataProfile[] } | null = { added: [], removed: [], updated: [] };
if (!remote) {
const added = local.filter(({ id }) => !ignored.includes(id));
if (added.length) {
result.remote!.added = added;
remoteResult.added = added;
} else {
result.remote = null;
remoteResult = null;
}
return result;
return {
local: localResult,
remote: remoteResult
};
}
const localToRemote = compare(local, remote, ignored);
@ -52,7 +46,7 @@ export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[]
for (const id of baseToRemote.removed) {
const e = local.find(profile => profile.id === id);
if (e) {
result.local.removed.push(e);
localResult.removed.push(e);
}
}
@ -64,24 +58,24 @@ export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[]
// Is different from local to remote
if (localToRemote.updated.includes(id)) {
// Remote wins always
result.local.updated.push(remoteProfile);
localResult.updated.push(remoteProfile);
}
} else {
result.local.added.push(remoteProfile);
localResult.added.push(remoteProfile);
}
}
// Remotely updated profiles
for (const id of baseToRemote.updated) {
// Remote wins always
result.local.updated.push(remote.find(profile => profile.id === id)!);
localResult.updated.push(remote.find(profile => profile.id === id)!);
}
// Locally added profiles
for (const id of baseToLocal.added) {
// Not there in remote
if (!baseToRemote.added.includes(id)) {
result.remote!.added.push(local.find(profile => profile.id === id)!);
remoteResult.added.push(local.find(profile => profile.id === id)!);
}
}
@ -94,21 +88,24 @@ export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[]
// If not updated in remote
if (!baseToRemote.updated.includes(id)) {
result.remote!.updated.push(local.find(profile => profile.id === id)!);
remoteResult.updated.push(local.find(profile => profile.id === id)!);
}
}
// Locally removed profiles
for (const id of baseToLocal.removed) {
result.remote!.removed.push(remote.find(profile => profile.id === id)!);
const removedProfile = remote.find(profile => profile.id === id);
if (removedProfile) {
remoteResult.removed.push(removedProfile);
}
}
}
if (result.remote!.added.length === 0 && result.remote!.removed.length === 0 && result.remote!.updated.length === 0) {
result.remote = null;
if (remoteResult.added.length === 0 && remoteResult.removed.length === 0 && remoteResult.updated.length === 0) {
remoteResult = null;
}
return result;
return { local: localResult, remote: remoteResult };
}
function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[], ignoredProfiles: string[]): { added: string[]; removed: string[]; updated: string[] } {

View file

@ -179,6 +179,11 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing profiles.`);
}
const remoteProfiles = resourcePreviews[0][0].remoteProfiles || [];
if (remoteProfiles.length + (remote?.added.length ?? 0) - (remote?.removed.length ?? 0) > 20) {
throw new UserDataSyncError('Too many profiles to sync. Please remove some profiles and try again.', UserDataSyncErrorCode.LocalTooManyProfiles);
}
if (localChange !== Change.None) {
await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false));
const promises: Promise<any>[] = [];
@ -212,11 +217,17 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i
}
if (remoteChange !== Change.None) {
const remoteProfiles = resourcePreviews[0][0].remoteProfiles || [];
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote profiles...`);
for (const profile of remote?.added || []) {
const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders);
remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName });
const addedCollections: string[] = [];
const canAddRemoteProfiles = remoteProfiles.length + (remote?.added.length ?? 0) <= 20;
if (canAddRemoteProfiles) {
for (const profile of remote?.added || []) {
const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders);
addedCollections.push(collection);
remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName });
}
} else {
this.logService.info(`${this.syncResourceLogLabel}: Could not create remote profiles as there are too many profiles.`);
}
for (const profile of remote?.removed || []) {
remoteProfiles.splice(remoteProfiles.findIndex(({ id }) => profile.id === id), 1);
@ -227,8 +238,19 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i
remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { id: profile.id, name: profile.name, collection: profileToBeUpdated.collection, shortName: profile.shortName });
}
}
remoteUserData = await this.updateRemoteUserData(this.stringifyRemoteProfiles(remoteProfiles), force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote profiles.${remote?.added.length ? ` Added: ${JSON.stringify(remote.added.map(e => e.name))}.` : ''}${remote?.updated.length ? ` Updated: ${JSON.stringify(remote.updated.map(e => e.name))}.` : ''}${remote?.removed.length ? ` Removed: ${JSON.stringify(remote.removed.map(e => e.name))}.` : ''}`);
try {
remoteUserData = await this.updateRemoteProfiles(remoteProfiles, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote profiles.${canAddRemoteProfiles && remote?.added.length ? ` Added: ${JSON.stringify(remote.added.map(e => e.name))}.` : ''}${remote?.updated.length ? ` Updated: ${JSON.stringify(remote.updated.map(e => e.name))}.` : ''}${remote?.removed.length ? ` Removed: ${JSON.stringify(remote.removed.map(e => e.name))}.` : ''}`);
} catch (error) {
if (addedCollections.length) {
this.logService.info(`${this.syncResourceLogLabel}: Failed to update remote profiles. Cleaning up added collections...`);
for (const collection of addedCollections) {
await this.userDataSyncStoreService.deleteCollection(collection, this.syncHeaders);
}
}
throw error;
}
for (const profile of remote?.removed || []) {
await this.userDataSyncStoreService.deleteCollection(profile.collection, this.syncHeaders);
@ -243,6 +265,10 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i
}
}
async updateRemoteProfiles(profiles: ISyncUserDataProfile[], ref: string | null): Promise<IRemoteUserData> {
return this.updateRemoteUserData(this.stringifyRemoteProfiles(profiles), ref);
}
async hasLocalData(): Promise<boolean> {
return this.getLocalUserDataProfiles().length > 0;
}

View file

@ -238,6 +238,7 @@ export const enum UserDataSyncErrorCode {
// Client Errors (>= 400 )
Unauthorized = 'Unauthorized', /* 401 */
NotFound = 'NotFound', /* 404 */
MethodNotFound = 'MethodNotFound', /* 405 */
Conflict = 'Conflict', /* 409 */
Gone = 'Gone', /* 410 */
PreconditionFailed = 'PreconditionFailed', /* 412 */
@ -261,6 +262,7 @@ export const enum UserDataSyncErrorCode {
SessionExpired = 'SessionExpired',
ServiceChanged = 'ServiceChanged',
DefaultServiceChanged = 'DefaultServiceChanged',
LocalTooManyProfiles = 'LocalTooManyProfiles',
LocalTooManyRequests = 'LocalTooManyRequests',
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
@ -509,6 +511,7 @@ export interface IUserDataSyncService {
reset(): Promise<void>;
resetRemote(): Promise<void>;
cleanUpRemoteData(): Promise<void>;
resetLocal(): Promise<void>;
hasLocalData(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;

View file

@ -167,7 +167,23 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return that.sync(manifest, true, executionId, cancellableToken.token);
},
async apply(): Promise<void> {
await that.applyManualSync(manifest, executionId, cancellableToken.token);
try {
try {
await that.applyManualSync(manifest, executionId, cancellableToken.token);
} catch (error) {
if (UserDataSyncError.toUserDataSyncError(error).code === UserDataSyncErrorCode.MethodNotFound) {
that.logService.info('Client is making invalid requests. Cleaning up data...');
await that.cleanUpRemoteData();
that.logService.info('Applying manual sync again...');
await that.applyManualSync(manifest, executionId, cancellableToken.token);
} else {
throw error;
}
}
} catch (error) {
that.logService.error(error);
throw error;
}
that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
that.updateLastSyncTime();
},
@ -395,6 +411,29 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.info('Did reset the local sync state.');
}
async cleanUpRemoteData(): Promise<void> {
const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles();
const remoteProfileCollections = remoteProfiles.map(profile => profile.collection);
const allCollections = await this.userDataSyncStoreService.getAllCollections();
const redundantCollections = allCollections.filter(c => !remoteProfileCollections.includes(c));
if (redundantCollections.length) {
this.logService.info(`Deleting ${redundantCollections.length} redundant collections on server`);
await Promise.allSettled(redundantCollections.map(collectionId => this.userDataSyncStoreService.deleteCollection(collectionId)));
this.logService.info(`Deleted redundant collections on server`);
}
const updatedRemoteProfiles = remoteProfiles.filter(profile => allCollections.includes(profile.collection));
if (updatedRemoteProfiles.length !== remoteProfiles.length) {
this.logService.info(`Updating remote profiles with invalid collections on server`);
const profileManifestSynchronizer = this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.userDataProfilesService.defaultProfile, undefined);
try {
await profileManifestSynchronizer.updateRemoteProfiles(updatedRemoteProfiles, null);
this.logService.info(`Updated remote profiles on server`);
} finally {
profileManifestSynchronizer.dispose();
}
}
}
private async performAction<T>(profile: IUserDataProfile, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>): Promise<T | null> {
const disposables = new DisposableStore();
try {
@ -756,13 +795,13 @@ class ProfileSynchronizer extends Disposable {
private getOrder(syncResource: SyncResource): number {
switch (syncResource) {
case SyncResource.Profiles: return 0;
case SyncResource.Settings: return 1;
case SyncResource.Keybindings: return 2;
case SyncResource.Snippets: return 3;
case SyncResource.Tasks: return 4;
case SyncResource.GlobalState: return 5;
case SyncResource.Extensions: return 6;
case SyncResource.Settings: return 0;
case SyncResource.Keybindings: return 1;
case SyncResource.Snippets: return 2;
case SyncResource.Tasks: return 3;
case SyncResource.GlobalState: return 4;
case SyncResource.Extensions: return 5;
case SyncResource.Profiles: return 6;
}
}
@ -771,10 +810,12 @@ class ProfileSynchronizer extends Disposable {
function canBailout(e: any): boolean {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.MethodNotFound:
case UserDataSyncErrorCode.TooLarge:
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.LocalTooManyProfiles:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.IncompatibleRemoteContent:

View file

@ -82,6 +82,7 @@ export class UserDataSyncChannel implements IServerChannel {
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 'cleanUpRemoteData': return this.service.cleanUpRemoteData();
case 'createManualSyncTask': return this.createManualSyncTask();
}
@ -95,7 +96,7 @@ export class UserDataSyncChannel implements IServerChannel {
switch (manualSyncTaskCommand) {
case 'merge': return manualSyncTask.merge();
case 'apply': return manualSyncTask.apply().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
case 'apply': return manualSyncTask.apply().then(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
case 'stop': return manualSyncTask.stop().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
}
}
@ -246,6 +247,10 @@ export class UserDataSyncChannelClient extends Disposable implements IUserDataSy
return this.channel.call<string | undefined>('getMachineId', [syncResourceHandle]);
}
cleanUpRemoteData(): Promise<void> {
return this.channel.call('cleanUpRemoteData');
}
replace(syncResourceHandle: ISyncResourceHandle): Promise<void> {
return this.channel.call('replace', [syncResourceHandle]);
}

View file

@ -20,7 +20,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IProductService } from 'vs/platform/product/common/productService';
import { asJson, asTextOrError, IRequestService, isSuccess as isSuccessContext } from 'vs/platform/request/common/request';
import { asJson, asText, asTextOrError, IRequestService, isSuccess as isSuccessContext } from 'vs/platform/request/common/request';
import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CONFIGURATION_SYNC_STORE_KEY, HEADER_EXECUTION_ID, HEADER_OPERATION_ID, IAuthenticationProvider, IResourceRefHandle, IUserData, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncStore, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, ServerResource, SYNC_SERVICE_URL_TYPE, UserDataSyncErrorCode, UserDataSyncStoreError, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync';
@ -243,7 +243,7 @@ export class UserDataSyncStoreClient extends Disposable {
const context = await this.request(url, { type: 'GET', headers }, [], CancellationToken.None);
return (await asJson<string[]>(context)) || [];
return (await asJson<{ id: string }[]>(context))?.map(({ id }) => id) || [];
}
async createCollection(headers: IHeaders = {}): Promise<string> {
@ -448,8 +448,8 @@ export class UserDataSyncStoreClient extends Disposable {
throw new Error('No settings sync store url configured.');
}
await this.deleteResources();
await this.deleteCollection();
await this.deleteResources();
// clear cached session.
this.clearSession();
@ -529,10 +529,12 @@ export class UserDataSyncStoreClient extends Disposable {
const operationId = context.res.headers[HEADER_OPERATION_ID];
const requestInfo = { url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId };
const isSuccess = isSuccessContext(context) || (context.res.statusCode && successCodes.indexOf(context.res.statusCode) !== -1);
let failureMessage = '';
if (isSuccess) {
this.logService.trace('Request succeeded', requestInfo);
} else {
this.logService.info('Request failed', requestInfo);
failureMessage = await asText(context) || '';
}
if (context.res.statusCode === 401) {
@ -547,6 +549,10 @@ export class UserDataSyncStoreClient extends Disposable {
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because the requested resource is not found (404).`, url, UserDataSyncErrorCode.NotFound, context.res.statusCode, operationId);
}
if (context.res.statusCode === 405) {
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because the requested endpoint is not found (405). ${failureMessage}`, url, UserDataSyncErrorCode.MethodNotFound, context.res.statusCode, operationId);
}
if (context.res.statusCode === 409) {
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, url, UserDataSyncErrorCode.Conflict, context.res.statusCode, operationId);
}

View file

@ -149,8 +149,6 @@ suite('UserDataAutoSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
@ -168,6 +166,8 @@ suite('UserDataAutoSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines

View file

@ -173,4 +173,24 @@ suite('UserDataProfilesManifestMerge', () => {
assert.strictEqual(actual.remote, null);
});
test('merge when profile is removed locally, but not exists in remote', () => {
const localProfiles: IUserDataProfile[] = [
toUserDataProfile('1', '1', URI.file('1')),
];
const base: ISyncUserDataProfile[] = [
{ id: '1', name: '1', collection: '1' },
{ id: '2', name: '2', collection: '2' },
];
const remoteProfiles: ISyncUserDataProfile[] = [
{ id: '1', name: '3', collection: '1' },
];
const actual = merge(localProfiles, remoteProfiles, base, []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, remoteProfiles);
assert.strictEqual(actual.remote, null);
});
});

View file

@ -32,8 +32,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
@ -51,6 +49,8 @@ suite('UserDataSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});
@ -69,8 +69,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
@ -85,6 +83,8 @@ suite('UserDataSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});
@ -102,8 +102,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings
@ -116,6 +114,8 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});
@ -139,13 +139,13 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});
@ -177,7 +177,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
@ -187,6 +186,7 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});
@ -219,9 +219,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/collection`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/profiles`, headers: { 'If-Match': '0' } },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
@ -231,6 +228,9 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/collection`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/profiles`, headers: { 'If-Match': '0' } },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
@ -322,9 +322,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'POST', url: `${target.url}/v1/collection`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/profiles`, headers: { 'If-Match': '0' } },
// Settings
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
// Keybindings
@ -333,6 +330,9 @@ suite('UserDataSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
// Global state
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
// Profiles
{ type: 'POST', url: `${target.url}/v1/collection`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/profiles`, headers: { 'If-Match': '0' } },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
@ -452,8 +452,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: { 'If-None-Match': '0' } },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
// Keybindings
@ -462,6 +460,8 @@ suite('UserDataSyncService', () => {
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: { 'If-None-Match': '1' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: { 'If-None-Match': '0' } },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/collection/1/resource/snippets/latest`, headers: {} },
@ -487,8 +487,8 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
{ type: 'DELETE', url: `${target.url}/v1/collection`, headers: {} },
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
]);
});
@ -512,8 +512,6 @@ suite('UserDataSyncService', () => {
assert.deepStrictEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
@ -531,6 +529,8 @@ suite('UserDataSyncService', () => {
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
// Profiles
{ type: 'GET', url: `${target.url}/v1/resource/profiles/latest`, headers: {} },
]);
});

View file

@ -53,6 +53,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ctxIsMergeResultEditor, ctxMergeBaseUri } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor';
import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';
type ConfigureSyncQuickPickItem = { id: SyncResource; label: string; description?: string };
@ -115,7 +116,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IHostService private readonly hostService: IHostService
@IHostService private readonly hostService: IHostService,
@ICommandService private readonly commandService: ICommandService,
@IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService
) {
super();
@ -268,6 +271,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
this.handleTooLargeError(error.resource, localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), error);
}
break;
case UserDataSyncErrorCode.LocalTooManyProfiles:
this.disableSync(SyncResource.Profiles);
this.notificationService.error(localize('too many profiles', "Disabled syncing profiles because there are too many profiles to sync. Settings Sync supports syncing maximum 20 profiles. Please reduce the number of profiles and enable sync"));
break;
case UserDataSyncErrorCode.IncompatibleLocalContent:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired: {
@ -279,6 +286,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
});
break;
}
case UserDataSyncErrorCode.MethodNotFound: {
const message = localize('method not found', "Settings sync is disabled because the client is making invalid requests. Please report an issue with the logs.");
const operationId = error.operationId ? localize('operationId', "Operation Id: {0}", error.operationId) : undefined;
this.notificationService.notify({
severity: Severity.Error,
message: operationId ? `${message} ${operationId}` : message,
actions: {
primary: [
new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)),
new Action('Report Issue', localize('report issue', "Report Issue"), undefined, true, () => this.workbenchIssueService.openReporter())
]
}
});
break;
}
case UserDataSyncErrorCode.IncompatibleRemoteContent:
this.notificationService.notify({
severity: Severity.Error,
@ -619,6 +641,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
case SyncResource.Tasks: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Tasks, false);
case SyncResource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false);
case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, false);
case SyncResource.Profiles: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Profiles, false);
}
}