Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -1,200 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IStorage, StorageService } from 'vs/platform/storage/common/storageService';
import { endsWith, startsWith, rtrim } from 'vs/base/common/strings';
import URI from 'vs/base/common/uri';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
/**
* We currently store local storage with the following format:
*
* [Global]
* storage://global/<key>
*
* [Workspace]
* storage://workspace/<folder>/<key>
* storage://workspace/empty:<id>/<key>
* storage://workspace/root:<id>/<key>
*
* <folder>
* macOS/Linux: /some/folder/path
* Windows: c%3A/Users/name/folder (normal path)
* file://localhost/c%24/name/folder (unc path)
*
* [no workspace]
* storage://workspace/__$noWorkspace__<key>
* => no longer being used (used for empty workspaces previously)
*/
const EMPTY_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/empty:`;
const MULTI_ROOT_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/root:`;
export type StorageObject = { [key: string]: string };
export interface IParsedStorage {
global: Map<string, string>;
multiRoot: Map<string, StorageObject>;
folder: Map<string, StorageObject>;
empty: Map<string, StorageObject>;
}
/**
* Parses the local storage implementation into global, multi root, folder and empty storage.
*/
export function parseStorage(storage: IStorage): IParsedStorage {
const globalStorage = new Map<string, string>();
const folderWorkspacesStorage = new Map<string /* workspace file resource */, StorageObject>();
const emptyWorkspacesStorage = new Map<string /* empty workspace id */, StorageObject>();
const multiRootWorkspacesStorage = new Map<string /* multi root workspace id */, StorageObject>();
const workspaces: { prefix: string; resource: string; }[] = [];
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
// Workspace Storage (storage://workspace/)
if (startsWith(key, StorageService.WORKSPACE_PREFIX)) {
// We are looking for key: storage://workspace/<folder>/workspaceIdentifier to be able to find all folder
// paths that are known to the storage. is the only way how to parse all folder paths known in storage.
if (endsWith(key, StorageService.WORKSPACE_IDENTIFIER)) {
// storage://workspace/<folder>/workspaceIdentifier => <folder>/
let workspace = key.substring(StorageService.WORKSPACE_PREFIX.length, key.length - StorageService.WORKSPACE_IDENTIFIER.length);
// macOS/Unix: Users/name/folder/
// Windows: c%3A/Users/name/folder/
if (!startsWith(workspace, 'file:')) {
workspace = `file:///${rtrim(workspace, '/')}`;
}
// Windows UNC path: file://localhost/c%3A/Users/name/folder/
else {
workspace = rtrim(workspace, '/');
}
// storage://workspace/<folder>/workspaceIdentifier => storage://workspace/<folder>/
const prefix = key.substr(0, key.length - StorageService.WORKSPACE_IDENTIFIER.length);
workspaces.push({ prefix, resource: workspace });
}
// Empty workspace key: storage://workspace/empty:<id>/<key>
else if (startsWith(key, EMPTY_WORKSPACE_PREFIX)) {
// storage://workspace/empty:<id>/<key> => <id>
const emptyWorkspaceId = key.substring(EMPTY_WORKSPACE_PREFIX.length, key.indexOf('/', EMPTY_WORKSPACE_PREFIX.length));
const emptyWorkspaceResource = URI.from({ path: emptyWorkspaceId, scheme: 'empty' }).toString();
let emptyWorkspaceStorage = emptyWorkspacesStorage.get(emptyWorkspaceResource);
if (!emptyWorkspaceStorage) {
emptyWorkspaceStorage = Object.create(null);
emptyWorkspacesStorage.set(emptyWorkspaceResource, emptyWorkspaceStorage);
}
// storage://workspace/empty:<id>/someKey => someKey
const storageKey = key.substr(EMPTY_WORKSPACE_PREFIX.length + emptyWorkspaceId.length + 1 /* trailing / */);
emptyWorkspaceStorage[storageKey] = storage.getItem(key);
}
// Multi root workspace key: storage://workspace/root:<id>/<key>
else if (startsWith(key, MULTI_ROOT_WORKSPACE_PREFIX)) {
// storage://workspace/root:<id>/<key> => <id>
const multiRootWorkspaceId = key.substring(MULTI_ROOT_WORKSPACE_PREFIX.length, key.indexOf('/', MULTI_ROOT_WORKSPACE_PREFIX.length));
const multiRootWorkspaceResource = URI.from({ path: multiRootWorkspaceId, scheme: 'root' }).toString();
let multiRootWorkspaceStorage = multiRootWorkspacesStorage.get(multiRootWorkspaceResource);
if (!multiRootWorkspaceStorage) {
multiRootWorkspaceStorage = Object.create(null);
multiRootWorkspacesStorage.set(multiRootWorkspaceResource, multiRootWorkspaceStorage);
}
// storage://workspace/root:<id>/someKey => someKey
const storageKey = key.substr(MULTI_ROOT_WORKSPACE_PREFIX.length + multiRootWorkspaceId.length + 1 /* trailing / */);
multiRootWorkspaceStorage[storageKey] = storage.getItem(key);
}
}
// Global Storage (storage://global)
else if (startsWith(key, StorageService.GLOBAL_PREFIX)) {
// storage://global/someKey => someKey
const globalStorageKey = key.substr(StorageService.GLOBAL_PREFIX.length);
if (startsWith(globalStorageKey, StorageService.COMMON_PREFIX)) {
continue; // filter out faulty keys that have the form storage://something/storage://
}
globalStorage.set(globalStorageKey, storage.getItem(key));
}
}
// With all the folder paths known we can now extract storage for each path. We have to go through all workspaces
// from the longest path first to reliably extract the storage. The reason is that one folder path can be a parent
// of another folder path and as such a simple indexOf check is not enough.
const workspacesByLength = workspaces.sort((w1, w2) => w1.prefix.length >= w2.prefix.length ? -1 : 1);
const handledKeys = new Map<string, boolean>();
workspacesByLength.forEach(workspace => {
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (handledKeys.has(key) || !startsWith(key, workspace.prefix)) {
continue; // not part of workspace prefix or already handled
}
handledKeys.set(key, true);
let folderWorkspaceStorage = folderWorkspacesStorage.get(workspace.resource);
if (!folderWorkspaceStorage) {
folderWorkspaceStorage = Object.create(null);
folderWorkspacesStorage.set(workspace.resource, folderWorkspaceStorage);
}
// storage://workspace/<folder>/someKey => someKey
const storageKey = key.substr(workspace.prefix.length);
folderWorkspaceStorage[storageKey] = storage.getItem(key);
}
});
return {
global: globalStorage,
multiRoot: multiRootWorkspacesStorage,
folder: folderWorkspacesStorage,
empty: emptyWorkspacesStorage
};
}
export function migrateStorageToMultiRootWorkspace(fromWorkspaceId: string, toWorkspace: IWorkspaceIdentifier, storage: IStorage): string {
const parsed = parseStorage(storage);
const newWorkspaceId = URI.from({ path: toWorkspace.id, scheme: 'root' }).toString();
// Find in which location the workspace storage is to be migrated from
let storageForWorkspace: StorageObject;
if (parsed.multiRoot.has(fromWorkspaceId)) {
storageForWorkspace = parsed.multiRoot.get(fromWorkspaceId);
} else if (parsed.empty.has(fromWorkspaceId)) {
storageForWorkspace = parsed.empty.get(fromWorkspaceId);
} else if (parsed.folder.has(fromWorkspaceId)) {
storageForWorkspace = parsed.folder.get(fromWorkspaceId);
}
// Migrate existing storage to new workspace id
if (storageForWorkspace) {
Object.keys(storageForWorkspace).forEach(key => {
if (key === StorageService.WORKSPACE_IDENTIFIER) {
return; // make sure to never migrate the workspace identifier
}
storage.setItem(`${StorageService.WORKSPACE_PREFIX}${newWorkspaceId}/${key}`, storageForWorkspace[key]);
});
}
return newWorkspaceId;
}

View File

