userDataSync.ts 15.1 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
S
Sandeep Somavarapu 已提交
8
import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
9
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
10
import { Registry } from 'vs/platform/registry/common/platform';
11
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry';
12
import { localize } from 'vs/nls';
13 14 15
import { IDisposable } from 'vs/base/common/lifecycle';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
16
import { ILogService } from 'vs/platform/log/common/log';
17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
S
Sandeep Somavarapu 已提交
18
import { IStringDictionary } from 'vs/base/common/collections';
19 20
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
21
import { isEqual, joinPath, dirname, basename } from 'vs/base/common/resources';
22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
23
import { IProductService } from 'vs/platform/product/common/productService';
S
Sandeep Somavarapu 已提交
24
import { distinct } from 'vs/base/common/arrays';
25

26
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
27

28 29 30 31 32 33 34 35 36 37 38 39 40
export interface ISyncConfiguration {
	sync: {
		enable: boolean,
		enableSettings: boolean,
		enableKeybindings: boolean,
		enableUIState: boolean,
		enableExtensions: boolean,
		keybindingsPerPlatform: boolean,
		ignoredExtensions: string[],
		ignoredSettings: string[]
	}
}

S
Sandeep Somavarapu 已提交
41 42 43 44 45
export function getDisallowedIgnoredSettings(): string[] {
	const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
	return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore);
}

S
Sandeep Somavarapu 已提交
46 47 48
export function getDefaultIgnoredSettings(): string[] {
	const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
	const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE);
S
Sandeep Somavarapu 已提交
49 50
	const disallowedSettings = getDisallowedIgnoredSettings();
	return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]);
S
Sandeep Somavarapu 已提交
51 52
}

53 54
export function registerConfiguration(): IDisposable {
	const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings';
55
	const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions';
56 57
	const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
	configurationRegistry.registerConfiguration({
58
		id: 'sync',
59
		order: 30,
60
		title: localize('sync', "Sync"),
61 62
		type: 'object',
		properties: {
63 64 65 66 67
			'sync.keybindingsPerPlatform': {
				type: 'boolean',
				description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
				default: true,
				scope: ConfigurationScope.APPLICATION,
S
Sandeep Somavarapu 已提交
68
				tags: ['sync', 'usesOnlineServices']
69
			},
70
			'sync.ignoredExtensions': {
71
				'type': 'array',
S
Sandeep Somavarapu 已提交
72
				'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."),
73
				$ref: ignoredExtensionsSchemaId,
74 75
				'default': [],
				'scope': ConfigurationScope.APPLICATION,
76
				uniqueItems: true,
R
Rob Lourens 已提交
77
				disallowSyncIgnore: true,
S
Sandeep Somavarapu 已提交
78
				tags: ['sync', 'usesOnlineServices']
79
			},
80
			'sync.ignoredSettings': {
81
				'type': 'array',
S
Sandeep Somavarapu 已提交
82
				description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
83
				'default': [],
84 85 86
				'scope': ConfigurationScope.APPLICATION,
				$ref: ignoredSettingsSchemaId,
				additionalProperties: true,
87
				uniqueItems: true,
R
Rob Lourens 已提交
88
				disallowSyncIgnore: true,
S
Sandeep Somavarapu 已提交
89
				tags: ['sync', 'usesOnlineServices']
90 91 92
			}
		}
	});
93
	const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
94
	const registerIgnoredSettingsSchema = () => {
S
Sandeep Somavarapu 已提交
95 96 97 98
		const disallowedIgnoredSettings = getDisallowedIgnoredSettings();
		const defaultIgnoredSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY);
		const settings = Object.keys(allSettings.properties).filter(setting => defaultIgnoredSettings.indexOf(setting) === -1);
		const ignoredSettings = defaultIgnoredSettings.filter(setting => disallowedIgnoredSettings.indexOf(setting) === -1);
99 100 101
		const ignoredSettingsSchema: IJSONSchema = {
			items: {
				type: 'string',
S
Sandeep Somavarapu 已提交
102
				enum: [...settings, ...ignoredSettings.map(setting => `-${setting}`)]
S
Sandeep Somavarapu 已提交
103
			},
104 105 106
		};
		jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema);
	};
