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:
Karl Burtram
2021-04-27 14:01:59 -07:00
committed by GitHub
parent 7e1c0076ba
commit 867a963882
1817 changed files with 81812 additions and 50843 deletions

View File

@@ -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();
});
}

View File

@@ -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(),
]);

View File

@@ -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 => {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -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));
}));
}

View File

@@ -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']);
}
});
});

View File

@@ -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
}
}
));

View File

@@ -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 });
}

View File

@@ -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);
}
}

View File

@@ -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
}
}
));

View File

@@ -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();
});
});

View File

@@ -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);
});
});

View File

@@ -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));