@@ -2,62 +2,96 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
export const ID = 'storageService';
export const IStorageService = createDecorator<IStorageService>('storageService');
export const IStorageService = createDecorator<IStorageService>(ID);
export enum WillSaveStateReason {
NONE = 0,
SHUTDOWN = 1
}
export interface IWillSaveStateEvent {
reason: WillSaveStateReason;
}
export interface IStorageService {
_serviceBrand: any;
/**
* Store a string value under the given key to local storage.
*
* The optional scope argument allows to define the scope of the operation.
* Emitted whenever data is updated or deleted.
*/
store(key: string, value: any, scope?: StorageScope): void;
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent>;
/**
* Delete an element stored under the provided key from local storage.
* Emitted when the storage is about to persist. This is the right time
* to persist data to ensure it is stored before the application shuts
* down.
*
* The optional scope argument allows to define the scope of the operation.
* The will save state event allows to optionally ask for the reason of
* saving the state, e.g. to find out if the state is saved due to a
* shutdown.
*/
remove(key: string, scope?: StorageScope): void;
readonly onWillSaveState: Event<IWillSaveStateEvent>;
/**
* Retrieve an element stored with the given key from local storage. Use
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*
* The optional scope argument allows to define the scope of the operation.
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
*/
get(key: string, scope?: StorageScope, defaultValue?: string): string;
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
/**
* Retrieve an element stored with the given key from local storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*
* The optional scope argument allows to define the scope of the operation.
*/
getInteger(key: string, scope?: StorageScope, defaultValue?: number): number;
/**
* Retrieve an element stored with the given key from local storage. Use
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
*
* The optional scope argument allows to define the scope of the operation.
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
*/
getBoolean(key: string, scope?: StorageScope, defaultValue?: boolean): boolean;
}
export enum StorageScope {
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
/**
* The stored data will be scoped to all workspaces of this domain.
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
*/
getInteger<R extends number | undefined>(key: string, scope: StorageScope, fallbackValue: number): number;
getInteger<R extends number | undefined>(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
*/
store(key: string, value: any, scope: StorageScope): void;
/**
* Delete an element stored under the provided key from storage.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
*/
remove(key: string, scope: StorageScope): void;
}
export const enum StorageScope {
/**
* The stored data will be scoped to all workspaces.
*/
GLOBAL,
@@ -67,12 +101,93 @@ export enum StorageScope {
WORKSPACE
}
export interface IWorkspaceStorageChangeEvent {
key: string;
scope: StorageScope;
}
export const NullStorageService: IStorageService = {
_serviceBrand: undefined,
store() { return undefined; },
remove() { return undefined; },
get(a, b, defaultValue) { return defaultValue; },
getInteger(a, b, defaultValue) { return defaultValue; },
getBoolean(a, b, defaultValue) { return defaultValue; }
};
export class InMemoryStorageService extends Disposable implements IStorageService {
_serviceBrand = undefined;
private _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
readonly onWillSaveState = Event.None;
private globalCache: Map<string, string> = new Map<string, string>();
private workspaceCache: Map<string, string> = new Map<string, string>();
private getCache(scope: StorageScope): Map<string, string> {
return scope === StorageScope.GLOBAL ? this.globalCache : this.workspaceCache;
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getInteger(key: string, scope: StorageScope, fallbackValue: number): number;
getInteger(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
store(key: string, value: any, scope: StorageScope): Promise<void> {
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.remove(key, scope);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.getCache(scope).get(key);
if (currentValue === valueStr) {
return Promise.resolve();
}
// Update in cache
this.getCache(scope).set(key, valueStr);
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
}
remove(key: string, scope: StorageScope): Promise<void> {
const wasDeleted = this.getCache(scope).delete(key);
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
}
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
}
}

View File

@@ -0,0 +1,156 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { StorageLegacyService, IStorageLegacy } from 'vs/platform/storage/common/storageLegacyService';
import { endsWith, startsWith, rtrim } from 'vs/base/common/strings';
/**
* We currently store local storage with the following format:
*
* [Global]
* storage://global/<key>
*
* [Workspace]
* storage://workspace/<folder>/<key>
* storage://workspace/empty:<id>/<key>
* storage://workspace/root:<id>/<key>
*
* <folder>
* macOS/Linux: /some/folder/path
* Windows: c%3A/Users/name/folder (normal path)
* file://localhost/c%24/name/folder (unc path)
*
* [no workspace]
* storage://workspace/__$noWorkspace__<key>
* => no longer being used (used for empty workspaces previously)
*/
const COMMON_WORKSPACE_PREFIX = `${StorageLegacyService.COMMON_PREFIX}workspace/`;
const NO_WORKSPACE_PREFIX = 'storage://workspace/__$noWorkspace__';
export type StorageObject = { [key: string]: string };
export interface IParsedStorage {
multiRoot: Map<string, StorageObject>;
folder: Map<string, StorageObject>;
empty: Map<string, StorageObject>;
noWorkspace: StorageObject;
}
export function parseFolderStorage(storage: IStorageLegacy, folderId: string): StorageObject {
const workspaces: { prefix: string; resource: string; }[] = [];
const activeKeys = new Set<string>();
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
// Workspace Storage (storage://workspace/)
if (!startsWith(key, StorageLegacyService.WORKSPACE_PREFIX)) {
continue;
}
activeKeys.add(key);
// We are looking for key: storage://workspace/<folder>/workspaceIdentifier to be able to find all folder
// paths that are known to the storage. is the only way how to parse all folder paths known in storage.
if (endsWith(key, StorageLegacyService.WORKSPACE_IDENTIFIER)) {
// storage://workspace/<folder>/workspaceIdentifier => <folder>/
let workspace = key.substring(StorageLegacyService.WORKSPACE_PREFIX.length, key.length - StorageLegacyService.WORKSPACE_IDENTIFIER.length);
// macOS/Unix: Users/name/folder/
// Windows: c%3A/Users/name/folder/
if (!startsWith(workspace, 'file:')) {
workspace = `file:///${rtrim(workspace, '/')}`;
}
// Windows UNC path: file://localhost/c%3A/Users/name/folder/
else {
workspace = rtrim(workspace, '/');
}
// storage://workspace/<folder>/workspaceIdentifier => storage://workspace/<folder>/
const prefix = key.substr(0, key.length - StorageLegacyService.WORKSPACE_IDENTIFIER.length);
if (startsWith(workspace, folderId)) {
workspaces.push({ prefix, resource: workspace });
}
}
}
// With all the folder paths known we can now extract storage for each path. We have to go through all workspaces
// from the longest path first to reliably extract the storage. The reason is that one folder path can be a parent
// of another folder path and as such a simple indexOf check is not enough.
const workspacesByLength = workspaces.sort((w1, w2) => w1.prefix.length >= w2.prefix.length ? -1 : 1);
const folderWorkspaceStorage: StorageObject = Object.create(null);
workspacesByLength.forEach(workspace => {
activeKeys.forEach(key => {
if (!startsWith(key, workspace.prefix)) {
return; // not part of workspace prefix or already handled
}
activeKeys.delete(key);
if (workspace.resource === folderId) {
// storage://workspace/<folder>/someKey => someKey
const storageKey = key.substr(workspace.prefix.length);
folderWorkspaceStorage[storageKey] = storage.getItem(key);
}
});
});
return folderWorkspaceStorage;
}
export function parseNoWorkspaceStorage(storage: IStorageLegacy) {
const noWorkspacePrefix = `${StorageLegacyService.WORKSPACE_PREFIX}__$noWorkspace__`;
const noWorkspaceStorage: StorageObject = Object.create(null);
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
// No Workspace key is for extension development windows
if (startsWith(key, noWorkspacePrefix) && !endsWith(key, StorageLegacyService.WORKSPACE_IDENTIFIER)) {
// storage://workspace/__$noWorkspace__someKey => someKey
noWorkspaceStorage[key.substr(NO_WORKSPACE_PREFIX.length)] = storage.getItem(key);
}
}
return noWorkspaceStorage;
}
export function parseEmptyStorage(storage: IStorageLegacy, targetWorkspaceId: string): StorageObject {
const emptyStoragePrefix = `${COMMON_WORKSPACE_PREFIX}${targetWorkspaceId}/`;
const emptyWorkspaceStorage: StorageObject = Object.create(null);
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (startsWith(key, emptyStoragePrefix) && !endsWith(key, StorageLegacyService.WORKSPACE_IDENTIFIER)) {
// storage://workspace/empty:<id>/someKey => someKey
emptyWorkspaceStorage[key.substr(emptyStoragePrefix.length)] = storage.getItem(key);
}
}
return emptyWorkspaceStorage;
}
export function parseMultiRootStorage(storage: IStorageLegacy, targetWorkspaceId: string): StorageObject {
const multiRootStoragePrefix = `${COMMON_WORKSPACE_PREFIX}${targetWorkspaceId}/`;
const multiRootWorkspaceStorage: StorageObject = Object.create(null);
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (startsWith(key, multiRootStoragePrefix) && !endsWith(key, StorageLegacyService.WORKSPACE_IDENTIFIER)) {
// storage://workspace/root:<id>/someKey => someKey
multiRootWorkspaceStorage[key.substr(multiRootStoragePrefix.length)] = storage.getItem(key);
}
}
return multiRootWorkspaceStorage;
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as types from 'vs/base/common/types';
import * as errors from 'vs/base/common/errors';
import * as strings from 'vs/base/common/strings';
import * as perf from 'vs/base/common/performance';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
// Browser localStorage interface
export interface IStorageLegacy {
length: number;
key(index: number): string | null;
setItem(key: string, value: any): void;
getItem(key: string): string | null;
removeItem(key: string): void;
}
export const ID = 'storageLegacyService';
export const IStorageLegacyService = createDecorator<IStorageLegacyService>(ID);
export interface IStorageLegacyService {
_serviceBrand: any;
/**
* Store a string value under the given key to local storage.
*
* The optional scope argument allows to define the scope of the operation.
*/
store(key: string, value: any, scope?: StorageLegacyScope): void;
/**
* Delete an element stored under the provided key from local storage.
*
* The optional scope argument allows to define the scope of the operation.
*/
remove(key: string, scope?: StorageLegacyScope): void;
/**
* Retrieve an element stored with the given key from local storage. Use
* the provided defaultValue if the element is null or undefined.
*
* The optional scope argument allows to define the scope of the operation.
*/
get(key: string, scope?: StorageLegacyScope, defaultValue?: string): string | undefined;
/**
* Retrieve an element stored with the given key from local storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*
* The optional scope argument allows to define the scope of the operation.
*/
getInteger(key: string, scope?: StorageLegacyScope, defaultValue?: number): number | undefined;
/**
* Retrieve an element stored with the given key from local storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
*
* The optional scope argument allows to define the scope of the operation.
*/
getBoolean(key: string, scope?: StorageLegacyScope, defaultValue?: boolean): boolean | undefined;
}
export const enum StorageLegacyScope {
/**
* The stored data will be scoped to all workspaces of this domain.
*/
GLOBAL,
/**
* The stored data will be scoped to the current workspace.
*/
WORKSPACE
}
export class StorageLegacyService implements IStorageLegacyService {
_serviceBrand: any;
static readonly COMMON_PREFIX = 'storage://';
static readonly GLOBAL_PREFIX = `${StorageLegacyService.COMMON_PREFIX}global/`;
static readonly WORKSPACE_PREFIX = `${StorageLegacyService.COMMON_PREFIX}workspace/`;
static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier';
static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
private _workspaceStorage: IStorageLegacy;
private _globalStorage: IStorageLegacy;
private workspaceKey: string;
private _workspaceId: string | undefined;
constructor(
globalStorage: IStorageLegacy,
workspaceStorage: IStorageLegacy,
workspaceId?: string,
legacyWorkspaceId?: number
) {
this._globalStorage = globalStorage;
this._workspaceStorage = workspaceStorage || globalStorage;
this.setWorkspaceId(workspaceId, legacyWorkspaceId);
}
get workspaceId(): string | undefined {
return this._workspaceId;
}
setWorkspaceId(workspaceId: string | undefined, legacyWorkspaceId?: number): void {
this._workspaceId = workspaceId;
// Calculate workspace storage key
this.workspaceKey = this.getWorkspaceKey(workspaceId);
// Make sure to delete all workspace storage if the workspace has been recreated meanwhile
// which is only possible if a id property is provided that we can check on
if (types.isNumber(legacyWorkspaceId)) {
this.cleanupWorkspaceScope(legacyWorkspaceId);
} else {
// ensure that we always store a workspace identifier because this key
// is used to migrate data out as needed
const workspaceIdentifier = this.getInteger(StorageLegacyService.WORKSPACE_IDENTIFIER, StorageLegacyScope.WORKSPACE);
if (!workspaceIdentifier) {
this.store(StorageLegacyService.WORKSPACE_IDENTIFIER, 42, StorageLegacyScope.WORKSPACE);
}
}
}
get globalStorage(): IStorageLegacy {
return this._globalStorage;
}
get workspaceStorage(): IStorageLegacy {
return this._workspaceStorage;
}
private getWorkspaceKey(id?: string): string {
if (!id) {
return StorageLegacyService.NO_WORKSPACE_IDENTIFIER;
}
// Special case file:// URIs: strip protocol from key to produce shorter key
const fileProtocol = 'file:///';
if (id.indexOf(fileProtocol) === 0) {
id = id.substr(fileProtocol.length);
}
// Always end with "/"
return `${strings.rtrim(id, '/')}/`;
}
private cleanupWorkspaceScope(workspaceUid: number): void {
// Get stored identifier from storage
perf.mark('willReadWorkspaceIdentifier');
const id = this.getInteger(StorageLegacyService.WORKSPACE_IDENTIFIER, StorageLegacyScope.WORKSPACE);
perf.mark('didReadWorkspaceIdentifier');
// If identifier differs, assume the workspace got recreated and thus clean all storage for this workspace
if (types.isNumber(id) && workspaceUid !== id) {
const keyPrefix = this.toStorageKey('', StorageLegacyScope.WORKSPACE);
const toDelete: string[] = [];
const length = this._workspaceStorage.length;
for (let i = 0; i < length; i++) {
const key = this._workspaceStorage.key(i);
if (!key || key.indexOf(StorageLegacyService.WORKSPACE_PREFIX) < 0) {
continue; // ignore stored things that don't belong to storage service or are defined globally
}
// Check for match on prefix
if (key.indexOf(keyPrefix) === 0) {
toDelete.push(key);
}
}
// Run the delete
toDelete.forEach((keyToDelete) => {
this._workspaceStorage.removeItem(keyToDelete);
});
}
// Store workspace identifier now
if (workspaceUid !== id) {
this.store(StorageLegacyService.WORKSPACE_IDENTIFIER, workspaceUid, StorageLegacyScope.WORKSPACE);
}
}
store(key: string, value: any, scope = StorageLegacyScope.GLOBAL): void {
const storage = (scope === StorageLegacyScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
if (types.isUndefinedOrNull(value)) {
this.remove(key, scope); // we cannot store null or undefined, in that case we remove the key
return;
}
const storageKey = this.toStorageKey(key, scope);
// Store
try {
storage.setItem(storageKey, value);
} catch (error) {
errors.onUnexpectedError(error);
}
}
get(key: string, scope = StorageLegacyScope.GLOBAL, defaultValue?: any): string {
const storage = (scope === StorageLegacyScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const value = storage.getItem(this.toStorageKey(key, scope));
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
return value;
}
getInteger(key: string, scope = StorageLegacyScope.GLOBAL, defaultValue: number = 0): number {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
return parseInt(value, 10);
}
getBoolean(key: string, scope = StorageLegacyScope.GLOBAL, defaultValue: boolean = false): boolean {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
if (types.isString(value)) {
return value.toLowerCase() === 'true' ? true : false;
}
return value ? true : false;
}
remove(key: string, scope = StorageLegacyScope.GLOBAL): void {
const storage = (scope === StorageLegacyScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const storageKey = this.toStorageKey(key, scope);
// Remove
storage.removeItem(storageKey);
}
private toStorageKey(key: string, scope: StorageLegacyScope): string {
if (scope === StorageLegacyScope.GLOBAL) {
return StorageLegacyService.GLOBAL_PREFIX + key.toLowerCase();
}
return StorageLegacyService.WORKSPACE_PREFIX + this.workspaceKey + key.toLowerCase();
}
}
export class InMemoryLocalStorage implements IStorageLegacy {
private store: { [key: string]: string; };
constructor() {
this.store = {};
}
get length() {
return Object.keys(this.store).length;
}
key(index: number): string | null {
const keys = Object.keys(this.store);
if (keys.length > index) {
return keys[index];
}
return null;
}
setItem(key: string, value: any): void {
this.store[key] = value.toString();
}
getItem(key: string): string | null {
const item = this.store[key];
if (!types.isUndefinedOrNull(item)) {
return item;
}
return null;
}
removeItem(key: string): void {
delete this.store[key];
}
}
export const inMemoryLocalStorageInstance = new InMemoryLocalStorage();

View File

@@ -1,235 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as types from 'vs/base/common/types';
import * as errors from 'vs/base/common/errors';
import * as strings from 'vs/base/common/strings';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import * as perf from 'vs/base/common/performance';
// Browser localStorage interface
export interface IStorage {
length: number;
key(index: number): string;
setItem(key: string, value: any): void;
getItem(key: string): string;
removeItem(key: string): void;
}
export class StorageService implements IStorageService {
_serviceBrand: any;
static readonly COMMON_PREFIX = 'storage://';
static readonly GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`;
static readonly WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`;
static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier';
static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
private _workspaceStorage: IStorage;
private _globalStorage: IStorage;
private workspaceKey: string;
private _workspaceId: string;
constructor(
globalStorage: IStorage,
workspaceStorage: IStorage,
workspaceId?: string,
legacyWorkspaceId?: number
) {
this._globalStorage = globalStorage;
this._workspaceStorage = workspaceStorage || globalStorage;
this.setWorkspaceId(workspaceId, legacyWorkspaceId);
}
get workspaceId(): string {
return this._workspaceId;
}
setWorkspaceId(workspaceId: string, legacyWorkspaceId?: number): void {
this._workspaceId = workspaceId;
// Calculate workspace storage key
this.workspaceKey = this.getWorkspaceKey(workspaceId);
// Make sure to delete all workspace storage if the workspace has been recreated meanwhile
// which is only possible if a id property is provided that we can check on
if (types.isNumber(legacyWorkspaceId)) {
this.cleanupWorkspaceScope(legacyWorkspaceId);
}
}
get globalStorage(): IStorage {
return this._globalStorage;
}
get workspaceStorage(): IStorage {
return this._workspaceStorage;
}
private getWorkspaceKey(id?: string): string {
if (!id) {
return StorageService.NO_WORKSPACE_IDENTIFIER;
}
// Special case file:// URIs: strip protocol from key to produce shorter key
const fileProtocol = 'file:///';
if (id.indexOf(fileProtocol) === 0) {
id = id.substr(fileProtocol.length);
}
// Always end with "/"
return `${strings.rtrim(id, '/')}/`;
}
private cleanupWorkspaceScope(workspaceUid: number): void {
// Get stored identifier from storage
perf.mark('willReadWorkspaceIdentifier');
const id = this.getInteger(StorageService.WORKSPACE_IDENTIFIER, StorageScope.WORKSPACE);
perf.mark('didReadWorkspaceIdentifier');
// If identifier differs, assume the workspace got recreated and thus clean all storage for this workspace
if (types.isNumber(id) && workspaceUid !== id) {
const keyPrefix = this.toStorageKey('', StorageScope.WORKSPACE);
const toDelete: string[] = [];
const length = this._workspaceStorage.length;
for (let i = 0; i < length; i++) {
const key = this._workspaceStorage.key(i);
if (key.indexOf(StorageService.WORKSPACE_PREFIX) < 0) {
continue; // ignore stored things that don't belong to storage service or are defined globally
}
// Check for match on prefix
if (key.indexOf(keyPrefix) === 0) {
toDelete.push(key);
}
}
// Run the delete
toDelete.forEach((keyToDelete) => {
this._workspaceStorage.removeItem(keyToDelete);
});
}
// Store workspace identifier now
if (workspaceUid !== id) {
this.store(StorageService.WORKSPACE_IDENTIFIER, workspaceUid, StorageScope.WORKSPACE);
}
}
store(key: string, value: any, scope = StorageScope.GLOBAL): void {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
if (types.isUndefinedOrNull(value)) {
this.remove(key, scope); // we cannot store null or undefined, in that case we remove the key
return;
}
const storageKey = this.toStorageKey(key, scope);
// Store
try {
storage.setItem(storageKey, value);
} catch (error) {
errors.onUnexpectedError(error);
}
}
get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const value = storage.getItem(this.toStorageKey(key, scope));
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
return value;
}
getInteger(key: string, scope = StorageScope.GLOBAL, defaultValue?: number): number {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
return parseInt(value, 10);
}
getBoolean(key: string, scope = StorageScope.GLOBAL, defaultValue?: boolean): boolean {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
return defaultValue;
}
if (types.isString(value)) {
return value.toLowerCase() === 'true' ? true : false;
}
return value ? true : false;
}
remove(key: string, scope = StorageScope.GLOBAL): void {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const storageKey = this.toStorageKey(key, scope);
// Remove
storage.removeItem(storageKey);
}
private toStorageKey(key: string, scope: StorageScope): string {
if (scope === StorageScope.GLOBAL) {
return StorageService.GLOBAL_PREFIX + key.toLowerCase();
}
return StorageService.WORKSPACE_PREFIX + this.workspaceKey + key.toLowerCase();
}
}
export class InMemoryLocalStorage implements IStorage {
private store: { [key: string]: string; };
constructor() {
this.store = {};
}
get length() {
return Object.keys(this.store).length;
}
key(index: number): string {
const keys = Object.keys(this.store);
if (keys.length > index) {
return keys[index];
}
return null;
}
setItem(key: string, value: any): void {
this.store[key] = value.toString();
}
getItem(key: string): string {
const item = this.store[key];
if (!types.isUndefinedOrNull(item)) {
return item;
}
return null;
}
removeItem(key: string): void {
delete this.store[key];
}
}
export const inMemoryLocalStorageInstance = new InMemoryLocalStorage();

View File

@@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc';
import { Event, Emitter, debounceEvent } from 'vs/base/common/event';
import { StorageMainService, IStorageChangeEvent } from 'vs/platform/storage/node/storageMainService';
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/node/storage';
import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
type Key = string;
type Value = string;
type Item = [Key, Value];
interface ISerializableUpdateRequest {
insert?: Item[];
delete?: Key[];
}
interface ISerializableItemsChangeEvent {
items: Item[];
}
export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel {
private static STORAGE_CHANGE_DEBOUNCE_TIME = 100;
private _onDidChangeItems: Emitter<ISerializableItemsChangeEvent> = this._register(new Emitter<ISerializableItemsChangeEvent>());
get onDidChangeItems(): Event<ISerializableItemsChangeEvent> { return this._onDidChangeItems.event; }
constructor(private storageMainService: StorageMainService) {
super();
this.registerListeners();
}
private registerListeners(): void {
// Listen for changes in global storage to send to listeners
// that are listening. Use a debouncer to reduce IPC traffic.
this._register(debounceEvent(this.storageMainService.onDidChangeStorage, (prev: IStorageChangeEvent[], cur: IStorageChangeEvent) => {
if (!prev) {
prev = [cur];
} else {
prev.push(cur);
}
return prev;
}, GlobalStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => {
if (events.length) {
this._onDidChangeItems.fire(this.serializeEvents(events));
}
}));
}
private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent {
const items = new Map<Key, Value>();
events.forEach(event => items.set(event.key, this.storageMainService.get(event.key, null)));
return { items: mapToSerializable(items) } as ISerializableItemsChangeEvent;
}
listen(_, event: string): Event<any> {
switch (event) {
case 'onDidChangeItems': return this.onDidChangeItems;
}
throw new Error(`Event not found: ${event}`);
}
call(_, command: string, arg?: any): Thenable<any> {
switch (command) {
case 'getItems': {
return Promise.resolve(mapToSerializable(this.storageMainService.items));
}
case 'updateItems': {
const items = arg as ISerializableUpdateRequest;
if (items.insert) {
for (const [key, value] of items.insert) {
this.storageMainService.store(key, value);
}
}
if (items.delete) {
items.delete.forEach(key => this.storageMainService.remove(key));
}
return Promise.resolve(); // do not wait for modifications to complete
}
case 'checkIntegrity': {
return this.storageMainService.checkIntegrity(arg);
}
}
throw new Error(`Call not found: ${command}`);
}
}
export class GlobalStorageDatabaseChannelClient extends Disposable implements IStorageDatabase {
_serviceBrand: any;
private _onDidChangeItemsExternal: Emitter<IStorageItemsChangeEvent> = this._register(new Emitter<IStorageItemsChangeEvent>());
get onDidChangeItemsExternal(): Event<IStorageItemsChangeEvent> { return this._onDidChangeItemsExternal.event; }
private onDidChangeItemsOnMainListener: IDisposable;
constructor(private channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this.onDidChangeItemsOnMainListener = this.channel.listen('onDidChangeItems')((e: ISerializableItemsChangeEvent) => this.onDidChangeItemsOnMain(e));
}
private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void {
if (Array.isArray(e.items)) {
this._onDidChangeItemsExternal.fire({ items: serializableToMap(e.items) });
}
}
getItems(): Thenable<Map<string, string>> {
return this.channel.call('getItems').then((data: Item[]) => serializableToMap(data));
}
updateItems(request: IUpdateRequest): Thenable<void> {
let updateCount = 0;
const serializableRequest: ISerializableUpdateRequest = Object.create(null);
if (request.insert) {
serializableRequest.insert = mapToSerializable(request.insert);
updateCount += request.insert.size;
}
if (request.delete) {
serializableRequest.delete = values(request.delete);
updateCount += request.delete.size;
}
if (updateCount === 0) {
return Promise.resolve(); // prevent work if not needed
}
return this.channel.call('updateItems', serializableRequest);
}
checkIntegrity(full: boolean): Thenable<string> {
return this.channel.call('checkIntegrity', full);
}
close(): Thenable<void> {
// when we are about to close, we start to ignore main-side changes since we close anyway
this.onDidChangeItemsOnMainListener = dispose(this.onDidChangeItemsOnMainListener);
return Promise.resolve(); // global storage is closed on the main side
}
dispose(): void {
super.dispose();
this.onDidChangeItemsOnMainListener = dispose(this.onDidChangeItemsOnMainListener);
}
}

View File

@@ -0,0 +1,389 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IStorage, Storage, SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions, InMemoryStorageDatabase } from 'vs/base/node/storage';
import { join } from 'path';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { mark, getDuration } from 'vs/base/common/performance';
import { exists, readdir } from 'vs/base/node/pfs';
import { Database } from 'vscode-sqlite3';
import { endsWith, startsWith } from 'vs/base/common/strings';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
export interface IStorageMainService {
_serviceBrand: any;
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
/**
* Emitted when the storage is about to persist. This is the right time
* to persist data to ensure it is stored before the application shuts
* down.
*/
readonly onWillSaveState: Event<void>;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*/
get(key: string, fallbackValue: string): string;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
*/
getBoolean(key: string, fallbackValue: boolean): boolean;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*/
getInteger(key: string, fallbackValue: number): number;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*/
store(key: string, value: any): void;
/**
* Delete an element stored under the provided key from storage.
*/
remove(key: string): void;
}
export interface IStorageChangeEvent {
key: string;
}
export class StorageMainService extends Disposable implements IStorageMainService {
_serviceBrand: any;
private static STORAGE_NAME = 'temp.vscdb';
private _onDidChangeStorage: Emitter<IStorageChangeEvent> = this._register(new Emitter<IStorageChangeEvent>());
get onDidChangeStorage(): Event<IStorageChangeEvent> { return this._onDidChangeStorage.event; }
private _onWillSaveState: Emitter<void> = this._register(new Emitter<void>());
get onWillSaveState(): Event<void> { return this._onWillSaveState.event; }
get items(): Map<string, string> { return this.storage.items; }
private storage: IStorage;
constructor(
@ILogService private logService: ILogService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super();
// Until the storage has been initialized, it can only be in memory
this.storage = new Storage(new InMemoryStorageDatabase());
}
private get storagePath(): string {
if (!!this.environmentService.extensionTestsPath || !process.env['VSCODE_TEST_STORAGE_MIGRATION']) {
return SQLiteStorageDatabase.IN_MEMORY_PATH; // no storage during extension tests!
}
return join(this.environmentService.globalStorageHome, StorageMainService.STORAGE_NAME);
}
private createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions {
const loggedStorageErrors = new Set<string>();
return {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : void 0,
logError: error => {
this.logService.error(error);
const errorStr = `${error}`;
if (!loggedStorageErrors.has(errorStr)) {
loggedStorageErrors.add(errorStr);
/* __GDPR__
"sqliteMainStorageError" : {
"storageError": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('sqliteMainStorageError', {
'storageError': errorStr
});
}
}
} as ISQLiteStorageDatabaseLoggingOptions;
}
initialize(): Thenable<void> {
const useInMemoryStorage = this.storagePath === SQLiteStorageDatabase.IN_MEMORY_PATH;
let globalStorageExists: Promise<boolean>;
if (useInMemoryStorage) {
globalStorageExists = Promise.resolve(true);
} else {
globalStorageExists = exists(this.storagePath);
}
return globalStorageExists.then(exists => {
this.storage.dispose();
this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, {
logging: this.createLogginOptions()
}));
this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key })));
mark('main:willInitGlobalStorage');
return this.storage.init().then(() => {
mark('main:didInitGlobalStorage');
}, error => {
mark('main:didInitGlobalStorage');
return Promise.reject(error);
}).then(() => {
// Migrate storage if this is the first start and we are not using in-memory
let migrationPromise: Thenable<void>;
if (!useInMemoryStorage && !exists) {
// TODO@Ben remove global storage migration and move Storage creation back to ctor
migrationPromise = this.migrateGlobalStorage().then(() => this.logService.info('[storage] migrated global storage'), error => this.logService.error(`[storage] migration error ${error}`));
} else {
migrationPromise = Promise.resolve();
}
return migrationPromise;
});
});
}
private migrateGlobalStorage(): Thenable<void> {
this.logService.info('[storage] migrating global storage from localStorage into SQLite');
const localStorageDBBackup = join(this.environmentService.userDataPath, 'Local Storage', 'file__0.localstorage.vscmig');
return exists(localStorageDBBackup).then(exists => {
if (!exists) {
return Promise.resolve(); // return if there is no DB to migrate from
}
return readdir(this.environmentService.extensionsPath).then(extensions => {
const supportedKeys = new Map<string, string>();
[
'editorFontInfo',
'peekViewLayout',
'expandSuggestionDocs',
'extensionsIdentifiers/disabled',
'integrityService',
'telemetry.lastSessionDate',
'telemetry.instanceId',
'telemetry.firstSessionDate',
'workbench.sidebar.width',
'workbench.panel.width',
'workbench.panel.height',
'workbench.panel.sizeBeforeMaximized',
'workbench.activity.placeholderViewlets',
'colorThemeData',
'iconThemeData',
'workbench.telemetryOptOutShown',
'workbench.hide.welcome',
'releaseNotes/lastVersion',
'debug.actionswidgetposition',
'debug.actionswidgety',
'editor.neverPromptForLargeFiles',
'menubar/electronFixRecommended',
'learnMoreDirtyWriteError',
'extensions.ignoredAutoUpdateExtension',
'askToInstallRemoteServerExtension',
'hasNotifiedOfSettingsAutosave',
'commandPalette.mru.cache',
'commandPalette.mru.counter',
'parts-splash-data',
'terminal.integrated.neverMeasureRenderTime',
'terminal.integrated.neverSuggestSelectWindowsShell',
'memento/workbench.parts.editor',
'memento/workbench.view.search',
'langugage.update.donotask',
'extensionsAssistant/languagePackSuggestionIgnore',
'workbench.panel.pinnedPanels',
'workbench.activity.pinnedViewlets',
'extensionsAssistant/ignored_recommendations',
'extensionsAssistant/recommendations',
'extensionsAssistant/importantRecommendationsIgnore',
'extensionsAssistant/fileExtensionsSuggestionIgnore',
'nps/skipVersion',
'nps/lastSessionDate',
'nps/sessionCount',
'nps/isCandidate',
'allExperiments',
'currentOrPreviouslyRunExperiments',
'update/win32-64bits',
'update/win32-fast-updates',
'update/lastKnownVersion',
'update/updateNotificationTime'
].forEach(key => supportedKeys.set(key.toLowerCase(), key));
// Support extension storage as well (always the ID of the extension)
extensions.forEach(extension => {
let extensionId: string;
if (extension.indexOf('-') >= 0) {
extensionId = extension.substring(0, extension.lastIndexOf('-')); // convert "author.extension-0.2.5" => "author.extension"
} else {
extensionId = extension;
}
if (extensionId) {
supportedKeys.set(extensionId.toLowerCase(), extensionId);
}
});
return import('vscode-sqlite3').then(sqlite3 => {
return new Promise((resolve, reject) => {
const handleSuffixKey = (row, key: string, suffix: string) => {
if (endsWith(key, suffix.toLowerCase())) {
const value: string = row.value.toString('utf16le');
const normalizedKey = key.substring(0, key.length - suffix.length) + suffix;
this.store(normalizedKey, value);
return true;
}
return false;
};
const db: Database = new (sqlite3.Database)(localStorageDBBackup, error => {
if (error) {
if (db) {
db.close();
}
return reject(error);
}
db.all('SELECT key, value FROM ItemTable', (error, rows) => {
if (error) {
db.close();
return reject(error);
}
try {
rows.forEach(row => {
let key: string = row.key;
if (key.indexOf('storage://global/') !== 0) {
return; // not a global key
}
// convert storage://global/colorthemedata => colorthemedata
key = key.substr('storage://global/'.length);
const supportedKey = supportedKeys.get(key);
if (supportedKey) {
const value: string = row.value.toString('utf16le');
this.store(supportedKey, value);
}
// dynamic values
else if (
endsWith(key, '.hidden') ||
startsWith(key, 'experiments.')
) {
const value: string = row.value.toString('utf16le');
this.store(key, value);
}
// fix lowercased ".sessionCount"
else if (handleSuffixKey(row, key, '.sessionCount')) { }
// fix lowercased ".lastSessionDate"
else if (handleSuffixKey(row, key, '.lastSessionDate')) { }
// fix lowercased ".skipVersion"
else if (handleSuffixKey(row, key, '.skipVersion')) { }
// fix lowercased ".isCandidate"
else if (handleSuffixKey(row, key, '.isCandidate')) { }
// fix lowercased ".editedCount"
else if (handleSuffixKey(row, key, '.editedCount')) { }
// fix lowercased ".editedDate"
else if (handleSuffixKey(row, key, '.editedDate')) { }
});
db.close();
} catch (error) {
db.close();
return reject(error);
}
resolve();
});
});
});
});
});
});
}
get(key: string, fallbackValue: string): string {
return this.storage.get(key, fallbackValue);
}
getBoolean(key: string, fallbackValue: boolean): boolean {
return this.storage.getBoolean(key, fallbackValue);
}
getInteger(key: string, fallbackValue: number): number {
return this.storage.getInteger(key, fallbackValue);
}
store(key: string, value: any): Thenable<void> {
return this.storage.set(key, value);
}
remove(key: string): Thenable<void> {
return this.storage.delete(key);
}
close(): Thenable<void> {
this.logService.trace('StorageMainService#close() - begin');
// Signal to storage that we are about to close
this.storage.beforeClose();
// Signal as event so that clients can still store data
this._onWillSaveState.fire();
// Do it
mark('main:willCloseGlobalStorage');
return this.storage.close().then(() => {
mark('main:didCloseGlobalStorage');
this.logService.trace(`StorageMainService#close() - finished in ${getDuration('main:willCloseGlobalStorage', 'main:didCloseGlobalStorage')}ms`);
});
}
checkIntegrity(full: boolean): Thenable<string> {
return this.storage.checkIntegrity(full);
}
}

View File

@@ -0,0 +1,608 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { Storage, ISQLiteStorageDatabaseLoggingOptions, IStorage, StorageHint, IStorageDatabase, SQLiteStorageDatabase } from 'vs/base/node/storage';
import { IStorageLegacyService, StorageLegacyScope } from 'vs/platform/storage/common/storageLegacyService';
import { startsWith, endsWith } from 'vs/base/common/strings';
import { Action } from 'vs/base/common/actions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { localize } from 'vs/nls';
import { mark, getDuration } from 'vs/base/common/performance';
import { join } from 'path';
import { copy, exists, mkdirp, readdir, writeFile } from 'vs/base/node/pfs';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { onUnexpectedError } from 'vs/base/common/errors';
import { StorageObject, parseMultiRootStorage, parseFolderStorage, parseNoWorkspaceStorage, parseEmptyStorage } from 'vs/platform/storage/common/storageLegacyMigration';
export class StorageService extends Disposable implements IStorageService {
_serviceBrand: any;
private static WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static WORKSPACE_META_NAME = 'workspace.json';
private _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
private _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
private _hasErrors = false;
get hasErrors(): boolean { return this._hasErrors; }
private bufferedWorkspaceStorageErrors?: (string | Error)[] = [];
private _onWorkspaceStorageError: Emitter<string | Error> = this._register(new Emitter<string | Error>());
get onWorkspaceStorageError(): Event<string | Error> {
if (Array.isArray(this.bufferedWorkspaceStorageErrors)) {
// todo@ben cleanup after a while
if (this.bufferedWorkspaceStorageErrors.length > 0) {
const bufferedStorageErrors = this.bufferedWorkspaceStorageErrors;
setTimeout(() => {
this._onWorkspaceStorageError.fire(`[startup errors] ${bufferedStorageErrors.join('\n')}`);
}, 0);
}
this.bufferedWorkspaceStorageErrors = void 0;
}
return this._onWorkspaceStorageError.event;
}
private globalStorage: IStorage;
private workspaceStoragePath: string;
private workspaceStorage: IStorage;
private workspaceStorageListener: IDisposable;
constructor(
globalStorageDatabase: IStorageDatabase,
@ILogService private logService: ILogService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
super();
// Global Storage
this.globalStorage = new Storage(globalStorageDatabase);
if (process.env['VSCODE_TEST_STORAGE_MIGRATION']) {
this._register(this.globalStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.GLOBAL)));
}
}
private handleDidChangeStorage(key: string, scope: StorageScope): void {
this._onDidChangeStorage.fire({ key, scope });
}
initialize(payload: IWorkspaceInitializationPayload): Thenable<void> {
return Promise.all([
this.initializeGlobalStorage(),
this.initializeWorkspaceStorage(payload)
]).then(() => void 0);
}
private initializeGlobalStorage(): Thenable<void> {
mark('willInitGlobalStorage');
return this.globalStorage.init().then(() => {
mark('didInitGlobalStorage');
}, error => {
mark('didInitGlobalStorage');
return Promise.reject(error);
});
}
private initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Thenable<void> {
// Prepare workspace storage folder for DB
return this.prepareWorkspaceStorageFolder(payload).then(result => {
const useInMemoryStorage = !!this.environmentService.extensionTestsPath; // no storage during extension tests!
let workspaceStoragePath: string;
let workspaceStorageExists: Thenable<boolean>;
if (useInMemoryStorage) {
workspaceStoragePath = SQLiteStorageDatabase.IN_MEMORY_PATH;
workspaceStorageExists = Promise.resolve(true);
} else {
workspaceStoragePath = join(result.path, StorageService.WORKSPACE_STORAGE_NAME);
mark('willCheckWorkspaceStorageExists');
workspaceStorageExists = exists(workspaceStoragePath).then(exists => {
mark('didCheckWorkspaceStorageExists');
return exists;
});
}
return workspaceStorageExists.then(exists => {
// Create workspace storage and initalize
mark('willInitWorkspaceStorage');
return this.createWorkspaceStorage(workspaceStoragePath, result.wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : void 0).init().then(() => {
mark('didInitWorkspaceStorage');
}, error => {
mark('didInitWorkspaceStorage');
return Promise.reject(error);
}).then(() => {
// Migrate storage if this is the first start and we are not using in-memory
let migrationPromise: Thenable<void>;
if (!useInMemoryStorage && !exists) {
migrationPromise = this.migrateWorkspaceStorage(payload);
} else {
migrationPromise = Promise.resolve();
}
return migrationPromise;
});
});
});
}
// TODO@Ben remove migration after a while
private migrateWorkspaceStorage(payload: IWorkspaceInitializationPayload): Thenable<void> {
mark('willMigrateWorkspaceStorageKeys');
return readdir(this.environmentService.extensionsPath).then(extensions => {
// Otherwise, we migrate data from window.localStorage over
try {
let workspaceItems: StorageObject;
if (isWorkspaceIdentifier(payload)) {
workspaceItems = parseMultiRootStorage(window.localStorage, `root:${payload.id}`);
} else if (isSingleFolderWorkspaceInitializationPayload(payload)) {
workspaceItems = parseFolderStorage(window.localStorage, payload.folder.toString());
} else {
if (payload.id === 'ext-dev') {
workspaceItems = parseNoWorkspaceStorage(window.localStorage);
} else {
workspaceItems = parseEmptyStorage(window.localStorage, `${payload.id}`);
}
}
const workspaceItemsKeys = workspaceItems ? Object.keys(workspaceItems) : [];
if (workspaceItemsKeys.length > 0) {
const supportedKeys = new Map<string, string>();
[
'workbench.search.history',
'history.entries',
'ignoreNetVersionError',
'ignoreEnospcError',
'extensionUrlHandler.urlToHandle',
'terminal.integrated.isWorkspaceShellAllowed',
'workbench.tasks.ignoreTask010Shown',
'workbench.tasks.recentlyUsedTasks',
'workspaces.dontPromptToOpen',
'output.activechannel',
'outline/state',
'extensionsAssistant/workspaceRecommendationsIgnore',
'extensionsAssistant/dynamicWorkspaceRecommendations',
'debug.repl.history',
'editor.matchCase',
'editor.wholeWord',
'editor.isRegex',
'lifecyle.lastShutdownReason',
'debug.selectedroot',
'debug.selectedconfigname',
'debug.breakpoint',
'debug.breakpointactivated',
'debug.functionbreakpoint',
'debug.exceptionbreakpoint',
'debug.watchexpressions',
'workbench.sidebar.activeviewletid',
'workbench.panelpart.activepanelid',
'workbench.zenmode.active',
'workbench.centerededitorlayout.active',
'workbench.sidebar.hidden',
'workbench.panel.hidden',
'workbench.panel.location',
'extensionsIdentifiers/disabled',
'extensionsIdentifiers/enabled',
'scm.views',
'suggest/memories/first',
'suggest/memories/recentlyUsed',
'suggest/memories/recentlyUsedByPrefix',
'workbench.view.explorer.numberOfVisibleViews',
'workbench.view.extensions.numberOfVisibleViews',
'workbench.view.debug.numberOfVisibleViews',
'workbench.explorer.views.state',
'workbench.view.extensions.state',
'workbench.view.debug.state',
'memento/workbench.editor.walkThroughPart',
'memento/workbench.editor.settings2',
'memento/workbench.editor.htmlPreviewPart',
'memento/workbench.editor.defaultPreferences',
'memento/workbench.editors.files.textFileEditor',
'memento/workbench.editors.logViewer',
'memento/workbench.editors.textResourceEditor',
'memento/workbench.panel.output'
].forEach(key => supportedKeys.set(key.toLowerCase(), key));
// Support extension storage as well (always the ID of the extension)
extensions.forEach(extension => {
let extensionId: string;
if (extension.indexOf('-') >= 0) {
extensionId = extension.substring(0, extension.lastIndexOf('-')); // convert "author.extension-0.2.5" => "author.extension"
} else {
extensionId = extension;
}
if (extensionId) {
supportedKeys.set(extensionId.toLowerCase(), extensionId);
}
});
workspaceItemsKeys.forEach(key => {
const value = workspaceItems[key];
// first check for a well known supported key and store with realcase value
const supportedKey = supportedKeys.get(key);
if (supportedKey) {
this.store(supportedKey, value, StorageScope.WORKSPACE);
}
// fix lowercased ".numberOfVisibleViews"
else if (endsWith(key, '.numberOfVisibleViews'.toLowerCase())) {
const normalizedKey = key.substring(0, key.length - '.numberOfVisibleViews'.length) + '.numberOfVisibleViews';
this.store(normalizedKey, value, StorageScope.WORKSPACE);
}
// support dynamic keys
else if (key.indexOf('memento/') === 0 || endsWith(key, '.state')) {
this.store(key, value, StorageScope.WORKSPACE);
}
});
}
} catch (error) {
onUnexpectedError(error);
this.logService.error(error);
}
mark('didMigrateWorkspaceStorageKeys');
});
}
private createWorkspaceStorage(workspaceStoragePath: string, hint?: StorageHint): IStorage {
// Logger for workspace storage
const workspaceLoggingOptions: ISQLiteStorageDatabaseLoggingOptions = {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : void 0,
logError: error => {
this.logService.error(error);
this._hasErrors = true;
if (Array.isArray(this.bufferedWorkspaceStorageErrors)) {
this.bufferedWorkspaceStorageErrors.push(error);
} else {
this._onWorkspaceStorageError.fire(error);
}
}
};
// Dispose old (if any)
this.workspaceStorage = dispose(this.workspaceStorage);
this.workspaceStorageListener = dispose(this.workspaceStorageListener);
// Create new
this.workspaceStoragePath = workspaceStoragePath;
this.workspaceStorage = new Storage(new SQLiteStorageDatabase(workspaceStoragePath, { logging: workspaceLoggingOptions }), { hint });
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.WORKSPACE));
return this.workspaceStorage;
}
private getWorkspaceStorageFolderPath(payload: IWorkspaceInitializationPayload): string {
return join(this.environmentService.workspaceStorageHome, payload.id); // workspace home + workspace id;
}
private prepareWorkspaceStorageFolder(payload: IWorkspaceInitializationPayload): Thenable<{ path: string, wasCreated: boolean }> {
const workspaceStorageFolderPath = this.getWorkspaceStorageFolderPath(payload);
return exists(workspaceStorageFolderPath).then(exists => {
if (exists) {
return { path: workspaceStorageFolderPath, wasCreated: false };
}
return mkdirp(workspaceStorageFolderPath).then(() => {
// Write metadata into folder
this.ensureWorkspaceStorageFolderMeta(payload);
return { path: workspaceStorageFolderPath, wasCreated: true };
});
});
}
private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void {
let meta: object | undefined = void 0;
if (isSingleFolderWorkspaceInitializationPayload(payload)) {
meta = { folder: payload.folder.toString() };
} else if (isWorkspaceIdentifier(payload)) {
meta = { configuration: payload.configPath };
}
if (meta) {
const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), StorageService.WORKSPACE_META_NAME);
exists(workspaceStorageMetaPath).then(exists => {
if (exists) {
return void 0; // already existing
}
return writeFile(workspaceStorageMetaPath, JSON.stringify(meta, void 0, 2));
}).then(null, error => onUnexpectedError(error));
}
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope).get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope).getBoolean(key, fallbackValue);
}
getInteger(key: string, scope: StorageScope, fallbackValue: number): number;
getInteger(key: string, scope: StorageScope): number | undefined;
getInteger(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope).getInteger(key, fallbackValue);
}
store(key: string, value: any, scope: StorageScope): void {
this.getStorage(scope).set(key, value);
}
remove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
close(): Promise<void> {
// Signal to storage that we are about to close
this.globalStorage.beforeClose();
this.workspaceStorage.beforeClose();
// Signal as event so that clients can still store data
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
// Do it
mark('willCloseGlobalStorage');
mark('willCloseWorkspaceStorage');
return Promise.all([
this.globalStorage.close().then(() => mark('didCloseGlobalStorage')),
this.workspaceStorage.close().then(() => mark('didCloseWorkspaceStorage'))
]).then(() => {
this.logService.trace(`[storage] closing took ${getDuration('willCloseGlobalStorage', 'didCloseGlobalStorage')}ms global / ${getDuration('willCloseWorkspaceStorage', 'didCloseWorkspaceStorage')}ms workspace`);
});
}
private getStorage(scope: StorageScope): IStorage {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
getSize(scope: StorageScope): number {
return scope === StorageScope.GLOBAL ? this.globalStorage.size : this.workspaceStorage.size;
}
checkIntegrity(scope: StorageScope, full: boolean): Thenable<string> {
return scope === StorageScope.GLOBAL ? this.globalStorage.checkIntegrity(full) : this.workspaceStorage.checkIntegrity(full);
}
logStorage(): Promise<void> {
return Promise.all([
this.globalStorage.items,
this.workspaceStorage.items,
this.globalStorage.checkIntegrity(true /* full */),
this.workspaceStorage.checkIntegrity(true /* full */)
]).then(result => {
const safeParse = (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
const globalItems = new Map<string, string>();
const globalItemsParsed = new Map<string, string>();
result[0].forEach((value, key) => {
globalItems.set(key, value);
globalItemsParsed.set(key, safeParse(value));
});
const workspaceItems = new Map<string, string>();
const workspaceItemsParsed = new Map<string, string>();
result[1].forEach((value, key) => {
workspaceItems.set(key, value);
workspaceItemsParsed.set(key, safeParse(value));
});
console.group(`Storage: Global (integrity: ${result[2]}, load: ${getDuration('main:willInitGlobalStorage', 'main:didInitGlobalStorage')}, path: ${this.environmentService.globalStorageHome})`);
let globalValues: { key: string, value: string }[] = [];
globalItems.forEach((value, key) => {
globalValues.push({ key, value });
});
console.table(globalValues);
console.groupEnd();
console.log(globalItemsParsed);
console.group(`Storage: Workspace (integrity: ${result[3]}, load: ${getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage')}, path: ${this.workspaceStoragePath})`);
let workspaceValues: { key: string, value: string }[] = [];
workspaceItems.forEach((value, key) => {
workspaceValues.push({ key, value });
});
console.table(workspaceValues);
console.groupEnd();
console.log(workspaceItemsParsed);
});
}
migrate(toWorkspace: IWorkspaceInitializationPayload): Thenable<void> {
if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) {
return Promise.resolve(); // no migration needed if running in memory
}
// Close workspace DB to be able to copy
return this.workspaceStorage.close().then(() => {
// Prepare new workspace storage folder
return this.prepareWorkspaceStorageFolder(toWorkspace).then(result => {
const newWorkspaceStoragePath = join(result.path, StorageService.WORKSPACE_STORAGE_NAME);
// Copy current storage over to new workspace storage
return copy(this.workspaceStoragePath, newWorkspaceStoragePath).then(() => {
// Recreate and init workspace storage
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
});
});
});
}
}
export class LogStorageAction extends Action {
static readonly ID = 'workbench.action.logStorage';
static LABEL = localize({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents");
constructor(
id: string,
label: string,
@IStorageService private storageService: DelegatingStorageService,
@IWindowService private windowService: IWindowService
) {
super(id, label);
}
run(): Thenable<void> {
this.storageService.storage.logStorage();
return this.windowService.openDevTools();
}
}
export class DelegatingStorageService extends Disposable implements IStorageService {
_serviceBrand: any;
private _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
private _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
private closed: boolean;
private useLegacyWorkspaceStorage: boolean;
constructor(
private storageService: IStorageService,
private storageLegacyService: IStorageLegacyService,
private logService: ILogService,
configurationService: IConfigurationService
) {
super();
this.useLegacyWorkspaceStorage = configurationService.inspect<boolean>('workbench.enableLegacyStorage').value === true;
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onDidChangeStorage(e => this._onDidChangeStorage.fire(e)));
this._register(this.storageService.onWillSaveState(e => this._onWillSaveState.fire(e)));
const globalKeyMarker = 'storage://global/';
window.addEventListener('storage', e => {
if (e.key && startsWith(e.key, globalKeyMarker)) {
const key = e.key.substr(globalKeyMarker.length);
this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL });
}
});
}
get storage(): StorageService {
return this.storageService as StorageService;
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
if (!this.useLegacyWorkspaceStorage) {
if (scope === StorageScope.WORKSPACE || process.env['VSCODE_TEST_STORAGE_MIGRATION']) {
return this.storageService.get(key, scope, fallbackValue);
}
}
return this.storageLegacyService.get(key, this.convertScope(scope), fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
if (!this.useLegacyWorkspaceStorage) {
if (scope === StorageScope.WORKSPACE || process.env['VSCODE_TEST_STORAGE_MIGRATION']) {
return this.storageService.getBoolean(key, scope, fallbackValue);
}
}
return this.storageLegacyService.getBoolean(key, this.convertScope(scope), fallbackValue);
}
getInteger(key: string, scope: StorageScope, fallbackValue: number): number;
getInteger(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
if (!this.useLegacyWorkspaceStorage) {
if (scope === StorageScope.WORKSPACE || process.env['VSCODE_TEST_STORAGE_MIGRATION']) {
return this.storageService.getInteger(key, scope, fallbackValue);
}
}
return this.storageLegacyService.getInteger(key, this.convertScope(scope), fallbackValue);
}
store(key: string, value: any, scope: StorageScope): void {
if (this.closed) {
this.logService.warn(`Unsupported write (store) access after close (key: ${key})`);
return; // prevent writing after close to detect late write access
}
this.storageLegacyService.store(key, value, this.convertScope(scope));
this.storageService.store(key, value, scope);
}
remove(key: string, scope: StorageScope): void {
if (this.closed) {
this.logService.warn(`Unsupported write (remove) access after close (key: ${key})`);
return; // prevent writing after close to detect late write access
}
this.storageLegacyService.remove(key, this.convertScope(scope));
this.storageService.remove(key, scope);
}
close(): Promise<void> {
const promise = this.storage.close();
this.closed = true;
return promise;
}
private convertScope(scope: StorageScope): StorageLegacyScope {
return scope === StorageScope.GLOBAL ? StorageLegacyScope.GLOBAL : StorageLegacyScope.WORKSPACE;
}
}