107 108 109 110 111
	jsonRegistry.registerSchema(ignoredExtensionsSchemaId, {
		type: 'string',
		pattern: EXTENSION_IDENTIFIER_PATTERN,
		errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.")
	});
112
	return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema());
113
}
114

115 116
// #region User Data Sync Store

117 118
export interface IUserData {
	ref: string;
119
	content: string | null;
120 121
}

122
export interface IUserDataSyncStore {
S
#90076  
Sandeep Somavarapu 已提交
123
	url: URI;
124 125 126
	authenticationProviderId: string;
}

127 128
export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined {
	const value = productService[CONFIGURATION_SYNC_STORE_KEY] || configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY);
S
#90076  
Sandeep Somavarapu 已提交
129 130 131 132 133 134 135
	if (value && value.url && value.authenticationProviderId) {
		return {
			url: joinPath(URI.parse(value.url), 'v1'),
			authenticationProviderId: value.authenticationProviderId
		};
	}
	return undefined;
136 137
}

S
Sandeep Somavarapu 已提交
138
export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState'];
139 140 141
export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState';

export interface IUserDataManifest {
S
#90076  
Sandeep Somavarapu 已提交
142 143
	latest?: Record<ResourceKey, string>
	session: string;
144 145
}

S
Sandeep Somavarapu 已提交
146 147 148 149 150
export interface IResourceRefHandle {
	ref: string;
	created: number;
}

151 152 153 154 155 156 157 158
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export interface IUserDataSyncStoreService {
	_serviceBrand: undefined;
	readonly userDataSyncStore: IUserDataSyncStore | undefined;
	read(key: ResourceKey, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData>;
	write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise<string>;
	manifest(): Promise<IUserDataManifest | null>;
	clear(): Promise<void>;
S
Sandeep Somavarapu 已提交
159
	getAllRefs(key: ResourceKey): Promise<IResourceRefHandle[]>;
S
Sandeep Somavarapu 已提交
160 161
	resolveContent(key: ResourceKey, ref: string): Promise<string | null>;
	delete(key: ResourceKey): Promise<void>;
162 163
}

S
Sandeep Somavarapu 已提交
164 165 166 167
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
export interface IUserDataSyncBackupStoreService {
	_serviceBrand: undefined;
	backup(resourceKey: ResourceKey, content: string): Promise<void>;
168 169
	getAllRefs(key: ResourceKey): Promise<IResourceRefHandle[]>;
	resolveContent(key: ResourceKey, ref?: string): Promise<string | null>;
S
Sandeep Somavarapu 已提交
170 171
}

172 173 174 175
//#endregion

// #region User Data Sync Error

S
Sandeep Somavarapu 已提交
176
export enum UserDataSyncErrorCode {
177
	// Server Errors
D
Daniel Imms 已提交
178
	Unauthorized = 'Unauthorized',
S
Sandeep Somavarapu 已提交
179 180
	Forbidden = 'Forbidden',
	ConnectionRefused = 'ConnectionRefused',
181
	RemotePreconditionFailed = 'RemotePreconditionFailed',
S
Sandeep Somavarapu 已提交
182
	TooLarge = 'TooLarge',
183
	NoRef = 'NoRef',
S
Sandeep Somavarapu 已提交
184
	TurnedOff = 'TurnedOff',
S
Sandeep Somavarapu 已提交
185
	SessionExpired = 'SessionExpired',
186 187 188 189

	// Local Errors
	LocalPreconditionFailed = 'LocalPreconditionFailed',
	LocalInvalidContent = 'LocalInvalidContent',
S
Sandeep Somavarapu 已提交
190
	LocalError = 'LocalError',
191
	Incompatible = 'Incompatible',
192

S
Sandeep Somavarapu 已提交
193
	Unknown = 'Unknown',
194 195
}

S
Sandeep Somavarapu 已提交
196
export class UserDataSyncError extends Error {
197

S
Sandeep Somavarapu 已提交
198
	constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) {
199
		super(message);
S
Sandeep Somavarapu 已提交
200 201 202 203 204 205 206 207 208 209 210 211
		this.name = `${this.code} (UserDataSyncError) ${this.source}`;
	}

	static toUserDataSyncError(error: Error): UserDataSyncError {
		if (error instanceof UserDataSyncStoreError) {
			return error;
		}
		const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name);
		if (match && match[1]) {
			return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], <SyncSource>match[2]);
		}
		return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
