mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-29 01:25:37 -05:00
233 lines
9.7 KiB
TypeScript
233 lines
9.7 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { Disposable, } from 'vs/base/common/lifecycle';
|
|
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
|
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
|
|
import { joinPath, relativePath } from 'vs/base/common/resources';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
|
|
import { IProductService } from 'vs/platform/product/common/productService';
|
|
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
|
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
|
import { IFileService } from 'vs/platform/files/common/files';
|
|
import { IStorageService } from 'vs/platform/storage/common/storage';
|
|
import { assign } from 'vs/base/common/objects';
|
|
|
|
|
|
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
|
|
|
|
_serviceBrand: any;
|
|
|
|
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
|
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
|
|
|
|
constructor(
|
|
@IProductService productService: IProductService,
|
|
@IConfigurationService configurationService: IConfigurationService,
|
|
@IRequestService private readonly requestService: IRequestService,
|
|
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
|
|
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
|
@IEnvironmentService environmentService: IEnvironmentService,
|
|
@IFileService fileService: IFileService,
|
|
@IStorageService storageService: IStorageService,
|
|
) {
|
|
super();
|
|
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
|
|
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
|
|
.then(uuid => {
|
|
const headers: IHeaders = {
|
|
'X-Sync-Client-Id': productService.version,
|
|
};
|
|
headers['X-Sync-Machine-Id'] = uuid;
|
|
return headers;
|
|
});
|
|
}
|
|
|
|
async getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const uri = joinPath(this.userDataSyncStore.url, 'resource', resource);
|
|
const headers: IHeaders = {};
|
|
|
|
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None);
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
|
|
}
|
|
|
|
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
|
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
|
|
}
|
|
|
|
async resolveContent(resource: SyncResource, ref: string): Promise<string | null> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString();
|
|
const headers: IHeaders = {};
|
|
headers['Cache-Control'] = 'no-cache';
|
|
|
|
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
|
|
}
|
|
|
|
const content = await asText(context);
|
|
return content;
|
|
}
|
|
|
|
async delete(resource: SyncResource): Promise<void> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
|
|
const headers: IHeaders = {};
|
|
|
|
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
|
|
}
|
|
}
|
|
|
|
async read(resource: SyncResource, oldValue: IUserData | null): Promise<IUserData> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString();
|
|
const headers: IHeaders = {};
|
|
// Disable caching as they are cached by synchronisers
|
|
headers['Cache-Control'] = 'no-cache';
|
|
if (oldValue) {
|
|
headers['If-None-Match'] = oldValue.ref;
|
|
}
|
|
|
|
const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None);
|
|
|
|
if (context.res.statusCode === 304) {
|
|
// There is no new value. Hence return the old value.
|
|
return oldValue!;
|
|
}
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
|
|
}
|
|
|
|
const ref = context.res.headers['etag'];
|
|
if (!ref) {
|
|
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
|
|
}
|
|
const content = await asText(context);
|
|
return { ref, content };
|
|
}
|
|
|
|
async write(resource: SyncResource, data: string, ref: string | null): Promise<string> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
|
|
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
|
if (ref) {
|
|
headers['If-Match'] = ref;
|
|
}
|
|
|
|
const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None);
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
|
|
}
|
|
|
|
const newRef = context.res.headers['etag'];
|
|
if (!newRef) {
|
|
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
|
|
}
|
|
return newRef;
|
|
}
|
|
|
|
async manifest(): Promise<IUserDataManifest | null> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
|
|
const headers: IHeaders = { 'Content-Type': 'application/json' };
|
|
|
|
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
|
}
|
|
|
|
return asJson(context);
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
if (!this.userDataSyncStore) {
|
|
throw new Error('No settings sync store url configured.');
|
|
}
|
|
|
|
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
|
|
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
|
|
|
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
|
|
|
|
if (!isSuccess(context)) {
|
|
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
|
}
|
|
}
|
|
|
|
private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise<IRequestContext> {
|
|
const authToken = await this.authTokenService.getToken();
|
|
if (!authToken) {
|
|
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source);
|
|
}
|
|
|
|
const commonHeaders = await this.commonHeadersPromise;
|
|
options.headers = assign(options.headers || {}, commonHeaders, {
|
|
'X-Account-Type': authToken.authenticationProviderId,
|
|
'authorization': `Bearer ${authToken.token}`,
|
|
});
|
|
|
|
this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } });
|
|
|
|
let context;
|
|
try {
|
|
context = await this.requestService.request(options, token);
|
|
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
|
|
} catch (e) {
|
|
throw new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
|
|
}
|
|
|
|
if (context.res.statusCode === 401) {
|
|
this.authTokenService.sendTokenFailed();
|
|
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
|
|
}
|
|
|
|
if (context.res.statusCode === 403) {
|
|
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source);
|
|
}
|
|
|
|
if (context.res.statusCode === 412) {
|
|
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed, source);
|
|
}
|
|
|
|
if (context.res.statusCode === 413) {
|
|
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source);
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
}
|