View File

@@ -1,18 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { StorageService } from 'vs/platform/storage/common/storageService';
import { parseStorage, migrateStorageToMultiRootWorkspace } from 'vs/platform/storage/common/migration';
import URI from 'vs/base/common/uri';
import { StorageScope } from 'vs/platform/storage/common/storage';
import { startsWith } from 'vs/base/common/strings';
suite('Storage Migration', () => {
test('Parse Storage (Global)', () => {
});
});

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { StorageLegacyScope, StorageLegacyService } from 'vs/platform/storage/common/storageLegacyService';
import { parseEmptyStorage, parseMultiRootStorage, parseFolderStorage } from 'vs/platform/storage/common/storageLegacyMigration';
import { URI } from 'vs/base/common/uri';
import { startsWith } from 'vs/base/common/strings';
suite('Storage Migration', () => {
let storage = window.localStorage;
setup(() => {
storage.clear();
});
teardown(() => {
storage.clear();
});
test('Parse Storage (mixed)', () => {
// Fill the storage with multiple workspaces of all kinds (empty, root, folders)
const workspaceIds = [
// Multi Root Workspace
URI.from({ path: '1500007676869', scheme: 'root' }).toString(),
URI.from({ path: '2500007676869', scheme: 'root' }).toString(),
URI.from({ path: '3500007676869', scheme: 'root' }).toString(),
// Empty Workspace
URI.from({ path: '4500007676869', scheme: 'empty' }).toString(),
URI.from({ path: '5500007676869', scheme: 'empty' }).toString(),
URI.from({ path: '6500007676869', scheme: 'empty' }).toString(),
// Unix Paths
URI.file('/some/folder/folder1').toString(),
URI.file('/some/folder/folder2').toString(),
URI.file('/some/folder/folder3').toString(),
URI.file('/some/folder/folder1/sub1').toString(),
URI.file('/some/folder/folder2/sub2').toString(),
URI.file('/some/folder/folder3/sub3').toString(),
// Windows Paths
URI.file('c:\\some\\folder\\folder1').toString(),
URI.file('c:\\some\\folder\\folder2').toString(),
URI.file('c:\\some\\folder\\folder3').toString(),
URI.file('c:\\some\\folder\\folder1\\sub1').toString(),
URI.file('c:\\some\\folder\\folder2\\sub2').toString(),
URI.file('c:\\some\\folder\\folder3\\sub3').toString(),
// UNC Paths
'file://localhost/c%3A/some/folder/folder1',
'file://localhost/c%3A/some/folder/folder2',
'file://localhost/c%3A/some/folder/folder3',
'file://localhost/c%3A/some/folder/folder1/sub1',
'file://localhost/c%3A/some/folder/folder2/sub2',
'file://localhost/c%3A/some/folder/folder3/sub3'
];
const services = workspaceIds.map(id => createService(id));
services.forEach((service, index) => {
let expectedKeyCount = 4;
let storageToTest;
const workspaceId = workspaceIds[index];
if (startsWith(workspaceId, 'file:')) {
storageToTest = parseFolderStorage(storage, workspaceId);
expectedKeyCount++; // workspaceIdentifier gets added!
} else if (startsWith(workspaceId, 'empty:')) {
storageToTest = parseEmptyStorage(storage, workspaceId);
} else if (startsWith(workspaceId, 'root:')) {
storageToTest = parseMultiRootStorage(storage, workspaceId);
}
assert.equal(Object.keys(storageToTest).length, expectedKeyCount, 's');
assert.equal(storageToTest['key1'], service.get('key1', StorageLegacyScope.WORKSPACE));
assert.equal(storageToTest['key2.something'], service.get('key2.something', StorageLegacyScope.WORKSPACE));
assert.equal(storageToTest['key3/special'], service.get('key3/special', StorageLegacyScope.WORKSPACE));
assert.equal(storageToTest['key4 space'], service.get('key4 space', StorageLegacyScope.WORKSPACE));
});
});
test('Parse Storage (handle subfolders properly)', () => {
const ws1 = URI.file('/some/folder/folder1').toString();
const ws2 = URI.file('/some/folder/folder1/sub1').toString();
const s1 = new StorageLegacyService(storage, storage, ws1, Date.now());
const s2 = new StorageLegacyService(storage, storage, ws2, Date.now());
s1.store('s1key1', 'value1', StorageLegacyScope.WORKSPACE);
s1.store('s1key2.something', JSON.stringify({ foo: 'bar' }), StorageLegacyScope.WORKSPACE);
s1.store('s1key3/special', true, StorageLegacyScope.WORKSPACE);
s1.store('s1key4 space', 4, StorageLegacyScope.WORKSPACE);
s2.store('s2key1', 'value1', StorageLegacyScope.WORKSPACE);
s2.store('s2key2.something', JSON.stringify({ foo: 'bar' }), StorageLegacyScope.WORKSPACE);
s2.store('s2key3/special', true, StorageLegacyScope.WORKSPACE);
s2.store('s2key4 space', 4, StorageLegacyScope.WORKSPACE);
const s1Storage = parseFolderStorage(storage, ws1);
assert.equal(Object.keys(s1Storage).length, 5);
assert.equal(s1Storage['s1key1'], s1.get('s1key1', StorageLegacyScope.WORKSPACE));
assert.equal(s1Storage['s1key2.something'], s1.get('s1key2.something', StorageLegacyScope.WORKSPACE));
assert.equal(s1Storage['s1key3/special'], s1.get('s1key3/special', StorageLegacyScope.WORKSPACE));
assert.equal(s1Storage['s1key4 space'], s1.get('s1key4 space', StorageLegacyScope.WORKSPACE));
const s2Storage = parseFolderStorage(storage, ws2);
assert.equal(Object.keys(s2Storage).length, 5);
assert.equal(s2Storage['s2key1'], s2.get('s2key1', StorageLegacyScope.WORKSPACE));
assert.equal(s2Storage['s2key2.something'], s2.get('s2key2.something', StorageLegacyScope.WORKSPACE));
assert.equal(s2Storage['s2key3/special'], s2.get('s2key3/special', StorageLegacyScope.WORKSPACE));
assert.equal(s2Storage['s2key4 space'], s2.get('s2key4 space', StorageLegacyScope.WORKSPACE));
});
function createService(workspaceId?: string): StorageLegacyService {
const service = new StorageLegacyService(storage, storage, workspaceId, workspaceId && startsWith(workspaceId, 'file:') ? Date.now() : void 0);
// Unrelated
storage.setItem('foo', 'bar');
storage.setItem('storage://foo', 'bar');
storage.setItem('storage://global/storage://foo', 'bar');
// Global
service.store('key1', 'value1');
service.store('key2.something', JSON.stringify({ foo: 'bar' }));
service.store('key3/special', true);
service.store('key4 space', 4);
// Workspace
service.store('key1', 'value1', StorageLegacyScope.WORKSPACE);
service.store('key2.something', JSON.stringify({ foo: 'bar' }), StorageLegacyScope.WORKSPACE);
service.store('key3/special', true, StorageLegacyScope.WORKSPACE);
service.store('key4 space', 4, StorageLegacyScope.WORKSPACE);
return service;
}
});

