Skip to content

Commit 59b4249

Browse files
authored
support collections to UserDataSyncStoreService (microsoft#160660)
1 parent c7c1717 commit 59b4249

File tree

9 files changed

+132
-77
lines changed

9 files changed

+132
-77
lines changed

src/vs/platform/userDataSync/common/abstractSynchronizer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
484484
}
485485

486486
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
487-
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
487+
const handles = await this.userDataSyncStoreService.getAllResourceRefs(this.resource);
488488
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
489489
}
490490

@@ -665,19 +665,19 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
665665

666666
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
667667
if (isString(refOrLastSyncData)) {
668-
const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData);
668+
const content = await this.userDataSyncStoreService.resolveResourceContent(this.resource, refOrLastSyncData);
669669
return { ref: refOrLastSyncData, content };
670670
} else {
671671
const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null;
672-
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, undefined, this.syncHeaders);
672+
return this.userDataSyncStoreService.readResource(this.resource, lastSyncUserData, undefined, this.syncHeaders);
673673
}
674674
}
675675

676676
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
677677
const machineId = await this.currentMachineIdPromise;
678678
const syncData: ISyncData = { version: this.version, machineId, content };
679679
try {
680-
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, undefined, this.syncHeaders);
680+
ref = await this.userDataSyncStoreService.writeResource(this.resource, JSON.stringify(syncData), ref, undefined, this.syncHeaders);
681681
return { ref, syncData };
682682
} catch (error) {
683683
if (error instanceof UserDataSyncError && error.code === UserDataSyncErrorCode.TooLarge) {

src/vs/platform/userDataSync/common/globalStateSync.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ export class UserDataSyncStoreTypeSynchronizer {
480480

481481
private async doSync(userDataSyncStoreType: UserDataSyncStoreType, syncHeaders: IHeaders): Promise<void> {
482482
// Read the global state from remote
483-
const globalStateUserData = await this.userDataSyncStoreClient.readResource(SyncResource.GlobalState, null, syncHeaders);
483+
const globalStateUserData = await this.userDataSyncStoreClient.readResource(SyncResource.GlobalState, null, undefined, syncHeaders);
484484
const remoteGlobalState = this.parseGlobalState(globalStateUserData) || { storage: {} };
485485

486486
// Update the sync store type
@@ -489,7 +489,7 @@ export class UserDataSyncStoreTypeSynchronizer {
489489
// Write the global state to remote
490490
const machineId = await getServiceMachineId(this.environmentService, this.fileService, this.storageService);
491491
const syncDataToUpdate: ISyncData = { version: GLOBAL_STATE_DATA_VERSION, machineId, content: stringify(remoteGlobalState, false) };
492-
await this.userDataSyncStoreClient.writeResource(SyncResource.GlobalState, JSON.stringify(syncDataToUpdate), globalStateUserData.ref, syncHeaders);
492+
await this.userDataSyncStoreClient.writeResource(SyncResource.GlobalState, JSON.stringify(syncDataToUpdate), globalStateUserData.ref, undefined, syncHeaders);
493493
}
494494

495495
private parseGlobalState({ content }: IUserData): IGlobalState | null {

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,27 @@ export function getLastSyncResourceUri(syncResource: SyncResource, environmentSe
145145
return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`);
146146
}
147147

148+
export type IUserDataResourceManifest = Record<ServerResource, string>;
149+
150+
export interface IUserDataCollectionManifest {
151+
[collectionId: string]: {
152+
readonly latest: IUserDataResourceManifest;
153+
};
154+
}
155+
148156
export interface IUserDataManifest {
149-
readonly latest?: Record<ServerResource, string>;
157+
readonly latest?: IUserDataResourceManifest;
150158
readonly session: string;
151159
readonly ref: string;
160+
readonly collections?: IUserDataCollectionManifest;
152161
}
153162

154163
export interface IResourceRefHandle {
155164
ref: string;
156165
created: number;
157166
}
158167

159-
export type ServerResource = SyncResource | 'machines' | 'editSessions' | 'profiles';
168+
export type ServerResource = SyncResource | 'machines' | 'editSessions';
160169
export type UserDataSyncStoreType = 'insiders' | 'stable';
161170

162171
export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
@@ -179,11 +188,16 @@ export interface IUserDataSyncStoreService {
179188
setAuthToken(token: string, type: string): void;
180189

181190
manifest(oldValue: IUserDataManifest | null, headers?: IHeaders): Promise<IUserDataManifest | null>;
182-
read(resource: ServerResource, oldValue: IUserData | null, profile?: string, headers?: IHeaders): Promise<IUserData>;
183-
write(resource: ServerResource, content: string, ref: string | null, profile?: string, headers?: IHeaders): Promise<string>;
184-
delete(resource: ServerResource, ref: string | null, profile?: string): Promise<void>;
185-
getAllRefs(resource: ServerResource, profile?: string): Promise<IResourceRefHandle[]>;
186-
resolveContent(resource: ServerResource, ref: string, profile?: string, headers?: IHeaders): Promise<string | null>;
191+
readResource(resource: ServerResource, oldValue: IUserData | null, collection?: string, headers?: IHeaders): Promise<IUserData>;
192+
writeResource(resource: ServerResource, content: string, ref: string | null, collection?: string, headers?: IHeaders): Promise<string>;
193+
deleteResource(resource: ServerResource, ref: string | null, collection?: string): Promise<void>;
194+
getAllResourceRefs(resource: ServerResource, collection?: string): Promise<IResourceRefHandle[]>;
195+
resolveResourceContent(resource: ServerResource, ref: string, collection?: string, headers?: IHeaders): Promise<string | null>;
196+
197+
getAllCollections(headers?: IHeaders): Promise<string[]>;
198+
createCollection(headers?: IHeaders): Promise<string>;
199+
deleteCollection(collection?: string, headers?: IHeaders): Promise<void>;
200+
187201
clear(): Promise<void>;
188202
}
189203

@@ -231,6 +245,7 @@ export const enum UserDataSyncErrorCode {
231245
RequestProtocolNotSupported = 'RequestProtocolNotSupported',
232246
RequestPathNotEscaped = 'RequestPathNotEscaped',
233247
RequestHeadersNotObject = 'RequestHeadersNotObject',
248+
NoCollection = 'NoCollection',
234249
NoRef = 'NoRef',
235250
EmptyResponse = 'EmptyResponse',
236251
TurnedOff = 'TurnedOff',

src/vs/platform/userDataSync/common/userDataSyncMachines.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
176176

177177
private async writeMachinesData(machinesData: IMachinesData): Promise<void> {
178178
const content = JSON.stringify(machinesData);
179-
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
179+
const ref = await this.userDataSyncStoreService.writeResource(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
180180
this.userData = { ref, content };
181181
this._onDidChange.fire();
182182
}
@@ -197,7 +197,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
197197
}
198198
}
199199

200-
return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData);
200+
return this.userDataSyncStoreService.readResource(UserDataSyncMachinesService.RESOURCE, this.userData);
201201
}
202202

203203
private parse(userData: IUserData): IMachinesData {

src/vs/platform/userDataSync/common/userDataSyncStoreService.ts

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { Mimes } from 'vs/base/common/mime';
1212
import { isWeb } from 'vs/base/common/platform';
1313
import { ConfigurationSyncStore } from 'vs/base/common/product';
1414
import { joinPath, relativePath } from 'vs/base/common/resources';
15-
import { join } from 'vs/base/common/path';
1615
import { isObject, isString } from 'vs/base/common/types';
1716
import { URI } from 'vs/base/common/uri';
1817
import { generateUuid } from 'vs/base/common/uuid';
@@ -231,12 +230,60 @@ export class UserDataSyncStoreClient extends Disposable {
231230
}
232231
}
233232

234-
async getAllResourceRefs(path: string): Promise<IResourceRefHandle[]> {
233+
// #region Collection
234+
235+
async getAllCollections(headers: IHeaders = {}): Promise<string[]> {
236+
if (!this.userDataSyncStoreUrl) {
237+
throw new Error('No settings sync store url configured.');
238+
}
239+
240+
const url = joinPath(this.userDataSyncStoreUrl, 'collection').toString();
241+
headers = { ...headers };
242+
headers['Content-Type'] = 'application/json';
243+
244+
const context = await this.request(url, { type: 'GET', headers }, [], CancellationToken.None);
245+
246+
return (await asJson<string[]>(context)) || [];
247+
}
248+
249+
async createCollection(headers: IHeaders = {}): Promise<string> {
250+
if (!this.userDataSyncStoreUrl) {
251+
throw new Error('No settings sync store url configured.');
252+
}
253+
254+
const url = joinPath(this.userDataSyncStoreUrl, 'collection').toString();
255+
headers = { ...headers };
256+
headers['Content-Type'] = Mimes.text;
257+
258+
const context = await this.request(url, { type: 'POST', headers }, [], CancellationToken.None);
259+
const collectionId = await asTextOrError(context);
260+
if (!collectionId) {
261+
throw new UserDataSyncStoreError('Server did not return the collection id', url, UserDataSyncErrorCode.NoCollection, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]);
262+
}
263+
return collectionId;
264+
}
265+
266+
async deleteCollection(collection?: string, headers: IHeaders = {}): Promise<void> {
267+
if (!this.userDataSyncStoreUrl) {
268+
throw new Error('No settings sync store url configured.');
269+
}
270+
271+
const url = collection ? joinPath(this.userDataSyncStoreUrl, 'collection', collection).toString() : joinPath(this.userDataSyncStoreUrl, 'collection').toString();
272+
headers = { ...headers };
273+
274+
await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None);
275+
}
276+
277+
// #endregion
278+
279+
// #region Resource
280+
281+
async getAllResourceRefs(resource: ServerResource, collection?: string): Promise<IResourceRefHandle[]> {
235282
if (!this.userDataSyncStoreUrl) {
236283
throw new Error('No settings sync store url configured.');
237284
}
238285

239-
const uri = joinPath(this.userDataSyncStoreUrl, 'resource', path);
286+
const uri = this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource);
240287
const headers: IHeaders = {};
241288

242289
const context = await this.request(uri.toString(), { type: 'GET', headers }, [], CancellationToken.None);
@@ -245,12 +292,12 @@ export class UserDataSyncStoreClient extends Disposable {
245292
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
246293
}
247294

248-
async resolveResourceContent(path: string, ref: string, headers: IHeaders = {}): Promise<string | null> {
295+
async resolveResourceContent(resource: ServerResource, ref: string, collection?: string, headers: IHeaders = {}): Promise<string | null> {
249296
if (!this.userDataSyncStoreUrl) {
250297
throw new Error('No settings sync store url configured.');
251298
}
252299

253-
const url = joinPath(this.userDataSyncStoreUrl, 'resource', path, ref).toString();
300+
const url = joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), ref).toString();
254301
headers = { ...headers };
255302
headers['Cache-Control'] = 'no-cache';
256303

@@ -259,23 +306,34 @@ export class UserDataSyncStoreClient extends Disposable {
259306
return content;
260307
}
261308

262-
async deleteResource(path: string, ref: string | null): Promise<void> {
309+
async deleteResource(resource: ServerResource, ref: string | null, collection?: string): Promise<void> {
263310
if (!this.userDataSyncStoreUrl) {
264311
throw new Error('No settings sync store url configured.');
265312
}
266313

267-
const url = ref !== null ? joinPath(this.userDataSyncStoreUrl, 'resource', path, ref).toString() : joinPath(this.userDataSyncStoreUrl, 'resource', path).toString();
314+
const url = ref !== null ? joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), ref).toString() : this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource).toString();
268315
const headers: IHeaders = {};
269316

270317
await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None);
271318
}
272319

273-
async readResource(path: string, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
320+
async deleteResources(): Promise<void> {
321+
if (!this.userDataSyncStoreUrl) {
322+
throw new Error('No settings sync store url configured.');
323+
}
324+
325+
const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString();
326+
const headers: IHeaders = { 'Content-Type': Mimes.text };
327+
328+
await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None);
329+
}
330+
331+
async readResource(resource: ServerResource, oldValue: IUserData | null, collection?: string, headers: IHeaders = {}): Promise<IUserData> {
274332
if (!this.userDataSyncStoreUrl) {
275333
throw new Error('No settings sync store url configured.');
276334
}
277335

278-
const url = joinPath(this.userDataSyncStoreUrl, 'resource', path, 'latest').toString();
336+
const url = joinPath(this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource), 'latest').toString();
279337
headers = { ...headers };
280338
// Disable caching as they are cached by synchronisers
281339
headers['Cache-Control'] = 'no-cache';
@@ -307,12 +365,12 @@ export class UserDataSyncStoreClient extends Disposable {
307365
return userData;
308366
}
309367

310-
async writeResource(path: string, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
368+
async writeResource(resource: ServerResource, data: string, ref: string | null, collection?: string, headers: IHeaders = {}): Promise<string> {
311369
if (!this.userDataSyncStoreUrl) {
312370
throw new Error('No settings sync store url configured.');
313371
}
314372

315-
const url = joinPath(this.userDataSyncStoreUrl, 'resource', path).toString();
373+
const url = this.getResourceUrl(this.userDataSyncStoreUrl, collection, resource).toString();
316374
headers = { ...headers };
317375
headers['Content-Type'] = Mimes.text;
318376
if (ref) {
@@ -328,6 +386,8 @@ export class UserDataSyncStoreClient extends Disposable {
328386
return newRef;
329387
}
330388

389+
// #endregion
390+
331391
async manifest(oldValue: IUserDataManifest | null, headers: IHeaders = {}): Promise<IUserDataManifest | null> {
332392
if (!this.userDataSyncStoreUrl) {
333393
throw new Error('No settings sync store url configured.');
@@ -388,15 +448,17 @@ export class UserDataSyncStoreClient extends Disposable {
388448
throw new Error('No settings sync store url configured.');
389449
}
390450

391-
const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString();
392-
const headers: IHeaders = { 'Content-Type': Mimes.text };
393-
394-
await this.request(url, { type: 'DELETE', headers }, [], CancellationToken.None);
451+
await this.deleteResources();
452+
await this.deleteCollection();
395453

396454
// clear cached session.
397455
this.clearSession();
398456
}
399457

458+
private getResourceUrl(userDataSyncStoreUrl: URI, collection: string | undefined, resource: ServerResource): URI {
459+
return collection ? joinPath(userDataSyncStoreUrl, 'collection', collection, 'resource', resource) : joinPath(userDataSyncStoreUrl, 'resource', resource);
460+
}
461+
400462
private clearSession(): void {
401463
this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.APPLICATION);
402464
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.APPLICATION);
@@ -551,32 +613,6 @@ export class UserDataSyncStoreService extends UserDataSyncStoreClient implements
551613
this._register(userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => this.updateUserDataSyncStoreUrl(userDataSyncStoreManagementService.userDataSyncStore?.url)));
552614
}
553615

554-
getAllRefs(resource: ServerResource, profile?: string): Promise<IResourceRefHandle[]> {
555-
return this.getAllResourceRefs(profile ? this.getProfileResource(resource, profile) : resource);
556-
}
557-
558-
read(resource: ServerResource, oldValue: IUserData | null, profile?: string, headers?: IHeaders): Promise<IUserData> {
559-
return this.readResource(profile ? this.getProfileResource(resource, profile) : resource, oldValue, headers);
560-
}
561-
562-
write(resource: ServerResource, content: string, ref: string | null, profile?: string, headers?: IHeaders): Promise<string> {
563-
return this.writeResource(profile ? this.getProfileResource(resource, profile) : resource, content, ref, headers);
564-
}
565-
566-
delete(resource: ServerResource, ref: string | null, profile?: string): Promise<void> {
567-
return this.deleteResource(profile ? this.getProfileResource(resource, profile) : resource, ref);
568-
}
569-
570-
resolveContent(resource: ServerResource, ref: string, profile?: string, headers?: IHeaders): Promise<string | null> {
571-
return this.resolveResourceContent(profile ? this.getProfileResource(resource, profile) : resource, ref, headers);
572-
}
573-
574-
private getProfileResource(resource: ServerResource, profile: string): string {
575-
if (resource === 'profiles') {
576-
throw new Error(`Invalid Resource Argument: ${resource}`);
577-
}
578-
return join('profiles', profile, resource);
579-
}
580616
}
581617

582618
export class RequestsSession {

src/vs/platform/userDataSync/test/common/userDataSyncClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export class UserDataSyncClient extends Disposable {
140140
}
141141

142142
read(resource: SyncResource): Promise<IUserData> {
143-
return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null);
143+
return this.instantiationService.get(IUserDataSyncStoreService).readResource(resource, null);
144144
}
145145

146146
manifest(): Promise<IUserDataManifest | null> {
@@ -215,6 +215,9 @@ export class UserDataSyncTestServer implements IRequestService {
215215
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') {
216216
return this.clear(options.headers);
217217
}
218+
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'collection') {
219+
return this.toResponse(204);
220+
}
218221
return this.toResponse(501);
219222
}
220223

src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ suite('UserDataSyncService', () => {
332332
assert.deepStrictEqual(target.requests, [
333333
// Manifest
334334
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
335+
{ type: 'DELETE', url: `${target.url}/v1/collection`, headers: {} },
335336
]);
336337

337338
});

0 commit comments

Comments
 (0)