userDataSync.ts 13.3 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';
S
#90076  
Sandeep Somavarapu 已提交
21
import { isEqual, joinPath } 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 146 147 148 149 150 151 152 153
}

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 已提交
154 155 156
	getAllRefs(key: ResourceKey): Promise<string[]>;
	resolveContent(key: ResourceKey, ref: string): Promise<string | null>;
	delete(key: ResourceKey): Promise<void>;
157 158 159 160 161 162
}

//#endregion

// #region User Data Sync Error

S
Sandeep Somavarapu 已提交
163
export enum UserDataSyncErrorCode {
164
	// Server Errors
D
Daniel Imms 已提交
165
	Unauthorized = 'Unauthorized',
S
Sandeep Somavarapu 已提交
166 167
	Forbidden = 'Forbidden',
	ConnectionRefused = 'ConnectionRefused',
168
	RemotePreconditionFailed = 'RemotePreconditionFailed',
S
Sandeep Somavarapu 已提交
169
	TooLarge = 'TooLarge',
170
	NoRef = 'NoRef',
S
Sandeep Somavarapu 已提交
171
	TurnedOff = 'TurnedOff',
S
Sandeep Somavarapu 已提交
172
	SessionExpired = 'SessionExpired',
173 174 175 176

	// Local Errors
	LocalPreconditionFailed = 'LocalPreconditionFailed',
	LocalInvalidContent = 'LocalInvalidContent',
S
Sandeep Somavarapu 已提交
177
	LocalError = 'LocalError',
178
	Incompatible = 'Incompatible',
179

S
Sandeep Somavarapu 已提交
180
	Unknown = 'Unknown',
181 182
}

S
Sandeep Somavarapu 已提交
183
export class UserDataSyncError extends Error {
184

S
Sandeep Somavarapu 已提交
185
	constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) {
186
		super(message);
S
Sandeep Somavarapu 已提交
187 188 189 190 191 192 193 194 195 196 197 198
		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);
199 200 201 202
	}

}

S
Sandeep Somavarapu 已提交
203 204
export class UserDataSyncStoreError extends UserDataSyncError { }

205
//#endregion
206

207
// #region User Data Synchroniser
208

S
Sandeep Somavarapu 已提交
209 210 211
export interface ISyncExtension {
	identifier: IExtensionIdentifier;
	version?: string;
212
	disabled?: boolean;
S
Sandeep Somavarapu 已提交
213 214
}

215 216 217 218 219
export interface IGlobalState {
	argv: IStringDictionary<any>;
	storage: IStringDictionary<any>;
}

S
Sandeep Somavarapu 已提交
220
export const enum SyncSource {
S
Sandeep Somavarapu 已提交
221 222 223
	Settings = 'Settings',
	Keybindings = 'Keybindings',
	Extensions = 'Extensions',
S
Sandeep Somavarapu 已提交
224
	GlobalState = 'GlobalState'
225
}
226

S
Sandeep Somavarapu 已提交
227
export const enum SyncStatus {
228 229 230 231 232 233
	Uninitialized = 'uninitialized',
	Idle = 'idle',
	Syncing = 'syncing',
	HasConflicts = 'hasConflicts',
}

234 235
export interface IUserDataSynchroniser {

S
Sandeep Somavarapu 已提交
236
	readonly resourceKey: ResourceKey;
237
	readonly source: SyncSource;
238 239 240
	readonly status: SyncStatus;
	readonly onDidChangeStatus: Event<SyncStatus>;
	readonly onDidChangeLocal: Event<void>;
241

242 243
	pull(): Promise<void>;
	push(): Promise<void>;
S
Sandeep Somavarapu 已提交
244
	sync(ref?: string): Promise<void>;
S
Sandeep Somavarapu 已提交
245
	stop(): Promise<void>;
246

247
	hasPreviouslySynced(): Promise<boolean>
248
	hasLocalData(): Promise<boolean>;
S
Sandeep Somavarapu 已提交
249
	resetLocal(): Promise<void>;
250

S
Sandeep Somavarapu 已提交
251 252
	getRemoteContent(preivew?: boolean): Promise<string | null>;
	accept(content: string): Promise<void>;
253 254
}

255 256 257 258
//#endregion

// #region User Data Sync Services

S
Sandeep Somavarapu 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271 272
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;
}

273
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
274
export interface IUserDataSyncService {
275
	_serviceBrand: any;
276 277 278 279

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

280 281
	readonly conflictsSources: SyncSource[];
	readonly onDidChangeConflicts: Event<SyncSource[]>;
282

283
	readonly onDidChangeLocal: Event<SyncSource>;
S
Sandeep Somavarapu 已提交
284
	readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>;
285

286 287 288
	readonly lastSyncTime: number | undefined;
	readonly onDidChangeLastSyncTime: Event<number>;

289 290 291
	pull(): Promise<void>;
	sync(): Promise<void>;
	stop(): Promise<void>;
S
Sandeep Somavarapu 已提交
292 293
	reset(): Promise<void>;
	resetLocal(): Promise<void>;
294

S
Sandeep Somavarapu 已提交
295
	isFirstTimeSyncWithMerge(): Promise<boolean>;
S
Sandeep Somavarapu 已提交
296 297
	getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null>;
	accept(source: SyncSource, content: string): Promise<void>;
298
}
299

S
Sandeep Somavarapu 已提交
300 301 302
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
export interface IUserDataAutoSyncService {
	_serviceBrand: any;
303
	readonly onError: Event<UserDataSyncError>;
304
	triggerAutoSync(sources: string[]): Promise<void>;
S
Sandeep Somavarapu 已提交
305 306
}

307 308
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {
309
	_serviceBrand: undefined;
310 311
	resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
	resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
S
Sandeep Somavarapu 已提交
312
	resolveDefaultIgnoredSettings(): Promise<string[]>;
313 314
}

315
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
S
Sandeep Somavarapu 已提交
316
export interface IUserDataSyncLogService extends ILogService { }
317

S
Sandeep Somavarapu 已提交
318 319 320 321 322
export interface IConflictSetting {
	key: string;
	localValue: any | undefined;
	remoteValue: any | undefined;
}
323

S
Sandeep Somavarapu 已提交
324
export const ISettingsSyncService = createDecorator<ISettingsSyncService>('ISettingsSyncService');
325
export interface ISettingsSyncService extends IUserDataSynchroniser {
S
Sandeep Somavarapu 已提交
326
	_serviceBrand: any;
327 328
	readonly onDidChangeConflicts: Event<IConflictSetting[]>;
	readonly conflicts: IConflictSetting[];
S
Sandeep Somavarapu 已提交
329
	resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
330 331
}

332 333
//#endregion

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

S
Sandeep Somavarapu 已提交
337
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
338 339 340 341
export function toRemoteContentResource(source: SyncSource): URI {
	return URI.from({ scheme: USER_DATA_SYNC_SCHEME, path: `${source}/remoteContent` });
}
export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined {
S
Sandeep Somavarapu 已提交
342
	return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0];
343
}
344 345 346 347 348 349 350 351 352
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;
}