View File

@@ -1,99 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService, IWorkspace, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService';
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
suite('Workbench StorageSevice', () => {
let contextService: IWorkspaceContextService;
let instantiationService: TestInstantiationService;
setup(() => {
instantiationService = new TestInstantiationService();
contextService = instantiationService.stub(IWorkspaceContextService, <IWorkspaceContextService>{
getWorkbenchState: () => WorkbenchState.FOLDER,
getWorkspace: () => {
return <IWorkspace>TestWorkspace;
}
});
});
test('Remove Data', () => {
let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace().id);
s.store('Monaco.IDE.Core.Storage.Test.remove', 'foobar');
assert.strictEqual('foobar', s.get('Monaco.IDE.Core.Storage.Test.remove'));
s.remove('Monaco.IDE.Core.Storage.Test.remove');
assert.ok(!s.get('Monaco.IDE.Core.Storage.Test.remove'));
});
test('Get Data, Integer, Boolean', () => {
let s = new StorageService(new InMemoryLocalStorage(), null, contextService.getWorkspace().id);
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get', StorageScope.GLOBAL, 'foobar'), 'foobar');
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get', StorageScope.GLOBAL, ''), '');
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.getInteger', StorageScope.GLOBAL, 5), 5);
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.getInteger', StorageScope.GLOBAL, 0), 0);
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.getBoolean', StorageScope.GLOBAL, true), true);
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.getBoolean', StorageScope.GLOBAL, false), false);
s.store('Monaco.IDE.Core.Storage.Test.get', 'foobar');
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get'), 'foobar');
s.store('Monaco.IDE.Core.Storage.Test.get', '');
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.get'), '');
s.store('Monaco.IDE.Core.Storage.Test.getInteger', 5);
assert.strictEqual(s.getInteger('Monaco.IDE.Core.Storage.Test.getInteger'), 5);
s.store('Monaco.IDE.Core.Storage.Test.getInteger', 0);
assert.strictEqual(s.getInteger('Monaco.IDE.Core.Storage.Test.getInteger'), 0);
s.store('Monaco.IDE.Core.Storage.Test.getBoolean', true);
assert.strictEqual(s.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean'), true);
s.store('Monaco.IDE.Core.Storage.Test.getBoolean', false);
assert.strictEqual(s.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean'), false);
assert.strictEqual(s.get('Monaco.IDE.Core.Storage.Test.getDefault', StorageScope.GLOBAL, 'getDefault'), 'getDefault');
assert.strictEqual(s.getInteger('Monaco.IDE.Core.Storage.Test.getIntegerDefault', StorageScope.GLOBAL, 5), 5);
assert.strictEqual(s.getBoolean('Monaco.IDE.Core.Storage.Test.getBooleanDefault', StorageScope.GLOBAL, true), true);
});
test('StorageSevice cleans up when workspace changes', () => {
let storageImpl = new InMemoryLocalStorage();
let time = new Date().getTime();
let s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time);
s.store('key1', 'foobar');
s.store('key2', 'something');
s.store('wkey1', 'foo', StorageScope.WORKSPACE);
s.store('wkey2', 'foo2', StorageScope.WORKSPACE);
s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time);
assert.strictEqual(s.get('key1', StorageScope.GLOBAL), 'foobar');
assert.strictEqual(s.get('key1', StorageScope.WORKSPACE, null), null);
assert.strictEqual(s.get('key2', StorageScope.GLOBAL), 'something');
assert.strictEqual(s.get('wkey1', StorageScope.WORKSPACE), 'foo');
assert.strictEqual(s.get('wkey2', StorageScope.WORKSPACE), 'foo2');
s = new StorageService(storageImpl, null, contextService.getWorkspace().id, time + 100);
assert.strictEqual(s.get('key1', StorageScope.GLOBAL), 'foobar');
assert.strictEqual(s.get('key1', StorageScope.WORKSPACE, null), null);
assert.strictEqual(s.get('key2', StorageScope.GLOBAL), 'something');
assert(!s.get('wkey1', StorageScope.WORKSPACE));
assert(!s.get('wkey2', StorageScope.WORKSPACE));
});
});