212 213 214 215
	}

}

S
Sandeep Somavarapu 已提交
216 217
export class UserDataSyncStoreError extends UserDataSyncError { }

218
//#endregion
219

220
// #region User Data Synchroniser
221

S
Sandeep Somavarapu 已提交
222 223 224
export interface ISyncExtension {
	identifier: IExtensionIdentifier;
	version?: string;
225
	disabled?: boolean;
S
Sandeep Somavarapu 已提交
226 227
}

228 229 230 231 232
export interface IGlobalState {
	argv: IStringDictionary<any>;
	storage: IStringDictionary<any>;
}

S
Sandeep Somavarapu 已提交
233
export const enum SyncSource {
S
Sandeep Somavarapu 已提交
234 235 236
	Settings = 'Settings',
	Keybindings = 'Keybindings',
	Extensions = 'Extensions',
S
Sandeep Somavarapu 已提交
237
	GlobalState = 'GlobalState'
238
}
239

S
Sandeep Somavarapu 已提交
240
export const enum SyncStatus {
241 242 243 244 245 246
	Uninitialized = 'uninitialized',
	Idle = 'idle',
	Syncing = 'syncing',
	HasConflicts = 'hasConflicts',
}

247 248
export interface IUserDataSynchroniser {

S
Sandeep Somavarapu 已提交
249
	readonly resourceKey: ResourceKey;
250
	readonly source: SyncSource;
251 252 253
	readonly status: SyncStatus;
	readonly onDidChangeStatus: Event<SyncStatus>;
	readonly onDidChangeLocal: Event<void>;
254

255 256
	pull(): Promise<void>;
	push(): Promise<void>;
S
Sandeep Somavarapu 已提交
257
	sync(ref?: string): Promise<void>;
S
Sandeep Somavarapu 已提交
258
	stop(): Promise<void>;
259

260
	hasPreviouslySynced(): Promise<boolean>
261
	hasLocalData(): Promise<boolean>;
S
Sandeep Somavarapu 已提交
262
	resetLocal(): Promise<void>;
263

264
	getRemoteContentFromPreview(): Promise<string | null>;
S
Sandeep Somavarapu 已提交
265
	getRemoteContent(ref?: string, fragment?: string): Promise<string | null>;
266
	getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null>;
S
Sandeep Somavarapu 已提交
267
	accept(content: string): Promise<void>;
268 269
}

270 271 272 273
//#endregion

// #region User Data Sync Services

S
Sandeep Somavarapu 已提交
274 275 276 277 278 279 280 281 282 283 284 285 286 287
export const IUserDataSyncEnablementService = createDecorator<IUserDataSyncEnablementService>('IUserDataSyncEnablementService');
export interface IUserDataSyncEnablementService {
	_serviceBrand: any;

	readonly onDidChangeEnablement: Event<boolean>;
	readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]>;

	isEnabled(): boolean;
	setEnablement(enabled: boolean): void;

	isResourceEnabled(key: ResourceKey): boolean;
	setResourceEnablement(key: ResourceKey, enabled: boolean): void;
}

288
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
289
export interface IUserDataSyncService {
290
	_serviceBrand: any;
291 292 293 294

	readonly status: SyncStatus;
	readonly onDidChangeStatus: Event<SyncStatus>;

295 296
	readonly conflictsSources: SyncSource[];
	readonly onDidChangeConflicts: Event<SyncSource[]>;
297

298
	readonly onDidChangeLocal: Event<SyncSource>;
S
Sandeep Somavarapu 已提交
299
	readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>;
300

301 302 303
	readonly lastSyncTime: number | undefined;
	readonly onDidChangeLastSyncTime: Event<number>;

304 305 306
	pull(): Promise<void>;
	sync(): Promise<void>;
	stop(): Promise<void>;
S
Sandeep Somavarapu 已提交
307 308
	reset(): Promise<void>;
	resetLocal(): Promise<void>;
309

S
Sandeep Somavarapu 已提交
310
	isFirstTimeSyncWithMerge(): Promise<boolean>;
311
	resolveContent(resource: URI): Promise<string | null>;
S
Sandeep Somavarapu 已提交
312
	accept(source: SyncSource, content: string): Promise<void>;
313
}
314

