mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8 (#14883)
* Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8 * Bump distro * Upgrade GCC to 4.9 due to yarn install errors * Update build image * Fix bootstrap base url * Bump distro * Fix build errors * Update source map file * Disable checkbox for blocking migration issues (#15131) * disable checkbox for blocking issues * wip * disable checkbox fixes * fix strings * Remove duplicate tsec command * Default to off for tab color if settings not present * re-skip failing tests * Fix mocha error * Bump sqlite version & fix notebooks search view * Turn off esbuild warnings * Update esbuild log level * Fix overflowactionbar tests * Fix ts-ignore in dropdown tests * cleanup/fixes * Fix hygiene * Bundle in entire zone.js module * Remove extra constructor param * bump distro for web compile break * bump distro for web compile break v2 * Undo log level change * New distro * Fix integration test scripts * remove the "no yarn.lock changes" workflow * fix scripts v2 * Update unit test scripts * Ensure ads-kerberos2 updates in .vscodeignore * Try fix unit tests * Upload crash reports * remove nogpu * always upload crashes * Use bash script * Consolidate data/ext dir names * Create in tmp directory Co-authored-by: chlafreniere <hichise@gmail.com> Co-authored-by: Christopher Suh <chsuh@microsoft.com> Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
@@ -8,14 +8,24 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
|
||||
import { Throttler } from 'vs/base/common/async';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
|
||||
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
|
||||
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
|
||||
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
|
||||
|
||||
// Standard FS Errors (expected to be thrown in production when invalid FS operations are requested)
|
||||
const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown);
|
||||
|
||||
// Arbitrary Internal Errors (should never be thrown in production)
|
||||
const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown);
|
||||
|
||||
export class IndexedDB {
|
||||
|
||||
private indexedDBPromise: Promise<IDBDatabase | null>;
|
||||
@@ -38,7 +48,7 @@ export class IndexedDB {
|
||||
}
|
||||
|
||||
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
|
||||
if (browser.isEdge) {
|
||||
if (browser.isEdgeLegacy) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return new Promise((c, e) => {
|
||||
@@ -65,13 +75,140 @@ export class IndexedDB {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability {
|
||||
reset(): Promise<void>;
|
||||
}
|
||||
|
||||
type DirEntry = [string, FileType];
|
||||
|
||||
type IndexedDBFileSystemEntry =
|
||||
| {
|
||||
path: string,
|
||||
type: FileType.Directory,
|
||||
children: Map<string, IndexedDBFileSystemNode>,
|
||||
}
|
||||
| {
|
||||
path: string,
|
||||
type: FileType.File,
|
||||
size: number | undefined,
|
||||
};
|
||||
|
||||
class IndexedDBFileSystemNode {
|
||||
public type: FileType;
|
||||
|
||||
constructor(private entry: IndexedDBFileSystemEntry) {
|
||||
this.type = entry.type;
|
||||
}
|
||||
|
||||
|
||||
read(path: string) {
|
||||
return this.doRead(path.split('/').filter(p => p.length));
|
||||
}
|
||||
|
||||
private doRead(pathParts: string[]): IndexedDBFileSystemEntry | undefined {
|
||||
if (pathParts.length === 0) { return this.entry; }
|
||||
if (this.entry.type !== FileType.Directory) {
|
||||
throw ERR_UNKNOWN_INTERNAL('Internal error reading from IndexedDBFSNode -- expected directory at ' + this.entry.path);
|
||||
}
|
||||
const next = this.entry.children.get(pathParts[0]);
|
||||
|
||||
if (!next) { return undefined; }
|
||||
return next.doRead(pathParts.slice(1));
|
||||
}
|
||||
|
||||
delete(path: string) {
|
||||
const toDelete = path.split('/').filter(p => p.length);
|
||||
if (toDelete.length === 0) {
|
||||
if (this.entry.type !== FileType.Directory) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode. Expected root entry to be directory`);
|
||||
}
|
||||
this.entry.children.clear();
|
||||
} else {
|
||||
return this.doDelete(toDelete, path);
|
||||
}
|
||||
}
|
||||
|
||||
private doDelete = (pathParts: string[], originalPath: string) => {
|
||||
if (pathParts.length === 0) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode -- got no deletion path parts (encountered while deleting ${originalPath})`);
|
||||
}
|
||||
else if (this.entry.type !== FileType.Directory) {
|
||||
throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected directory at ' + this.entry.path);
|
||||
}
|
||||
else if (pathParts.length === 1) {
|
||||
this.entry.children.delete(pathParts[0]);
|
||||
}
|
||||
else {
|
||||
const next = this.entry.children.get(pathParts[0]);
|
||||
if (!next) {
|
||||
throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected entry at ' + this.entry.path + '/' + next);
|
||||
}
|
||||
next.doDelete(pathParts.slice(1), originalPath);
|
||||
}
|
||||
};
|
||||
|
||||
add(path: string, entry: { type: 'file', size?: number } | { type: 'dir' }) {
|
||||
this.doAdd(path.split('/').filter(p => p.length), entry, path);
|
||||
}
|
||||
|
||||
private doAdd(pathParts: string[], entry: { type: 'file', size?: number } | { type: 'dir' }, originalPath: string) {
|
||||
if (pathParts.length === 0) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- adding empty path (encountered while adding ${originalPath})`);
|
||||
}
|
||||
else if (this.entry.type !== FileType.Directory) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- parent is not a directory (encountered while adding ${originalPath})`);
|
||||
}
|
||||
else if (pathParts.length === 1) {
|
||||
const next = pathParts[0];
|
||||
const existing = this.entry.children.get(next);
|
||||
if (entry.type === 'dir') {
|
||||
if (existing?.entry.type === FileType.File) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
|
||||
}
|
||||
this.entry.children.set(next, existing ?? new IndexedDBFileSystemNode({
|
||||
type: FileType.Directory,
|
||||
path: this.entry.path + '/' + next,
|
||||
children: new Map(),
|
||||
}));
|
||||
} else {
|
||||
if (existing?.entry.type === FileType.Directory) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting directory with file: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
|
||||
}
|
||||
this.entry.children.set(next, new IndexedDBFileSystemNode({
|
||||
type: FileType.File,
|
||||
path: this.entry.path + '/' + next,
|
||||
size: entry.size,
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (pathParts.length > 1) {
|
||||
const next = pathParts[0];
|
||||
let childNode = this.entry.children.get(next);
|
||||
if (!childNode) {
|
||||
childNode = new IndexedDBFileSystemNode({
|
||||
children: new Map(),
|
||||
path: this.entry.path + '/' + next,
|
||||
type: FileType.Directory
|
||||
});
|
||||
this.entry.children.set(next, childNode);
|
||||
}
|
||||
else if (childNode.type === FileType.File) {
|
||||
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file entry with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
|
||||
}
|
||||
childNode.doAdd(pathParts.slice(1), entry, originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
print(indentation = '') {
|
||||
console.log(indentation + this.entry.path);
|
||||
if (this.entry.type === FileType.Directory) {
|
||||
this.entry.children.forEach(child => child.print(indentation + ' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider {
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
@@ -83,11 +220,14 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
|
||||
|
||||
private readonly versions: Map<string, number> = new Map<string, number>();
|
||||
private readonly dirs: Set<string> = new Set<string>();
|
||||
|
||||
constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
|
||||
private cachedFiletree: Promise<IndexedDBFileSystemNode> | undefined;
|
||||
private writeManyThrottler: Throttler;
|
||||
|
||||
constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
|
||||
super();
|
||||
this.dirs.add('/');
|
||||
this.writeManyThrottler = new Throttler();
|
||||
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
@@ -98,29 +238,22 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
|
||||
try {
|
||||
const resourceStat = await this.stat(resource);
|
||||
if (resourceStat.type === FileType.File) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
throw ERR_FILE_NOT_DIR;
|
||||
}
|
||||
} catch (error) { /* Ignore */ }
|
||||
|
||||
// Make sure parent dir exists
|
||||
await this.stat(dirname(resource));
|
||||
|
||||
this.dirs.add(resource.path);
|
||||
(await this.getFiletree()).add(resource.path, { type: 'dir' });
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const content = await this.readFile(resource);
|
||||
const content = (await this.getFiletree()).read(resource.path);
|
||||
if (content?.type === FileType.File) {
|
||||
return {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: this.versions.get(resource.toString()) || 0,
|
||||
size: content.byteLength
|
||||
size: content.size ?? (await this.readFile(resource)).byteLength
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
} else if (content?.type === FileType.Directory) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
@@ -128,75 +261,112 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
if (this.dirs.has(resource.path)) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
else {
|
||||
throw ERR_FILE_NOT_FOUND;
|
||||
}
|
||||
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
async readdir(resource: URI): Promise<DirEntry[]> {
|
||||
const entry = (await this.getFiletree()).read(resource.path);
|
||||
if (!entry) {
|
||||
// Dirs aren't saved to disk, so empty dirs will be lost on reload.
|
||||
// Thus we have two options for what happens when you try to read a dir and nothing is found:
|
||||
// - Throw FileSystemProviderErrorCode.FileNotFound
|
||||
// - Return []
|
||||
// We choose to return [] as creating a dir then reading it (even after reload) should not throw an error.
|
||||
return [];
|
||||
}
|
||||
const keys = await this.getAllKeys();
|
||||
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
|
||||
for (const key of keys) {
|
||||
const keyResource = this.toResource(key);
|
||||
if (extUri.isEqualOrParent(keyResource, resource)) {
|
||||
const path = extUri.relativePath(resource, keyResource);
|
||||
if (path) {
|
||||
const keySegments = path.split('/');
|
||||
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
}
|
||||
if (entry.type !== FileType.Directory) {
|
||||
throw ERR_FILE_NOT_DIR;
|
||||
}
|
||||
else {
|
||||
return [...entry.children.entries()].map(([name, node]) => [name, node.type]);
|
||||
}
|
||||
return [...files.values()];
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
const value = await this.getValue(resource.path);
|
||||
if (typeof value === 'string') {
|
||||
return VSBuffer.fromString(value).buffer;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
const buffer = await new Promise<Uint8Array>((c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.get(resource.path);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
if (request.result instanceof Uint8Array) {
|
||||
c(request.result);
|
||||
} else if (typeof request.result === 'string') {
|
||||
c(VSBuffer.fromString(request.result).buffer);
|
||||
}
|
||||
else {
|
||||
if (request.result === undefined) {
|
||||
e(ERR_FILE_NOT_FOUND);
|
||||
} else {
|
||||
e(ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
(await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength });
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
}
|
||||
const existing = await this.stat(resource).catch(() => undefined);
|
||||
if (existing?.type === FileType.Directory) {
|
||||
throw ERR_FILE_IS_DIR;
|
||||
}
|
||||
await this.setValue(resource.path, content);
|
||||
|
||||
this.fileWriteBatch.push({ content, resource });
|
||||
await this.writeManyThrottler.queue(() => this.writeMany());
|
||||
(await this.getFiletree()).add(resource.path, { type: 'file', size: content.byteLength });
|
||||
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
await this.deleteKey(resource.path);
|
||||
this.versions.delete(resource.path);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
|
||||
return;
|
||||
let stat: IStat;
|
||||
try {
|
||||
stat = await this.stat(resource);
|
||||
} catch (e) {
|
||||
if (e.code === FileSystemProviderErrorCode.FileNotFound) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
let toDelete: string[];
|
||||
if (opts.recursive) {
|
||||
const files = await this.readdir(resource);
|
||||
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
|
||||
const tree = (await this.tree(resource));
|
||||
toDelete = tree.map(([path]) => path);
|
||||
} else {
|
||||
if (stat.type === FileType.Directory && (await this.readdir(resource)).length) {
|
||||
throw ERR_DIR_NOT_EMPTY;
|
||||
}
|
||||
toDelete = [resource.path];
|
||||
}
|
||||
await this.deleteKeys(toDelete);
|
||||
(await this.getFiletree()).delete(resource.path);
|
||||
toDelete.forEach(key => this.versions.delete(key));
|
||||
this._onDidChangeFile.fire(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED })));
|
||||
}
|
||||
|
||||
private async tree(resource: URI): Promise<DirEntry[]> {
|
||||
if ((await this.stat(resource)).type === FileType.Directory) {
|
||||
const topLevelEntries = (await this.readdir(resource)).map(([key, type]) => {
|
||||
return [joinPath(resource, key).path, type] as [string, FileType];
|
||||
});
|
||||
let allEntries = topLevelEntries;
|
||||
await Promise.all(topLevelEntries.map(
|
||||
async ([key, type]) => {
|
||||
if (type === FileType.Directory) {
|
||||
const childEntries = (await this.tree(resource.with({ path: key })));
|
||||
allEntries = allEntries.concat(childEntries);
|
||||
}
|
||||
}));
|
||||
return allEntries;
|
||||
} else {
|
||||
const entries: DirEntry[] = [[resource.path, FileType.File]];
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,58 +374,57 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
|
||||
return Promise.reject(new Error('Not Supported'));
|
||||
}
|
||||
|
||||
private toResource(key: string): URI {
|
||||
return URI.file(key).with({ scheme: this.scheme });
|
||||
private getFiletree(): Promise<IndexedDBFileSystemNode> {
|
||||
if (!this.cachedFiletree) {
|
||||
this.cachedFiletree = new Promise((c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.getAllKeys();
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
const rootNode = new IndexedDBFileSystemNode({
|
||||
children: new Map(),
|
||||
path: '',
|
||||
type: FileType.Directory
|
||||
});
|
||||
const keys = request.result.map(key => key.toString());
|
||||
keys.forEach(key => rootNode.add(key, { type: 'file' }));
|
||||
c(rootNode);
|
||||
};
|
||||
});
|
||||
}
|
||||
return this.cachedFiletree;
|
||||
}
|
||||
|
||||
async getAllKeys(): Promise<string[]> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.getAllKeys();
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(<string[]>request.result);
|
||||
});
|
||||
}
|
||||
private fileWriteBatch: { resource: URI, content: Uint8Array }[] = [];
|
||||
private async writeMany() {
|
||||
return new Promise<void>((c, e) => {
|
||||
const fileBatch = this.fileWriteBatch;
|
||||
this.fileWriteBatch = [];
|
||||
if (fileBatch.length === 0) { return c(); }
|
||||
|
||||
hasKey(key: string): Promise<boolean> {
|
||||
return new Promise<boolean>(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.getKey(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
c(!!request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getValue(key: string): Promise<Uint8Array | string> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.get(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(request.result || '');
|
||||
});
|
||||
}
|
||||
|
||||
setValue(key: string, value: Uint8Array): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
transaction.onerror = () => e(transaction.error);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.put(value, key);
|
||||
request.onerror = () => e(request.error);
|
||||
let request: IDBRequest = undefined!;
|
||||
for (const entry of fileBatch) {
|
||||
request = objectStore.put(entry.content, entry.resource.path);
|
||||
}
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
deleteKey(key: string): Promise<void> {
|
||||
private deleteKeys(keys: string[]): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
if (keys.length === 0) { return c(); }
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
transaction.onerror = () => e(transaction.error);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.delete(key);
|
||||
request.onerror = () => e(request.error);
|
||||
let request: IDBRequest = undefined!;
|
||||
for (const key of keys) {
|
||||
request = objectStore.delete(key);
|
||||
}
|
||||
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
|
||||
import { getBaseLabel } from 'vs/base/common/labels';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
|
||||
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream } from 'vs/base/common/stream';
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
|
||||
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream';
|
||||
import { Promises, Queue } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { readFileIntoStream } from 'vs/platform/files/common/io';
|
||||
@@ -49,6 +49,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);
|
||||
}
|
||||
|
||||
mark(`code/registerFilesystem/${scheme}`);
|
||||
|
||||
// Add provider with event
|
||||
this.provider.set(scheme, provider);
|
||||
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
|
||||
@@ -89,7 +91,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// If the provider is not yet there, make sure to join on the listeners assuming
|
||||
// that it takes a bit longer to register the file system provider.
|
||||
await Promise.all(joiners);
|
||||
await Promises.settled(joiners);
|
||||
}
|
||||
|
||||
canHandleResource(resource: URI): boolean {
|
||||
@@ -102,7 +104,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return !!(provider && (provider.capabilities & capability));
|
||||
}
|
||||
|
||||
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> {
|
||||
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities; }> {
|
||||
return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
|
||||
}
|
||||
|
||||
@@ -215,14 +217,15 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;
|
||||
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;
|
||||
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStatWithMetadata>;
|
||||
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
|
||||
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
|
||||
const { providerExtUri } = this.getExtUri(provider);
|
||||
|
||||
// convert to file stat
|
||||
const fileStat: IFileStat = {
|
||||
resource,
|
||||
name: getBaseLabel(resource),
|
||||
name: providerExtUri.basename(resource),
|
||||
isFile: (stat.type & FileType.File) !== 0,
|
||||
isDirectory: (stat.type & FileType.Directory) !== 0,
|
||||
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
|
||||
@@ -236,9 +239,9 @@ export class FileService extends Disposable implements IFileService {
|
||||
if (fileStat.isDirectory && recurse(fileStat, siblings)) {
|
||||
try {
|
||||
const entries = await provider.readdir(resource);
|
||||
const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => {
|
||||
const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
|
||||
try {
|
||||
const childResource = joinPath(resource, name);
|
||||
const childResource = providerExtUri.joinPath(resource, name);
|
||||
const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
|
||||
|
||||
return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
|
||||
@@ -263,10 +266,10 @@ export class FileService extends Disposable implements IFileService {
|
||||
return fileStat;
|
||||
}
|
||||
|
||||
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]>;
|
||||
async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise<IResolveFileResultWithMetadata[]>;
|
||||
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]>;
|
||||
async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise<IResolveFileResultWithMetadata[]>;
|
||||
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
|
||||
return Promise.all(toResolve.map(async entry => {
|
||||
return Promises.settled(toResolve.map(async entry => {
|
||||
try {
|
||||
return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
|
||||
} catch (error) {
|
||||
@@ -327,6 +330,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
|
||||
const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);
|
||||
const { providerExtUri } = this.getExtUri(provider);
|
||||
|
||||
try {
|
||||
|
||||
@@ -335,7 +339,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// mkdir recursively as needed
|
||||
if (!stat) {
|
||||
await this.mkdirp(provider, dirname(resource));
|
||||
await this.mkdirp(provider, providerExtUri.dirname(resource));
|
||||
}
|
||||
|
||||
// optimization: if the provider has unbuffered write capability and the data
|
||||
@@ -435,7 +439,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return this.doReadAsFileStream(provider, resource, options);
|
||||
}
|
||||
|
||||
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean }): Promise<IFileStreamContent> {
|
||||
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
|
||||
|
||||
// install a cancellation token that gets cancelled
|
||||
// when any error occurs. this allows us to resolve
|
||||
@@ -450,6 +454,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
throw error;
|
||||
});
|
||||
|
||||
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
|
||||
|
||||
try {
|
||||
|
||||
// if the etag is provided, we await the result of the validation
|
||||
@@ -460,30 +466,41 @@ export class FileService extends Disposable implements IFileService {
|
||||
await statPromise;
|
||||
}
|
||||
|
||||
let fileStreamPromise: Promise<VSBufferReadableStream>;
|
||||
let fileStream: VSBufferReadableStream | undefined = undefined;
|
||||
|
||||
// read unbuffered (only if either preferred, or the provider has no buffered read capability)
|
||||
if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) {
|
||||
fileStreamPromise = this.readFileUnbuffered(provider, resource, options);
|
||||
fileStream = this.readFileUnbuffered(provider, resource, options);
|
||||
}
|
||||
|
||||
// read streamed (always prefer over primitive buffered read)
|
||||
else if (hasFileReadStreamCapability(provider)) {
|
||||
fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options));
|
||||
fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options);
|
||||
}
|
||||
|
||||
// read buffered
|
||||
else {
|
||||
fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options));
|
||||
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
|
||||
}
|
||||
|
||||
const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]);
|
||||
// observe the stream for the error case below
|
||||
fileStreamObserver = observe(fileStream);
|
||||
|
||||
const fileStat = await statPromise;
|
||||
|
||||
return {
|
||||
...fileStat,
|
||||
value: fileStream
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
// Await the stream to finish so that we exit this method
|
||||
// in a consistent state with file handles closed
|
||||
// (https://github.com/microsoft/vscode/issues/114024)
|
||||
if (fileStreamObserver) {
|
||||
await fileStreamObserver.errorOrEnd();
|
||||
}
|
||||
|
||||
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
|
||||
}
|
||||
}
|
||||
@@ -509,23 +526,36 @@ export class FileService extends Disposable implements IFileService {
|
||||
return stream;
|
||||
}
|
||||
|
||||
private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise<VSBufferReadableStream> {
|
||||
let buffer = await provider.readFile(resource);
|
||||
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
|
||||
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
|
||||
|
||||
// respect position option
|
||||
if (options && typeof options.position === 'number') {
|
||||
buffer = buffer.slice(options.position);
|
||||
}
|
||||
// Read the file into the stream async but do not wait for
|
||||
// this to complete because streams work via events
|
||||
(async () => {
|
||||
try {
|
||||
let buffer = await provider.readFile(resource);
|
||||
|
||||
// respect length option
|
||||
if (options && typeof options.length === 'number') {
|
||||
buffer = buffer.slice(0, options.length);
|
||||
}
|
||||
// respect position option
|
||||
if (options && typeof options.position === 'number') {
|
||||
buffer = buffer.slice(options.position);
|
||||
}
|
||||
|
||||
// Throw if file is too large to load
|
||||
this.validateReadFileLimits(resource, buffer.byteLength, options);
|
||||
// respect length option
|
||||
if (options && typeof options.length === 'number') {
|
||||
buffer = buffer.slice(0, options.length);
|
||||
}
|
||||
|
||||
return bufferToStream(VSBuffer.wrap(buffer));
|
||||
// Throw if file is too large to load
|
||||
this.validateReadFileLimits(resource, buffer.byteLength, options);
|
||||
|
||||
// End stream with data
|
||||
stream.end(VSBuffer.wrap(buffer));
|
||||
} catch (err) {
|
||||
stream.error(err);
|
||||
}
|
||||
})();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
|
||||
@@ -634,7 +664,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
// create parent folders
|
||||
await this.mkdirp(targetProvider, dirname(target));
|
||||
await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
|
||||
|
||||
// copy source => target
|
||||
if (mode === 'copy') {
|
||||
@@ -671,7 +701,6 @@ export class FileService extends Disposable implements IFileService {
|
||||
// across providers: copy to target & delete at source
|
||||
else {
|
||||
await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);
|
||||
|
||||
await this.del(source, { recursive: true });
|
||||
|
||||
return 'copy';
|
||||
@@ -709,8 +738,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// create children in target
|
||||
if (Array.isArray(sourceFolder.children)) {
|
||||
await Promise.all(sourceFolder.children.map(async sourceChild => {
|
||||
const targetChild = joinPath(targetFolder, sourceChild.name);
|
||||
await Promises.settled(sourceFolder.children.map(async sourceChild => {
|
||||
const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);
|
||||
if (sourceChild.isDirectory) {
|
||||
return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);
|
||||
} else {
|
||||
@@ -720,21 +749,21 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
}
|
||||
|
||||
private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> {
|
||||
private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean; }> {
|
||||
let isSameResourceWithDifferentPathCase = false;
|
||||
|
||||
// Check if source is equal or parent to target (requires providers to be the same)
|
||||
if (sourceProvider === targetProvider) {
|
||||
const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);
|
||||
const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);
|
||||
if (!isPathCaseSensitive) {
|
||||
isSameResourceWithDifferentPathCase = extUri.isEqual(source, target);
|
||||
isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);
|
||||
}
|
||||
|
||||
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
|
||||
throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target)));
|
||||
}
|
||||
|
||||
if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) {
|
||||
if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {
|
||||
throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
|
||||
}
|
||||
}
|
||||
@@ -751,8 +780,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
// Special case: if the target is a parent of the source, we cannot delete
|
||||
// it as it would delete the source as well. In this case we have to throw
|
||||
if (sourceProvider === targetProvider) {
|
||||
const { extUri } = this.getExtUri(sourceProvider);
|
||||
if (extUri.isEqualOrParent(source, target)) {
|
||||
const { providerExtUri } = this.getExtUri(sourceProvider);
|
||||
if (providerExtUri.isEqualOrParent(source, target)) {
|
||||
throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));
|
||||
}
|
||||
}
|
||||
@@ -761,11 +790,11 @@ export class FileService extends Disposable implements IFileService {
|
||||
return { exists, isSameResourceWithDifferentPathCase };
|
||||
}
|
||||
|
||||
private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } {
|
||||
private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean; } {
|
||||
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
|
||||
|
||||
return {
|
||||
extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
|
||||
providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
|
||||
isPathCaseSensitive
|
||||
};
|
||||
}
|
||||
@@ -791,8 +820,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
const directoriesToCreate: string[] = [];
|
||||
|
||||
// mkdir until we reach root
|
||||
const { extUri } = this.getExtUri(provider);
|
||||
while (!extUri.isEqual(directory, dirname(directory))) {
|
||||
const { providerExtUri } = this.getExtUri(provider);
|
||||
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
|
||||
try {
|
||||
const stat = await provider.stat(directory);
|
||||
if ((stat.type & FileType.Directory) === 0) {
|
||||
@@ -808,16 +837,16 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
// Upon error, remember directories that need to be created
|
||||
directoriesToCreate.push(basename(directory));
|
||||
directoriesToCreate.push(providerExtUri.basename(directory));
|
||||
|
||||
// Continue up
|
||||
directory = dirname(directory);
|
||||
directory = providerExtUri.dirname(directory);
|
||||
}
|
||||
}
|
||||
|
||||
// Create directories as needed
|
||||
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
|
||||
directory = joinPath(directory, directoriesToCreate[i]);
|
||||
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
|
||||
|
||||
try {
|
||||
await provider.mkdir(directory);
|
||||
@@ -894,11 +923,11 @@ export class FileService extends Disposable implements IFileService {
|
||||
private readonly _onDidFilesChange = this._register(new Emitter<FileChangesEvent>());
|
||||
readonly onDidFilesChange = this._onDidFilesChange.event;
|
||||
|
||||
private readonly activeWatchers = new Map<string, { disposable: IDisposable, count: number }>();
|
||||
private readonly activeWatchers = new Map<string, { disposable: IDisposable, count: number; }>();
|
||||
|
||||
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable {
|
||||
let watchDisposed = false;
|
||||
let watchDisposable = toDisposable(() => watchDisposed = true);
|
||||
let disposeWatch = () => { watchDisposed = true; };
|
||||
|
||||
// Watch and wire in disposable which is async but
|
||||
// check if we got disposed meanwhile and forward
|
||||
@@ -906,11 +935,11 @@ export class FileService extends Disposable implements IFileService {
|
||||
if (watchDisposed) {
|
||||
dispose(disposable);
|
||||
} else {
|
||||
watchDisposable = disposable;
|
||||
disposeWatch = () => dispose(disposable);
|
||||
}
|
||||
}, error => this.logService.error(error));
|
||||
|
||||
return toDisposable(() => dispose(watchDisposable));
|
||||
return toDisposable(() => disposeWatch());
|
||||
}
|
||||
|
||||
async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
|
||||
@@ -940,12 +969,12 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string {
|
||||
const { extUri } = this.getExtUri(provider);
|
||||
const { providerExtUri } = this.getExtUri(provider);
|
||||
|
||||
return [
|
||||
extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive
|
||||
String(options.recursive), // use recursive: true | false as part of the key
|
||||
options.excludes.join() // use excludes as part of the key
|
||||
providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive
|
||||
String(options.recursive), // use recursive: true | false as part of the key
|
||||
options.excludes.join() // use excludes as part of the key
|
||||
].join();
|
||||
}
|
||||
|
||||
@@ -963,8 +992,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
private readonly writeQueues: Map<string, Queue<void>> = new Map();
|
||||
|
||||
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
|
||||
const { extUri } = this.getExtUri(provider);
|
||||
const queueKey = extUri.getComparisonKey(resource);
|
||||
const { providerExtUri } = this.getExtUri(provider);
|
||||
const queueKey = providerExtUri.getComparisonKey(resource);
|
||||
|
||||
// ensure to never write to the same resource without finishing
|
||||
// the one write. this ensures a write finishes consistently
|
||||
@@ -1141,7 +1170,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
} catch (error) {
|
||||
throw ensureFileSystemProviderError(error);
|
||||
} finally {
|
||||
await Promise.all([
|
||||
await Promises.settled([
|
||||
typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),
|
||||
typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),
|
||||
]);
|
||||
|
||||
@@ -278,7 +278,7 @@ export interface IFileSystemProvider {
|
||||
readonly capabilities: FileSystemProviderCapabilities;
|
||||
readonly onDidChangeCapabilities: Event<void>;
|
||||
|
||||
readonly onDidErrorOccur?: Event<string>; // TODO@ben remove once file watchers are solid
|
||||
readonly onDidErrorOccur?: Event<string>; // TODO@bpasero remove once file watchers are solid
|
||||
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]>;
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable;
|
||||
@@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined
|
||||
return stat.mtime.toString(29) + stat.size.toString(31);
|
||||
}
|
||||
|
||||
export function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
|
||||
export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
|
||||
if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
@@ -58,10 +58,11 @@ async function doReadFileIntoStream<T>(provider: IFileSystemProviderWithOpenRead
|
||||
// open handle through provider
|
||||
const handle = await provider.open(resource, { create: false });
|
||||
|
||||
// Check for cancellation
|
||||
throwIfCancelled(token);
|
||||
|
||||
try {
|
||||
|
||||
// Check for cancellation
|
||||
throwIfCancelled(token);
|
||||
|
||||
let totalBytesRead = 0;
|
||||
let bytesRead = 0;
|
||||
let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined;
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs';
|
||||
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs';
|
||||
import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs';
|
||||
import { normalize, basename, dirname } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { isEqual } from 'vs/base/common/extpath';
|
||||
@@ -80,7 +80,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
|
||||
const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
|
||||
|
||||
return {
|
||||
type: this.toType(stat, symbolicLink),
|
||||
@@ -95,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
try {
|
||||
const children = await readdirWithFileTypes(this.toFilePath(resource));
|
||||
const children = await readdir(this.toFilePath(resource), { withFileTypes: true });
|
||||
|
||||
const result: [string, FileType][] = [];
|
||||
await Promise.all(children.map(async child => {
|
||||
@@ -119,7 +119,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
}
|
||||
}
|
||||
|
||||
private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType {
|
||||
private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType {
|
||||
|
||||
// Signal file type by checking for file / directory, except:
|
||||
// - symbolic links pointing to non-existing files are FileType.Unknown
|
||||
@@ -151,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
return await readFile(filePath);
|
||||
return await promises.readFile(filePath);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
@@ -212,18 +212,20 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
let flags: string | undefined = undefined;
|
||||
if (opts.create) {
|
||||
if (isWindows && await exists(filePath)) {
|
||||
if (isWindows) {
|
||||
try {
|
||||
// On Windows and if the file exists, we use a different strategy of saving the file
|
||||
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
|
||||
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
|
||||
// (see https://github.com/microsoft/vscode/issues/6363)
|
||||
await truncate(filePath, 0);
|
||||
await promises.truncate(filePath, 0);
|
||||
|
||||
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
|
||||
flags = 'r+';
|
||||
} catch (error) {
|
||||
this.logService.trace(error);
|
||||
if (error.code !== 'ENOENT') {
|
||||
this.logService.trace(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +400,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
try {
|
||||
await promisify(mkdir)(this.toFilePath(resource));
|
||||
await promises.mkdir(this.toFilePath(resource));
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
@@ -418,7 +420,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
if (opts.recursive) {
|
||||
await rimraf(filePath, RimRafMode.MOVE);
|
||||
} else {
|
||||
await unlink(filePath);
|
||||
await promises.unlink(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +465,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
|
||||
|
||||
// Copy
|
||||
await copy(fromFilePath, toFilePath);
|
||||
await copy(fromFilePath, toFilePath, { preserveSymlinks: true });
|
||||
} catch (error) {
|
||||
|
||||
// rewrite some typical errors that can happen especially around symlinks
|
||||
@@ -522,7 +524,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
return this.watchRecursive(resource, opts.excludes);
|
||||
}
|
||||
|
||||
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
|
||||
return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases
|
||||
}
|
||||
|
||||
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { statLink } from 'vs/base/node/pfs';
|
||||
import { SymlinkSupport } from 'vs/base/node/pfs';
|
||||
import { realpath } from 'vs/base/node/extpath';
|
||||
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
@@ -35,7 +35,7 @@ export class FileWatcher extends Disposable {
|
||||
|
||||
private async startWatching(): Promise<void> {
|
||||
try {
|
||||
const { stat, symbolicLink } = await statLink(this.path);
|
||||
const { stat, symbolicLink } = await SymlinkSupport.stat(this.path);
|
||||
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
@@ -47,6 +47,10 @@ export class FileWatcher extends Disposable {
|
||||
pathToWatch = await realpath(pathToWatch);
|
||||
} catch (error) {
|
||||
this.onError(error);
|
||||
|
||||
if (symbolicLink.dangling) {
|
||||
return; // give up if symbolic link is dangling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +74,9 @@ export class FileWatcher extends Disposable {
|
||||
}, error => this.onError(error)));
|
||||
}
|
||||
} catch (error) {
|
||||
this.onError(error);
|
||||
if (error.code !== 'ENOENT') {
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import * as extpath from 'vs/base/common/extpath';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import * as nsfw from 'vscode-nsfw';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
@@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
// We have to detect this case and massage the events to correct this.
|
||||
let realBasePathDiffers = false;
|
||||
let realBasePathLength = request.path.length;
|
||||
if (platform.isMacintosh) {
|
||||
if (isMacintosh) {
|
||||
try {
|
||||
|
||||
// First check for symbolic link
|
||||
@@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
for (const e of events) {
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || '');
|
||||
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
|
||||
}
|
||||
|
||||
@@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
let absolutePath: string;
|
||||
if (e.action === nsfw.actions.RENAMED) {
|
||||
// Rename fires when a file's name changes within a single directory
|
||||
absolutePath = path.join(e.directory, e.oldFile || '');
|
||||
absolutePath = join(e.directory, e.oldFile || '');
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
|
||||
} else if (this.verboseLogging) {
|
||||
this.log(` >> ignored ${absolutePath}`);
|
||||
}
|
||||
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
|
||||
absolutePath = join(e.newDirectory || e.directory, e.newFile || '');
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
|
||||
} else if (this.verboseLogging) {
|
||||
this.log(` >> ignored ${absolutePath}`);
|
||||
}
|
||||
} else {
|
||||
absolutePath = path.join(e.directory, e.file || '');
|
||||
absolutePath = join(e.directory, e.file || '');
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({
|
||||
type: nsfwActionToRawChangeType[e.action],
|
||||
@@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
const events = undeliveredFileEvents;
|
||||
undeliveredFileEvents = [];
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
if (isMacintosh) {
|
||||
events.forEach(e => {
|
||||
|
||||
// Mac uses NFD unicode form on disk, but we want NFC
|
||||
@@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
// Normalizes a set of root paths by removing any root paths that are
|
||||
// sub-paths of other roots.
|
||||
return roots.filter(r => roots.every(other => {
|
||||
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
|
||||
return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,28 +30,28 @@ suite('NSFW Watcher Service', async () => {
|
||||
test('should not impacts roots that don\'t overlap', () => {
|
||||
const service = new TestNsfwWatcherService();
|
||||
if (platform.isWindows) {
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
|
||||
} else {
|
||||
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a']), ['/a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
|
||||
}
|
||||
});
|
||||
|
||||
test('should remove sub-folders of other roots', () => {
|
||||
const service = new TestNsfwWatcherService();
|
||||
if (platform.isWindows) {
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
|
||||
} else {
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,9 +39,9 @@ export class FileWatcher extends Disposable {
|
||||
serverName: 'File Watcher (nsfw)',
|
||||
args: ['--type=watcherService'],
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
|
||||
VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
|
||||
VSCODE_PIPE_LOGGING: 'true',
|
||||
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
import * as chokidar from 'chokidar';
|
||||
import * as fs from 'fs';
|
||||
import * as gracefulFs from 'graceful-fs';
|
||||
gracefulFs.gracefulify(fs);
|
||||
import * as extpath from 'vs/base/common/extpath';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
@@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
gracefulFs.gracefulify(fs); // enable gracefulFs
|
||||
|
||||
process.noAsar = true; // disable ASAR support in watcher process
|
||||
|
||||
interface IWatcher {
|
||||
@@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extpath.isEqualOrParent(path, request.path)) {
|
||||
if (isEqualOrParent(path, request.path)) {
|
||||
if (!request.parsedPattern) {
|
||||
if (request.excludes && request.excludes.length > 0) {
|
||||
const pattern = `{${request.excludes.join(',')}}`;
|
||||
@@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string
|
||||
for (const request of requests) {
|
||||
const basePath = request.path;
|
||||
const ignored = (request.excludes || []).sort();
|
||||
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
|
||||
if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) {
|
||||
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
|
||||
result[prevRequest.path].push({ path: basePath, excludes: ignored });
|
||||
}
|
||||
|
||||
@@ -20,18 +20,18 @@ suite('Chokidar normalizeRoots', async () => {
|
||||
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
|
||||
const requests = inputPaths.map(path => newRequest(path));
|
||||
const actual = normalizeRoots(requests);
|
||||
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
|
||||
assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths);
|
||||
}
|
||||
|
||||
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
|
||||
const actual = normalizeRoots(inputRequests);
|
||||
const actualPath = Object.keys(actual).sort();
|
||||
const expectedPaths = Object.keys(expectedRequests).sort();
|
||||
assert.deepEqual(actualPath, expectedPaths);
|
||||
assert.deepStrictEqual(actualPath, expectedPaths);
|
||||
for (let path of actualPath) {
|
||||
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
assert.deepEqual(a, e);
|
||||
assert.deepStrictEqual(a, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ export class FileWatcher extends Disposable {
|
||||
serverName: 'File Watcher (chokidar)',
|
||||
args: ['--type=watcherService'],
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
|
||||
VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
|
||||
VSCODE_PIPE_LOGGING: 'true',
|
||||
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
@@ -19,7 +19,7 @@ suite('File Service', () => {
|
||||
const resource = URI.parse('test://foo/bar');
|
||||
const provider = new NullFileSystemProvider();
|
||||
|
||||
assert.equal(service.canHandleResource(resource), false);
|
||||
assert.strictEqual(service.canHandleResource(resource), false);
|
||||
|
||||
const registrations: IFileSystemProviderRegistrationEvent[] = [];
|
||||
service.onDidChangeFileSystemProviderRegistrations(e => {
|
||||
@@ -47,33 +47,35 @@ suite('File Service', () => {
|
||||
|
||||
await service.activateProvider('test');
|
||||
|
||||
assert.equal(service.canHandleResource(resource), true);
|
||||
assert.strictEqual(service.canHandleResource(resource), true);
|
||||
|
||||
assert.equal(registrations.length, 1);
|
||||
assert.equal(registrations[0].scheme, 'test');
|
||||
assert.equal(registrations[0].added, true);
|
||||
assert.strictEqual(registrations.length, 1);
|
||||
assert.strictEqual(registrations[0].scheme, 'test');
|
||||
assert.strictEqual(registrations[0].added, true);
|
||||
assert.ok(registrationDisposable);
|
||||
|
||||
assert.equal(capabilityChanges.length, 0);
|
||||
assert.strictEqual(capabilityChanges.length, 0);
|
||||
|
||||
provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy);
|
||||
assert.equal(capabilityChanges.length, 1);
|
||||
assert.strictEqual(capabilityChanges.length, 1);
|
||||
provider.setCapabilities(FileSystemProviderCapabilities.Readonly);
|
||||
assert.equal(capabilityChanges.length, 2);
|
||||
assert.strictEqual(capabilityChanges.length, 2);
|
||||
|
||||
await service.activateProvider('test');
|
||||
assert.equal(callCount, 2); // activation is called again
|
||||
assert.strictEqual(callCount, 2); // activation is called again
|
||||
|
||||
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
|
||||
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
|
||||
assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
|
||||
assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
|
||||
|
||||
registrationDisposable!.dispose();
|
||||
|
||||
assert.equal(service.canHandleResource(resource), false);
|
||||
assert.strictEqual(service.canHandleResource(resource), false);
|
||||
|
||||
assert.equal(registrations.length, 2);
|
||||
assert.equal(registrations[1].scheme, 'test');
|
||||
assert.equal(registrations[1].added, false);
|
||||
assert.strictEqual(registrations.length, 2);
|
||||
assert.strictEqual(registrations[1].scheme, 'test');
|
||||
assert.strictEqual(registrations[1].added, false);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test('watch', async () => {
|
||||
@@ -91,9 +93,9 @@ suite('File Service', () => {
|
||||
const watcher1Disposable = service.watch(resource1);
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
assert.strictEqual(disposeCounter, 0);
|
||||
watcher1Disposable.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
assert.strictEqual(disposeCounter, 1);
|
||||
|
||||
disposeCounter = 0;
|
||||
const resource2 = URI.parse('test://foo/bar2');
|
||||
@@ -102,13 +104,13 @@ suite('File Service', () => {
|
||||
const watcher2Disposable3 = service.watch(resource2);
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
assert.strictEqual(disposeCounter, 0);
|
||||
watcher2Disposable1.dispose();
|
||||
assert.equal(disposeCounter, 0);
|
||||
assert.strictEqual(disposeCounter, 0);
|
||||
watcher2Disposable2.dispose();
|
||||
assert.equal(disposeCounter, 0);
|
||||
assert.strictEqual(disposeCounter, 0);
|
||||
watcher2Disposable3.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
assert.strictEqual(disposeCounter, 1);
|
||||
|
||||
disposeCounter = 0;
|
||||
const resource3 = URI.parse('test://foo/bar3');
|
||||
@@ -116,10 +118,12 @@ suite('File Service', () => {
|
||||
const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
assert.strictEqual(disposeCounter, 0);
|
||||
watcher3Disposable1.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
assert.strictEqual(disposeCounter, 1);
|
||||
watcher3Disposable2.dispose();
|
||||
assert.equal(disposeCounter, 2);
|
||||
assert.strictEqual(disposeCounter, 2);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
import * as assert from 'assert';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files';
|
||||
import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
|
||||
// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s,
|
||||
// making /-style absolute paths fail isAbsolute checks.
|
||||
const join = posix.join;
|
||||
import { basename, joinPath } from 'vs/base/common/resources';
|
||||
import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
suite('IndexedDB File Service', function () {
|
||||
|
||||
// IDB sometimes under pressure in build machines.
|
||||
this.retries(3);
|
||||
|
||||
const logSchema = 'logs';
|
||||
|
||||
let service: FileService;
|
||||
@@ -27,12 +27,43 @@ suite('IndexedDB File Service', function () {
|
||||
let userdataFileProvider: IIndexedDBFileSystemProvider;
|
||||
const testDir = '/';
|
||||
|
||||
const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path });
|
||||
const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path });
|
||||
const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths);
|
||||
const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
setup(async () => {
|
||||
const initFixtures = async () => {
|
||||
await Promise.all(
|
||||
[['fixtures', 'resolver', 'examples'],
|
||||
['fixtures', 'resolver', 'other', 'deep'],
|
||||
['fixtures', 'service', 'deep'],
|
||||
['batched']]
|
||||
.map(path => userdataURIFromPaths(path))
|
||||
.map(uri => service.createFolder(uri)));
|
||||
await Promise.all(
|
||||
([
|
||||
[['fixtures', 'resolver', 'examples', 'company.js'], 'class company {}'],
|
||||
[['fixtures', 'resolver', 'examples', 'conway.js'], 'export function conway() {}'],
|
||||
[['fixtures', 'resolver', 'examples', 'employee.js'], 'export const employee = "jax"'],
|
||||
[['fixtures', 'resolver', 'examples', 'small.js'], ''],
|
||||
[['fixtures', 'resolver', 'other', 'deep', 'company.js'], 'class company {}'],
|
||||
[['fixtures', 'resolver', 'other', 'deep', 'conway.js'], 'export function conway() {}'],
|
||||
[['fixtures', 'resolver', 'other', 'deep', 'employee.js'], 'export const employee = "jax"'],
|
||||
[['fixtures', 'resolver', 'other', 'deep', 'small.js'], ''],
|
||||
[['fixtures', 'resolver', 'index.html'], '<p>p</p>'],
|
||||
[['fixtures', 'resolver', 'site.css'], '.p {color: red;}'],
|
||||
[['fixtures', 'service', 'deep', 'company.js'], 'class company {}'],
|
||||
[['fixtures', 'service', 'deep', 'conway.js'], 'export function conway() {}'],
|
||||
[['fixtures', 'service', 'deep', 'employee.js'], 'export const employee = "jax"'],
|
||||
[['fixtures', 'service', 'deep', 'small.js'], ''],
|
||||
[['fixtures', 'service', 'binary.txt'], '<p>p</p>'],
|
||||
] as const)
|
||||
.map(([path, contents]) => [userdataURIFromPaths(path), contents] as const)
|
||||
.map(([uri, contents]) => service.createFile(uri, VSBuffer.fromString(contents)))
|
||||
);
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
const logService = new NullLogService();
|
||||
|
||||
service = new FileService(logService);
|
||||
@@ -45,33 +76,302 @@ suite('IndexedDB File Service', function () {
|
||||
userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE));
|
||||
disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider));
|
||||
disposables.add(userdataFileProvider);
|
||||
};
|
||||
|
||||
setup(async () => {
|
||||
await reload();
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
disposables.clear();
|
||||
await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false });
|
||||
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false });
|
||||
});
|
||||
|
||||
await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false });
|
||||
await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false });
|
||||
test('root is always present', async () => {
|
||||
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
|
||||
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false });
|
||||
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
|
||||
});
|
||||
|
||||
test('createFolder', async () => {
|
||||
let event: FileOperationEvent | undefined;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const parent = await service.resolve(makeUserdataURI(testDir));
|
||||
const parent = await service.resolve(userdataURIFromPaths([]));
|
||||
const newFolderResource = joinPath(parent.resource, 'newFolder');
|
||||
|
||||
const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder'));
|
||||
|
||||
assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0);
|
||||
assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 0);
|
||||
const newFolder = await service.createFolder(newFolderResource);
|
||||
assert.equal(newFolder.name, 'newFolder');
|
||||
// Invalid.. dirs dont exist in our IDBFSB.
|
||||
// assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1);
|
||||
assert.strictEqual(newFolder.name, 'newFolder');
|
||||
assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 1);
|
||||
assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory);
|
||||
|
||||
assert.ok(event);
|
||||
assert.equal(event!.resource.path, newFolderResource.path);
|
||||
assert.equal(event!.operation, FileOperation.CREATE);
|
||||
assert.equal(event!.target!.resource.path, newFolderResource.path);
|
||||
assert.equal(event!.target!.isDirectory, true);
|
||||
assert.strictEqual(event!.resource.path, newFolderResource.path);
|
||||
assert.strictEqual(event!.operation, FileOperation.CREATE);
|
||||
assert.strictEqual(event!.target!.resource.path, newFolderResource.path);
|
||||
assert.strictEqual(event!.target!.isDirectory, true);
|
||||
});
|
||||
|
||||
test('createFolder: creating multiple folders at once', async () => {
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const multiFolderPaths = ['a', 'couple', 'of', 'folders'];
|
||||
const parent = await service.resolve(userdataURIFromPaths([]));
|
||||
const newFolderResource = joinPath(parent.resource, ...multiFolderPaths);
|
||||
|
||||
const newFolder = await service.createFolder(newFolderResource);
|
||||
|
||||
const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1];
|
||||
assert.strictEqual(newFolder.name, lastFolderName);
|
||||
assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory);
|
||||
|
||||
assert.ok(event!);
|
||||
assert.strictEqual(event!.resource.path, newFolderResource.path);
|
||||
assert.strictEqual(event!.operation, FileOperation.CREATE);
|
||||
assert.strictEqual(event!.target!.resource.path, newFolderResource.path);
|
||||
assert.strictEqual(event!.target!.isDirectory, true);
|
||||
});
|
||||
|
||||
test('exists', async () => {
|
||||
let exists = await service.exists(userdataURIFromPaths([]));
|
||||
assert.strictEqual(exists, true);
|
||||
|
||||
exists = await service.exists(userdataURIFromPaths(['hello']));
|
||||
assert.strictEqual(exists, false);
|
||||
});
|
||||
|
||||
test('resolve - file', async () => {
|
||||
await initFixtures();
|
||||
|
||||
const resource = userdataURIFromPaths(['fixtures', 'resolver', 'index.html']);
|
||||
const resolved = await service.resolve(resource);
|
||||
|
||||
assert.strictEqual(resolved.name, 'index.html');
|
||||
assert.strictEqual(resolved.isFile, true);
|
||||
assert.strictEqual(resolved.isDirectory, false);
|
||||
assert.strictEqual(resolved.isSymbolicLink, false);
|
||||
assert.strictEqual(resolved.resource.toString(), resource.toString());
|
||||
assert.strictEqual(resolved.children, undefined);
|
||||
assert.ok(resolved.size! > 0);
|
||||
});
|
||||
|
||||
test('resolve - directory', async () => {
|
||||
await initFixtures();
|
||||
|
||||
const testsElements = ['examples', 'other', 'index.html', 'site.css'];
|
||||
|
||||
const resource = userdataURIFromPaths(['fixtures', 'resolver']);
|
||||
const result = await service.resolve(resource);
|
||||
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.resource.toString(), resource.toString());
|
||||
assert.strictEqual(result.name, 'resolver');
|
||||
assert.ok(result.children);
|
||||
assert.ok(result.children!.length > 0);
|
||||
assert.ok(result!.isDirectory);
|
||||
assert.strictEqual(result.children!.length, testsElements.length);
|
||||
|
||||
assert.ok(result.children!.every(entry => {
|
||||
return testsElements.some(name => {
|
||||
return basename(entry.resource) === name;
|
||||
});
|
||||
}));
|
||||
|
||||
result.children!.forEach(value => {
|
||||
assert.ok(basename(value.resource));
|
||||
if (['examples', 'other'].indexOf(basename(value.resource)) >= 0) {
|
||||
assert.ok(value.isDirectory);
|
||||
assert.strictEqual(value.mtime, undefined);
|
||||
assert.strictEqual(value.ctime, undefined);
|
||||
} else if (basename(value.resource) === 'index.html') {
|
||||
assert.ok(!value.isDirectory);
|
||||
assert.ok(!value.children);
|
||||
assert.strictEqual(value.mtime, undefined);
|
||||
assert.strictEqual(value.ctime, undefined);
|
||||
} else if (basename(value.resource) === 'site.css') {
|
||||
assert.ok(!value.isDirectory);
|
||||
assert.ok(!value.children);
|
||||
assert.strictEqual(value.mtime, undefined);
|
||||
assert.strictEqual(value.ctime, undefined);
|
||||
} else {
|
||||
assert.ok(!'Unexpected value ' + basename(value.resource));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('createFile', async () => {
|
||||
return assertCreateFile(contents => VSBuffer.fromString(contents));
|
||||
});
|
||||
|
||||
test('createFile (readable)', async () => {
|
||||
return assertCreateFile(contents => bufferToReadable(VSBuffer.fromString(contents)));
|
||||
});
|
||||
|
||||
test('createFile (stream)', async () => {
|
||||
return assertCreateFile(contents => bufferToStream(VSBuffer.fromString(contents)));
|
||||
});
|
||||
|
||||
async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<void> {
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const contents = 'Hello World';
|
||||
const resource = userdataURIFromPaths(['test.txt']);
|
||||
|
||||
assert.strictEqual(await service.canCreateFile(resource), true);
|
||||
const fileStat = await service.createFile(resource, converter(contents));
|
||||
assert.strictEqual(fileStat.name, 'test.txt');
|
||||
assert.strictEqual((await userdataFileProvider.stat(fileStat.resource)).type, FileType.File);
|
||||
assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(fileStat.resource)), contents);
|
||||
|
||||
assert.ok(event!);
|
||||
assert.strictEqual(event!.resource.path, resource.path);
|
||||
assert.strictEqual(event!.operation, FileOperation.CREATE);
|
||||
assert.strictEqual(event!.target!.resource.path, resource.path);
|
||||
}
|
||||
|
||||
const makeBatchTester = (size: number, name: string) => {
|
||||
const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) }));
|
||||
let stats: Promise<IFileStatWithMetadata[]> | undefined = undefined;
|
||||
return {
|
||||
async create() {
|
||||
return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents))));
|
||||
},
|
||||
async assertContentsCorrect() {
|
||||
await Promise.all(batch.map(async (entry, i) => {
|
||||
if (!stats) { throw Error('read called before create'); }
|
||||
const stat = (await stats!)[i];
|
||||
assert.strictEqual(stat.name, `Hello${i}.txt`);
|
||||
assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File);
|
||||
assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents);
|
||||
}));
|
||||
},
|
||||
async delete() {
|
||||
await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false });
|
||||
},
|
||||
async assertContentsEmpty() {
|
||||
if (!stats) { throw Error('assertContentsEmpty called before create'); }
|
||||
await Promise.all((await stats).map(async stat => {
|
||||
const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code);
|
||||
assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound);
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
test('createFile (small batch)', async () => {
|
||||
const tester = makeBatchTester(50, 'smallBatch');
|
||||
await tester.create();
|
||||
await tester.assertContentsCorrect();
|
||||
await tester.delete();
|
||||
await tester.assertContentsEmpty();
|
||||
});
|
||||
|
||||
test('createFile (mixed parallel/sequential)', async () => {
|
||||
const single1 = makeBatchTester(1, 'single1');
|
||||
const single2 = makeBatchTester(1, 'single2');
|
||||
|
||||
const batch1 = makeBatchTester(20, 'batch1');
|
||||
const batch2 = makeBatchTester(20, 'batch2');
|
||||
|
||||
single1.create();
|
||||
batch1.create();
|
||||
await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]);
|
||||
single2.create();
|
||||
batch2.create();
|
||||
await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]);
|
||||
await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]);
|
||||
|
||||
await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()]));
|
||||
await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()]));
|
||||
});
|
||||
|
||||
test('deleteFile', async () => {
|
||||
await initFixtures();
|
||||
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const anotherResource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']);
|
||||
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']);
|
||||
const source = await service.resolve(resource);
|
||||
|
||||
assert.strictEqual(await service.canDelete(source.resource, { useTrash: false }), true);
|
||||
await service.del(source.resource, { useTrash: false });
|
||||
|
||||
assert.strictEqual(await service.exists(source.resource), false);
|
||||
assert.strictEqual(await service.exists(anotherResource), true);
|
||||
|
||||
assert.ok(event!);
|
||||
assert.strictEqual(event!.resource.path, resource.path);
|
||||
assert.strictEqual(event!.operation, FileOperation.DELETE);
|
||||
|
||||
{
|
||||
let error: Error | undefined = undefined;
|
||||
try {
|
||||
await service.del(source.resource, { useTrash: false });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
assert.strictEqual((<FileOperationError>error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
|
||||
}
|
||||
await reload();
|
||||
{
|
||||
let error: Error | undefined = undefined;
|
||||
try {
|
||||
await service.del(source.resource, { useTrash: false });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
assert.strictEqual((<FileOperationError>error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
test('deleteFolder (recursive)', async () => {
|
||||
await initFixtures();
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']);
|
||||
const subResource1 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']);
|
||||
const subResource2 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']);
|
||||
assert.strictEqual(await service.exists(subResource1), true);
|
||||
assert.strictEqual(await service.exists(subResource2), true);
|
||||
|
||||
const source = await service.resolve(resource);
|
||||
|
||||
assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash: false }), true);
|
||||
await service.del(source.resource, { recursive: true, useTrash: false });
|
||||
|
||||
assert.strictEqual(await service.exists(source.resource), false);
|
||||
assert.strictEqual(await service.exists(subResource1), false);
|
||||
assert.strictEqual(await service.exists(subResource2), false);
|
||||
assert.ok(event!);
|
||||
assert.strictEqual(event!.resource.fsPath, resource.fsPath);
|
||||
assert.strictEqual(event!.operation, FileOperation.DELETE);
|
||||
});
|
||||
|
||||
|
||||
test('deleteFolder (non recursive)', async () => {
|
||||
await initFixtures();
|
||||
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']);
|
||||
const source = await service.resolve(resource);
|
||||
|
||||
assert.ok((await service.canDelete(source.resource)) instanceof Error);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await service.del(source.resource);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
assert.ok(error);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 3);
|
||||
assert.strictEqual(e.changes.length, 3);
|
||||
assert.ok(e.contains(added, FileChangeType.ADDED));
|
||||
assert.ok(e.contains(updated, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(deleted, FileChangeType.DELETED));
|
||||
@@ -103,7 +103,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 5);
|
||||
assert.strictEqual(e.changes.length, 5);
|
||||
|
||||
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
|
||||
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
|
||||
@@ -133,7 +133,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 1);
|
||||
assert.strictEqual(e.changes.length, 1);
|
||||
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
|
||||
@@ -158,7 +158,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
assert.strictEqual(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
@@ -184,7 +184,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
assert.strictEqual(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(created, FileChangeType.ADDED));
|
||||
assert.ok(!e.contains(created, FileChangeType.UPDATED));
|
||||
@@ -213,7 +213,7 @@ suite('Normalizer', () => {
|
||||
|
||||
watch.onDidFilesChange(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
assert.strictEqual(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(deleted, FileChangeType.DELETED));
|
||||
assert.ok(!e.contains(updated, FileChangeType.UPDATED));
|
||||
|
||||
Reference in New Issue
Block a user