mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 02:32:35 -05:00
* Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463 * fix config changes * fix strictnull checks
461 lines
14 KiB
TypeScript
461 lines
14 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 { join } from 'vs/base/common/path';
|
|
import { joinPath } from 'vs/base/common/resources';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { hash } from 'vs/base/common/hash';
|
|
import { coalesce } from 'vs/base/common/arrays';
|
|
import { equals, deepClone } from 'vs/base/common/objects';
|
|
import { ResourceQueue } from 'vs/base/common/async';
|
|
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
|
|
import { IFileService } from 'vs/platform/files/common/files';
|
|
import { ITextSnapshot } from 'vs/editor/common/model';
|
|
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
|
import { keys, ResourceMap } from 'vs/base/common/map';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
|
import { VSBuffer } from 'vs/base/common/buffer';
|
|
import { TextSnapshotReadable } from 'vs/workbench/services/textfile/common/textfiles';
|
|
|
|
export interface IBackupFilesModel {
|
|
resolve(backupRoot: URI): Promise<IBackupFilesModel>;
|
|
|
|
add(resource: URI, versionId?: number, meta?: object): void;
|
|
has(resource: URI, versionId?: number, meta?: object): boolean;
|
|
get(): URI[];
|
|
remove(resource: URI): void;
|
|
count(): number;
|
|
clear(): void;
|
|
}
|
|
|
|
interface IBackupCacheEntry {
|
|
versionId?: number;
|
|
meta?: object;
|
|
}
|
|
|
|
export class BackupFilesModel implements IBackupFilesModel {
|
|
private cache: ResourceMap<IBackupCacheEntry> = new ResourceMap();
|
|
|
|
constructor(private fileService: IFileService) { }
|
|
|
|
async resolve(backupRoot: URI): Promise<IBackupFilesModel> {
|
|
try {
|
|
const backupRootStat = await this.fileService.resolve(backupRoot);
|
|
if (backupRootStat.children) {
|
|
await Promise.all(backupRootStat.children
|
|
.filter(child => child.isDirectory)
|
|
.map(async backupSchema => {
|
|
|
|
// Read backup directory for backups
|
|
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
|
|
|
// Remember known backups in our caches
|
|
if (backupSchemaStat.children) {
|
|
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
|
}
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
// ignore any errors
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
add(resource: URI, versionId = 0, meta?: object): void {
|
|
this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache...
|
|
}
|
|
|
|
count(): number {
|
|
return this.cache.size;
|
|
}
|
|
|
|
has(resource: URI, versionId?: number, meta?: object): boolean {
|
|
const entry = this.cache.get(resource);
|
|
if (!entry) {
|
|
return false; // unknown resource
|
|
}
|
|
|
|
if (typeof versionId === 'number' && versionId !== entry.versionId) {
|
|
return false; // different versionId
|
|
}
|
|
|
|
if (meta && !equals(meta, entry.meta)) {
|
|
return false; // different metadata
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
get(): URI[] {
|
|
return this.cache.keys();
|
|
}
|
|
|
|
remove(resource: URI): void {
|
|
this.cache.delete(resource);
|
|
}
|
|
|
|
clear(): void {
|
|
this.cache.clear();
|
|
}
|
|
}
|
|
|
|
export class BackupFileService implements IBackupFileService {
|
|
|
|
_serviceBrand: undefined;
|
|
|
|
private impl: IBackupFileService;
|
|
|
|
constructor(
|
|
@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService,
|
|
@IFileService protected fileService: IFileService
|
|
) {
|
|
this.impl = this.initialize();
|
|
}
|
|
|
|
protected hashPath(resource: URI): string {
|
|
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
|
|
|
return hash(str).toString(16);
|
|
}
|
|
|
|
private initialize(): IBackupFileService {
|
|
const backupWorkspaceResource = this.environmentService.configuration.backupWorkspaceResource;
|
|
if (backupWorkspaceResource) {
|
|
return new BackupFileServiceImpl(backupWorkspaceResource, this.hashPath, this.fileService);
|
|
}
|
|
|
|
return new InMemoryBackupFileService(this.hashPath);
|
|
}
|
|
|
|
reinitialize(): void {
|
|
|
|
// Re-init implementation (unless we are running in-memory)
|
|
if (this.impl instanceof BackupFileServiceImpl) {
|
|
const backupWorkspaceResource = this.environmentService.configuration.backupWorkspaceResource;
|
|
if (backupWorkspaceResource) {
|
|
this.impl.initialize(backupWorkspaceResource);
|
|
} else {
|
|
this.impl = new InMemoryBackupFileService(this.hashPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
hasBackups(): Promise<boolean> {
|
|
return this.impl.hasBackups();
|
|
}
|
|
|
|
hasBackupSync(resource: URI, versionId?: number): boolean {
|
|
return this.impl.hasBackupSync(resource, versionId);
|
|
}
|
|
|
|
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
|
return this.impl.loadBackupResource(resource);
|
|
}
|
|
|
|
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
|
return this.impl.backupResource(resource, content, versionId, meta);
|
|
}
|
|
|
|
discardResourceBackup(resource: URI): Promise<void> {
|
|
return this.impl.discardResourceBackup(resource);
|
|
}
|
|
|
|
discardAllWorkspaceBackups(): Promise<void> {
|
|
return this.impl.discardAllWorkspaceBackups();
|
|
}
|
|
|
|
getWorkspaceFileBackups(): Promise<URI[]> {
|
|
return this.impl.getWorkspaceFileBackups();
|
|
}
|
|
|
|
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
|
return this.impl.resolveBackupContent(backup);
|
|
}
|
|
|
|
toBackupResource(resource: URI): URI {
|
|
return this.impl.toBackupResource(resource);
|
|
}
|
|
}
|
|
|
|
class BackupFileServiceImpl implements IBackupFileService {
|
|
|
|
private static readonly PREAMBLE_END_MARKER = '\n';
|
|
private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator
|
|
private static readonly PREAMBLE_MAX_LENGTH = 10000;
|
|
|
|
_serviceBrand: undefined;
|
|
|
|
private backupWorkspacePath!: URI;
|
|
|
|
private isShuttingDown: boolean;
|
|
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
|
|
|
private ready!: Promise<IBackupFilesModel>;
|
|
private model!: IBackupFilesModel;
|
|
|
|
constructor(
|
|
backupWorkspaceResource: URI,
|
|
private readonly hashPath: (resource: URI) => string,
|
|
@IFileService private readonly fileService: IFileService
|
|
) {
|
|
this.isShuttingDown = false;
|
|
this.ioOperationQueues = new ResourceQueue();
|
|
|
|
this.initialize(backupWorkspaceResource);
|
|
}
|
|
|
|
initialize(backupWorkspaceResource: URI): void {
|
|
this.backupWorkspacePath = backupWorkspaceResource;
|
|
|
|
this.ready = this.doInitialize();
|
|
}
|
|
|
|
private doInitialize(): Promise<IBackupFilesModel> {
|
|
this.model = new BackupFilesModel(this.fileService);
|
|
|
|
return this.model.resolve(this.backupWorkspacePath);
|
|
}
|
|
|
|
async hasBackups(): Promise<boolean> {
|
|
const model = await this.ready;
|
|
|
|
return model.count() > 0;
|
|
}
|
|
|
|
hasBackupSync(resource: URI, versionId?: number): boolean {
|
|
const backupResource = this.toBackupResource(resource);
|
|
|
|
return this.model.has(backupResource, versionId);
|
|
}
|
|
|
|
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
|
const model = await this.ready;
|
|
|
|
// Return directly if we have a known backup with that resource
|
|
const backupResource = this.toBackupResource(resource);
|
|
if (model.has(backupResource)) {
|
|
return backupResource;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
|
if (this.isShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
const model = await this.ready;
|
|
|
|
const backupResource = this.toBackupResource(resource);
|
|
if (model.has(backupResource, versionId, meta)) {
|
|
return; // return early if backup version id matches requested one
|
|
}
|
|
|
|
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
|
let preamble: string | undefined = undefined;
|
|
|
|
// With Metadata: URI + META-START + Meta + END
|
|
if (meta) {
|
|
const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
|
if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) {
|
|
preamble = preambleWithMeta;
|
|
}
|
|
}
|
|
|
|
// Without Metadata: URI + END
|
|
if (!preamble) {
|
|
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
|
}
|
|
|
|
// Update content with value
|
|
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
|
|
|
// Update model
|
|
model.add(backupResource, versionId, meta);
|
|
});
|
|
}
|
|
|
|
async discardResourceBackup(resource: URI): Promise<void> {
|
|
const model = await this.ready;
|
|
const backupResource = this.toBackupResource(resource);
|
|
|
|
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
|
await this.fileService.del(backupResource, { recursive: true });
|
|
|
|
model.remove(backupResource);
|
|
});
|
|
}
|
|
|
|
async discardAllWorkspaceBackups(): Promise<void> {
|
|
this.isShuttingDown = true;
|
|
|
|
const model = await this.ready;
|
|
|
|
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
|
|
|
model.clear();
|
|
}
|
|
|
|
async getWorkspaceFileBackups(): Promise<URI[]> {
|
|
const model = await this.ready;
|
|
|
|
const backups = await Promise.all(model.get().map(async fileBackup => {
|
|
const backupPreamble = await this.readToMatchingString(fileBackup, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
|
|
if (!backupPreamble) {
|
|
return undefined;
|
|
}
|
|
|
|
// Preamble with metadata: URI + META-START + Meta + END
|
|
const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
|
if (metaStartIndex > 0) {
|
|
return URI.parse(backupPreamble.substring(0, metaStartIndex));
|
|
}
|
|
|
|
// Preamble without metadata: URI + END
|
|
else {
|
|
return URI.parse(backupPreamble);
|
|
}
|
|
}));
|
|
|
|
return coalesce(backups);
|
|
}
|
|
|
|
private async readToMatchingString(file: URI, matchingString: string, maximumBytesToRead: number): Promise<string> {
|
|
const contents = (await this.fileService.readFile(file, { length: maximumBytesToRead })).value.toString();
|
|
|
|
const newLineIndex = contents.indexOf(matchingString);
|
|
if (newLineIndex >= 0) {
|
|
return contents.substr(0, newLineIndex);
|
|
}
|
|
|
|
throw new Error(`Backup: Could not find ${JSON.stringify(matchingString)} in first ${maximumBytesToRead} bytes of ${file}`);
|
|
}
|
|
|
|
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
|
|
|
// Metadata extraction
|
|
let metaRaw = '';
|
|
let metaEndFound = false;
|
|
|
|
// Add a filter method to filter out everything until the meta end marker
|
|
const metaPreambleFilter = (chunk: VSBuffer) => {
|
|
const chunkString = chunk.toString();
|
|
|
|
if (!metaEndFound) {
|
|
const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER);
|
|
if (metaEndIndex === -1) {
|
|
metaRaw += chunkString;
|
|
|
|
return VSBuffer.fromString(''); // meta not yet found, return empty string
|
|
}
|
|
|
|
metaEndFound = true;
|
|
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
|
|
|
return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after
|
|
}
|
|
|
|
return chunk;
|
|
};
|
|
|
|
// Read backup into factory
|
|
const content = await this.fileService.readFileStream(backup);
|
|
const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
|
|
|
|
// Extract meta data (if any)
|
|
let meta: T | undefined;
|
|
const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
|
if (metaStartIndex !== -1) {
|
|
try {
|
|
meta = JSON.parse(metaRaw.substr(metaStartIndex + 1));
|
|
} catch (error) {
|
|
// ignore JSON parse errors
|
|
}
|
|
}
|
|
|
|
// We have seen reports (e.g. https://github.com/microsoft/vscode/issues/78500) where
|
|
// if VSCode goes down while writing the backup file, the file can turn empty because
|
|
// it always first gets truncated and then written to. In this case, we will not find
|
|
// the meta-end marker ('\n') and as such the backup can only be invalid. We bail out
|
|
// here if that is the case.
|
|
if (!metaEndFound) {
|
|
throw new Error(`Backup: Could not find meta end marker in ${backup}. The file is probably corrupt.`);
|
|
}
|
|
|
|
return { value: factory, meta };
|
|
}
|
|
|
|
toBackupResource(resource: URI): URI {
|
|
return joinPath(this.backupWorkspacePath, resource.scheme, this.hashPath(resource));
|
|
}
|
|
}
|
|
|
|
export class InMemoryBackupFileService implements IBackupFileService {
|
|
|
|
_serviceBrand: undefined;
|
|
|
|
private backups: Map<string, ITextSnapshot> = new Map();
|
|
|
|
constructor(private readonly hashPath: (resource: URI) => string) { }
|
|
|
|
hasBackups(): Promise<boolean> {
|
|
return Promise.resolve(this.backups.size > 0);
|
|
}
|
|
|
|
hasBackupSync(resource: URI, versionId?: number): boolean {
|
|
const backupResource = this.toBackupResource(resource);
|
|
|
|
return this.backups.has(backupResource.toString());
|
|
}
|
|
|
|
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
|
const backupResource = this.toBackupResource(resource);
|
|
if (this.backups.has(backupResource.toString())) {
|
|
return Promise.resolve(backupResource);
|
|
}
|
|
|
|
return Promise.resolve(undefined);
|
|
}
|
|
|
|
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
|
const backupResource = this.toBackupResource(resource);
|
|
this.backups.set(backupResource.toString(), content);
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
|
|
const snapshot = this.backups.get(backupResource.toString());
|
|
if (snapshot) {
|
|
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
|
}
|
|
|
|
return Promise.reject('Unexpected backup resource to resolve');
|
|
}
|
|
|
|
getWorkspaceFileBackups(): Promise<URI[]> {
|
|
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
|
}
|
|
|
|
discardResourceBackup(resource: URI): Promise<void> {
|
|
this.backups.delete(this.toBackupResource(resource).toString());
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
discardAllWorkspaceBackups(): Promise<void> {
|
|
this.backups.clear();
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
toBackupResource(resource: URI): URI {
|
|
return URI.file(join(resource.scheme, this.hashPath(resource)));
|
|
}
|
|
}
|