View File

@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok, equal } from 'assert';
import { StorageScope } from 'vs/platform/storage/common/storage';
import { TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { StorageService } from 'vs/platform/storage/node/storageService';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'path';
import { tmpdir } from 'os';
import { mkdirp, del } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/node/storage';
suite('StorageService', () => {
test('Remove Data (global, in-memory)', () => {
removeData(StorageScope.GLOBAL);
});
test('Remove Data (workspace, in-memory)', () => {
removeData(StorageScope.WORKSPACE);
});
function removeData(scope: StorageScope): void {
const storage = new TestStorageService();
storage.store('Monaco.IDE.Core.Storage.Test.remove', 'foobar', scope);
strictEqual('foobar', storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, void 0));
storage.remove('Monaco.IDE.Core.Storage.Test.remove', scope);
ok(!storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, void 0));
}
test('Get Data, Integer, Boolean (global, in-memory)', () => {
storeData(StorageScope.GLOBAL);
});
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
storeData(StorageScope.WORKSPACE);
});
function storeData(scope: StorageScope): void {
const storage = new TestStorageService();
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, ''), '');
strictEqual(storage.getInteger('Monaco.IDE.Core.Storage.Test.getInteger', scope, 5), 5);
strictEqual(storage.getInteger('Monaco.IDE.Core.Storage.Test.getInteger', scope, 0), 0);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, false), false);
storage.store('Monaco.IDE.Core.Storage.Test.get', 'foobar', scope);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, void 0), 'foobar');
storage.store('Monaco.IDE.Core.Storage.Test.get', '', scope);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, void 0), '');
storage.store('Monaco.IDE.Core.Storage.Test.getInteger', 5, scope);
strictEqual(storage.getInteger('Monaco.IDE.Core.Storage.Test.getInteger', scope, void 0), 5);
storage.store('Monaco.IDE.Core.Storage.Test.getInteger', 0, scope);
strictEqual(storage.getInteger('Monaco.IDE.Core.Storage.Test.getInteger', scope, void 0), 0);
storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', true, scope);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, void 0), true);
storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', false, scope);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, void 0), false);
strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getInteger('Monaco.IDE.Core.Storage.Test.getIntegerDefault', scope, 5), 5);
strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBooleanDefault', scope, true), true);
}
function uniqueStorageDir(): string {
const id = generateUuid();
return join(tmpdir(), 'vsctests', id, 'storage2', id);
}
test('Migrate Data', async () => {
class StorageTestEnvironmentService extends EnvironmentService {
constructor(private workspaceStorageFolderPath: string, private _extensionsPath) {
super(parseArgs(process.argv), process.execPath);
}
get workspaceStorageHome(): string {
return this.workspaceStorageFolderPath;
}
get extensionsPath(): string {
return this._extensionsPath;
}
}
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storage = new StorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(storageDir, storageDir));
await storage.initialize({ id: String(Date.now()) });
storage.store('bar', 'foo', StorageScope.WORKSPACE);
storage.store('barNumber', 55, StorageScope.WORKSPACE);
storage.store('barBoolean', true, StorageScope.GLOBAL);
await storage.migrate({ id: String(Date.now() + 100) });
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
equal(storage.getInteger('barNumber', StorageScope.WORKSPACE), 55);
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.close();
await del(storageDir, tmpdir());
});
});