S
Sandeep Somavarapu 已提交
315 316 317
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
export interface IUserDataAutoSyncService {
	_serviceBrand: any;
318
	readonly onError: Event<UserDataSyncError>;
319
	triggerAutoSync(sources: string[]): Promise<void>;
S
Sandeep Somavarapu 已提交
320 321
}

322 323
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {
324
	_serviceBrand: undefined;
325 326
	resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
	resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
S
Sandeep Somavarapu 已提交
327
	resolveDefaultIgnoredSettings(): Promise<string[]>;
328 329
}

330
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
S
Sandeep Somavarapu 已提交
331
export interface IUserDataSyncLogService extends ILogService { }
332

S
Sandeep Somavarapu 已提交
333 334 335 336 337
export interface IConflictSetting {
	key: string;
	localValue: any | undefined;
	remoteValue: any | undefined;
}
338

S
Sandeep Somavarapu 已提交
339
export const ISettingsSyncService = createDecorator<ISettingsSyncService>('ISettingsSyncService');
340
export interface ISettingsSyncService extends IUserDataSynchroniser {
S
Sandeep Somavarapu 已提交
341
	_serviceBrand: any;
342 343
	readonly onDidChangeConflicts: Event<IConflictSetting[]>;
	readonly conflicts: IConflictSetting[];
S
Sandeep Somavarapu 已提交
344
	resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
345 346
}

347 348
//#endregion

349
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
S
Sandeep Somavarapu 已提交
350
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
351

S
Sandeep Somavarapu 已提交
352
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
353
export const PREVIEW_QUERY = 'preview=true';
354 355
export function toRemoteSyncResourceFromSource(source: SyncSource, ref?: string): URI {
	return toRemoteSyncResource(getResourceKeyFromSyncSource(source), ref);
356
}
357 358
export function toRemoteSyncResource(resourceKey: ResourceKey, ref?: string): URI {
	return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resourceKey}/${ref ? ref : 'latest'}` });
359
}
360 361 362
export function toLocalBackupSyncResource(resourceKey: ResourceKey, ref?: string): URI {
	return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resourceKey}/${ref ? ref : 'latest'}` });
}
363

364 365
export function resolveSyncResource(resource: URI): { remote: boolean, resourceKey: ResourceKey, ref?: string } | null {
	const remote = resource.authority === 'remote';
366 367 368
	const resourceKey: ResourceKey = basename(dirname(resource)) as ResourceKey;
	const ref = basename(resource);
	if (resourceKey && ref) {
369
		return { remote, resourceKey, ref: ref !== 'latest' ? ref : undefined };
370 371 372 373
	}
	return null;
}

374 375 376 377 378 379 380 381 382
export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncSource | undefined {
	if (isEqual(uri, environmentService.settingsSyncPreviewResource)) {
		return SyncSource.Settings;
	}
	if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) {
		return SyncSource.Keybindings;
	}
	return undefined;
}
383 384

export function getResourceKeyFromSyncSource(source: SyncSource): ResourceKey {
385 386 387 388 389 390 391
	switch (source) {
		case SyncSource.Settings: return 'settings';
		case SyncSource.Keybindings: return 'keybindings';
		case SyncSource.Extensions: return 'extensions';
		case SyncSource.GlobalState: return 'globalState';
	}
}
392 393

export function getSyncSourceFromResourceKey(resourceKey: ResourceKey): SyncSource {
394 395 396 397 398 399 400
	switch (resourceKey) {
		case 'settings': return SyncSource.Settings;
		case 'keybindings': return SyncSource.Keybindings;
		case 'extensions': return SyncSource.Extensions;
		case 'globalState': return SyncSource.GlobalState;
	}
}