mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-07 17:23:56 -05:00
Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 (#15681)
* Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 * Fixes and cleanup * Distro * Fix hygiene yarn * delete no yarn lock changes file * Fix hygiene * Fix layer check * Fix CI * Skip lib checks * Remove tests deleted in vs code * Fix tests * Distro * Fix tests and add removed extension point * Skip failing notebook tests for now * Disable broken tests and cleanup build folder * Update yarn.lock and fix smoke tests * Bump sqlite * fix contributed actions and file spacing * Fix user data path * Update yarn.locks Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
210
src/vs/platform/files/browser/htmlFileSystemProvider.ts
Normal file
210
src/vs/platform/files/browser/htmlFileSystemProvider.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { extUri } from 'vs/base/common/resources';
|
||||
|
||||
function split(path: string): [string, string] | undefined {
|
||||
const match = /^(.*)\/([^/]+)$/.exec(path);
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [, parentPath, name] = match;
|
||||
return [parentPath, name];
|
||||
}
|
||||
|
||||
function getRootUUID(uri: URI): string | undefined {
|
||||
const match = /^\/([^/]+)\/[^/]+\/?$/.exec(uri.path);
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
|
||||
|
||||
private readonly files = new Map<string, FileSystemFileHandle>();
|
||||
private readonly directories = new Map<string, FileSystemDirectoryHandle>();
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
|
||||
readonly onDidChangeCapabilities = Event.None;
|
||||
|
||||
private readonly _onDidChangeFile = new Emitter<readonly IFileChange[]>();
|
||||
readonly onDidChangeFile = this._onDidChangeFile.event;
|
||||
|
||||
private readonly _onDidErrorOccur = new Emitter<string>();
|
||||
readonly onDidErrorOccur = this._onDidErrorOccur.event;
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const handle = await this.getFileHandle(resource);
|
||||
|
||||
if (!handle) {
|
||||
throw new Error('File not found.');
|
||||
}
|
||||
|
||||
const file = await handle.getFile();
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const handle = await this.getFileHandle(resource);
|
||||
|
||||
if (!handle) {
|
||||
throw new Error('File not found.');
|
||||
}
|
||||
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(content);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
const rootUUID = getRootUUID(resource);
|
||||
|
||||
if (rootUUID) {
|
||||
const fileHandle = this.files.get(rootUUID);
|
||||
|
||||
if (fileHandle) {
|
||||
const file = await fileHandle.getFile();
|
||||
|
||||
return {
|
||||
type: FileType.File,
|
||||
mtime: file.lastModified,
|
||||
ctime: 0,
|
||||
size: file.size
|
||||
};
|
||||
}
|
||||
|
||||
const directoryHandle = this.directories.get(rootUUID);
|
||||
|
||||
if (directoryHandle) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
mtime: 0,
|
||||
ctime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await this.getParentDirectoryHandle(resource);
|
||||
|
||||
if (!parent) {
|
||||
throw new Error('Stat error: no parent found');
|
||||
}
|
||||
|
||||
const name = extUri.basename(resource);
|
||||
for await (const [childName, child] of parent) {
|
||||
if (childName === name) {
|
||||
if (child.kind === 'file') {
|
||||
const file = await child.getFile();
|
||||
|
||||
return {
|
||||
type: FileType.File,
|
||||
mtime: file.lastModified,
|
||||
ctime: 0,
|
||||
size: file.size
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
mtime: 0,
|
||||
ctime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Stat error: entry not found');
|
||||
}
|
||||
|
||||
mkdir(resource: URI): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const parent = await this.getDirectoryHandle(resource);
|
||||
|
||||
if (!parent) {
|
||||
throw new Error('Stat error: no parent found');
|
||||
}
|
||||
|
||||
const result: [string, FileType][] = [];
|
||||
|
||||
for await (const [name, child] of parent) {
|
||||
result.push([name, child.kind === 'file' ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
throw new Error('Method not implemented: delete');
|
||||
}
|
||||
|
||||
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
throw new Error('Method not implemented: rename');
|
||||
}
|
||||
|
||||
private async getDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
|
||||
const rootUUID = getRootUUID(uri);
|
||||
|
||||
if (rootUUID) {
|
||||
return this.directories.get(rootUUID);
|
||||
}
|
||||
|
||||
const splitResult = split(uri.path);
|
||||
|
||||
if (!splitResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parent = await this.getDirectoryHandle(URI.from({ ...uri, path: splitResult[0] }));
|
||||
return await parent?.getDirectoryHandle(extUri.basename(uri));
|
||||
}
|
||||
|
||||
private async getParentDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
|
||||
return this.getDirectoryHandle(URI.from({ ...uri, path: extUri.dirname(uri).path }));
|
||||
}
|
||||
|
||||
private async getFileHandle(uri: URI): Promise<FileSystemFileHandle | undefined> {
|
||||
const rootUUID = getRootUUID(uri);
|
||||
|
||||
if (rootUUID) {
|
||||
return this.files.get(rootUUID);
|
||||
}
|
||||
|
||||
const parent = await this.getParentDirectoryHandle(uri);
|
||||
const name = extUri.basename(uri);
|
||||
return await parent?.getFileHandle(name);
|
||||
}
|
||||
|
||||
registerFileHandle(uuid: string, handle: FileSystemFileHandle): void {
|
||||
this.files.set(uuid, handle);
|
||||
}
|
||||
|
||||
registerDirectoryHandle(uuid: string, handle: FileSystemDirectoryHandle): void {
|
||||
this.directories.set(uuid, handle);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidChangeFile.dispose();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
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';
|
||||
@@ -48,9 +47,6 @@ export class IndexedDB {
|
||||
}
|
||||
|
||||
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
|
||||
if (browser.isEdgeLegacy) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return new Promise((c, e) => {
|
||||
const request = window.indexedDB.open(name, version);
|
||||
request.onerror = (err) => e(request.error);
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
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 { 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, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
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 { ILogService } from 'vs/platform/log/common/log';
|
||||
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 { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, listenStream, consumeStream } from 'vs/base/common/stream';
|
||||
import { Promises, ResourceQueue } 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';
|
||||
@@ -71,6 +71,10 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
getProvider(scheme: string): IFileSystemProvider | undefined {
|
||||
return this.provider.get(scheme);
|
||||
}
|
||||
|
||||
async activateProvider(scheme: string): Promise<void> {
|
||||
|
||||
// Emit an event that we are about to activate a provider with the given scheme.
|
||||
@@ -79,9 +83,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
this._onWillActivateFileSystemProvider.fire({
|
||||
scheme,
|
||||
join(promise) {
|
||||
if (promise) {
|
||||
joiners.push(promise);
|
||||
}
|
||||
joiners.push(promise);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -364,12 +366,12 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
|
||||
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) {
|
||||
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream);
|
||||
await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream);
|
||||
}
|
||||
|
||||
// write file: buffered
|
||||
else {
|
||||
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
|
||||
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
|
||||
@@ -379,6 +381,14 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise<IStat | undefined> {
|
||||
|
||||
// Validate unlock support
|
||||
const unlock = !!options?.unlock;
|
||||
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
|
||||
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
|
||||
}
|
||||
|
||||
// Validate via file stat meta data
|
||||
let stat: IStat | undefined = undefined;
|
||||
try {
|
||||
stat = await provider.stat(resource);
|
||||
@@ -386,7 +396,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return undefined; // file might not exist
|
||||
}
|
||||
|
||||
// file cannot be directory
|
||||
// File cannot be directory
|
||||
if ((stat.type & FileType.Directory) !== 0) {
|
||||
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
|
||||
}
|
||||
@@ -417,7 +427,28 @@ export class FileService extends Disposable implements IFileService {
|
||||
async readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
|
||||
const provider = await this.withReadProvider(resource);
|
||||
|
||||
const stream = await this.doReadAsFileStream(provider, resource, {
|
||||
if (options?.atomic) {
|
||||
return this.doReadFileAtomic(provider, resource, options);
|
||||
}
|
||||
|
||||
return this.doReadFile(provider, resource, options);
|
||||
}
|
||||
|
||||
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
|
||||
return new Promise<IFileContent>((resolve, reject) => {
|
||||
this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
|
||||
try {
|
||||
const content = await this.doReadFile(provider, resource, options);
|
||||
resolve(content);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
|
||||
const stream = await this.doReadFileStream(provider, resource, {
|
||||
...options,
|
||||
// optimization: since we know that the caller does not
|
||||
// care about buffering, we indicate this to the reader.
|
||||
@@ -433,13 +464,13 @@ export class FileService extends Disposable implements IFileService {
|
||||
};
|
||||
}
|
||||
|
||||
async readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent> {
|
||||
async readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent> {
|
||||
const provider = await this.withReadProvider(resource);
|
||||
|
||||
return this.doReadAsFileStream(provider, resource, options);
|
||||
return this.doReadFileStream(provider, resource, options);
|
||||
}
|
||||
|
||||
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
|
||||
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileStreamOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
|
||||
|
||||
// install a cancellation token that gets cancelled
|
||||
// when any error occurs. this allows us to resolve
|
||||
@@ -454,20 +485,17 @@ export class FileService extends Disposable implements IFileService {
|
||||
throw error;
|
||||
});
|
||||
|
||||
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
|
||||
|
||||
let fileStream: VSBufferReadableStream | undefined = undefined;
|
||||
try {
|
||||
|
||||
// if the etag is provided, we await the result of the validation
|
||||
// due to the likelyhood of hitting a NOT_MODIFIED_SINCE result.
|
||||
// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
|
||||
// otherwise, we let it run in parallel to the file reading for
|
||||
// optimal startup performance.
|
||||
if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED) {
|
||||
await statPromise;
|
||||
}
|
||||
|
||||
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)) {
|
||||
fileStream = this.readFileUnbuffered(provider, resource, options);
|
||||
@@ -483,9 +511,6 @@ export class FileService extends Disposable implements IFileService {
|
||||
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
|
||||
}
|
||||
|
||||
// observe the stream for the error case below
|
||||
fileStreamObserver = observe(fileStream);
|
||||
|
||||
const fileStat = await statPromise;
|
||||
|
||||
return {
|
||||
@@ -497,15 +522,15 @@ export class FileService extends Disposable implements IFileService {
|
||||
// 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();
|
||||
if (fileStream) {
|
||||
await consumeStream(fileStream);
|
||||
}
|
||||
|
||||
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
|
||||
}
|
||||
}
|
||||
|
||||
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
|
||||
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
|
||||
const fileStream = provider.readFileStream(resource, options, token);
|
||||
|
||||
return transform(fileStream, {
|
||||
@@ -514,7 +539,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}, data => VSBuffer.concat(data));
|
||||
}
|
||||
|
||||
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
|
||||
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
|
||||
const stream = newWriteableBufferStream();
|
||||
|
||||
readFileIntoStream(provider, resource, stream, data => data, {
|
||||
@@ -526,7 +551,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return stream;
|
||||
}
|
||||
|
||||
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
|
||||
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileStreamOptions): VSBufferReadableStream {
|
||||
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
|
||||
|
||||
// Read the file into the stream async but do not wait for
|
||||
@@ -552,13 +577,14 @@ export class FileService extends Disposable implements IFileService {
|
||||
stream.end(VSBuffer.wrap(buffer));
|
||||
} catch (err) {
|
||||
stream.error(err);
|
||||
stream.end();
|
||||
}
|
||||
})();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
|
||||
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
|
||||
const stat = await this.resolve(resource, { resolveMetadata: true });
|
||||
|
||||
// Throw if resource is a directory
|
||||
@@ -577,7 +603,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return stat;
|
||||
}
|
||||
|
||||
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileOptions): void {
|
||||
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
|
||||
if (options?.limits) {
|
||||
let tooLargeErrorResult: FileOperationResult | undefined = undefined;
|
||||
|
||||
@@ -866,7 +892,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
}
|
||||
|
||||
async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<Error | true> {
|
||||
async canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true> {
|
||||
try {
|
||||
await this.doValidateDelete(resource, options);
|
||||
} catch (error) {
|
||||
@@ -876,7 +902,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<IFileSystemProvider> {
|
||||
private async doValidateDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<IFileSystemProvider> {
|
||||
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
|
||||
|
||||
// Validate trash support
|
||||
@@ -903,7 +929,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return provider;
|
||||
}
|
||||
|
||||
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
|
||||
async del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void> {
|
||||
const provider = await this.doValidateDelete(resource, options);
|
||||
|
||||
const useTrash = !!options?.useTrash;
|
||||
@@ -978,7 +1004,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
].join();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.activeWatchers.forEach(watcher => dispose(watcher.disposable));
|
||||
@@ -989,35 +1015,13 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
//#region Helpers
|
||||
|
||||
private readonly writeQueues: Map<string, Queue<void>> = new Map();
|
||||
private readonly writeQueue = this._register(new ResourceQueue());
|
||||
|
||||
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
|
||||
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
|
||||
// (even with error) before another write is done.
|
||||
let writeQueue = this.writeQueues.get(queueKey);
|
||||
if (!writeQueue) {
|
||||
writeQueue = new Queue<void>();
|
||||
this.writeQueues.set(queueKey, writeQueue);
|
||||
|
||||
const onFinish = Event.once(writeQueue.onFinished);
|
||||
onFinish(() => {
|
||||
this.writeQueues.delete(queueKey);
|
||||
dispose(writeQueue);
|
||||
});
|
||||
}
|
||||
|
||||
return writeQueue;
|
||||
}
|
||||
|
||||
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
return this.ensureWriteQueue(provider, resource).queue(async () => {
|
||||
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
|
||||
|
||||
// open handle
|
||||
const handle = await provider.open(resource, { create: true });
|
||||
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
|
||||
|
||||
// write into handle until all bytes from buffer have been written
|
||||
try {
|
||||
@@ -1065,28 +1069,29 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
stream.on('data', async chunk => {
|
||||
listenStream(stream, {
|
||||
onData: async chunk => {
|
||||
|
||||
// pause stream to perform async write operation
|
||||
stream.pause();
|
||||
// pause stream to perform async write operation
|
||||
stream.pause();
|
||||
|
||||
try {
|
||||
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
try {
|
||||
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
posInFile += chunk.byteLength;
|
||||
posInFile += chunk.byteLength;
|
||||
|
||||
// resume stream now that we have successfully written
|
||||
// run this on the next tick to prevent increasing the
|
||||
// execution stack because resume() may call the event
|
||||
// handler again before finishing.
|
||||
setTimeout(() => stream.resume());
|
||||
// resume stream now that we have successfully written
|
||||
// run this on the next tick to prevent increasing the
|
||||
// execution stack because resume() may call the event
|
||||
// handler again before finishing.
|
||||
setTimeout(() => stream.resume());
|
||||
},
|
||||
onError: error => reject(error),
|
||||
onEnd: () => resolve()
|
||||
});
|
||||
|
||||
stream.on('error', error => reject(error));
|
||||
stream.on('end', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1111,11 +1116,11 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
}
|
||||
|
||||
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStreamOrBufferedStream));
|
||||
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream));
|
||||
}
|
||||
|
||||
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
|
||||
let buffer: VSBuffer;
|
||||
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
|
||||
buffer = bufferOrReadableOrStreamOrBufferedStream;
|
||||
@@ -1128,11 +1133,11 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
// Write through the provider
|
||||
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
|
||||
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
|
||||
}
|
||||
|
||||
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
|
||||
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
}
|
||||
|
||||
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
|
||||
@@ -1143,7 +1148,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// Open handles
|
||||
sourceHandle = await sourceProvider.open(source, { create: false });
|
||||
targetHandle = await targetProvider.open(target, { create: true });
|
||||
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
|
||||
|
||||
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
|
||||
|
||||
@@ -1178,21 +1183,21 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
|
||||
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
}
|
||||
|
||||
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
|
||||
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true });
|
||||
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
|
||||
}
|
||||
|
||||
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
|
||||
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
|
||||
}
|
||||
|
||||
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
|
||||
|
||||
// Open handle
|
||||
const targetHandle = await targetProvider.open(target, { create: true });
|
||||
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
|
||||
|
||||
// Read entire buffer from source and write buffered
|
||||
try {
|
||||
@@ -1211,7 +1216,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
|
||||
|
||||
// Write buffer into target at once
|
||||
await this.doWriteUnbuffered(targetProvider, target, buffer);
|
||||
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
|
||||
}
|
||||
|
||||
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { localize } from 'vs/nls';
|
||||
import { sep } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { IExpression } from 'vs/base/common/glob';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { startsWithIgnoreCase } from 'vs/base/common/strings';
|
||||
@@ -17,6 +17,8 @@ import { ReadableStreamEvents } from 'vs/base/common/stream';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
|
||||
//#region file service & providers
|
||||
|
||||
export const IFileService = createDecorator<IFileService>('fileService');
|
||||
|
||||
export interface IFileService {
|
||||
@@ -44,6 +46,11 @@ export interface IFileService {
|
||||
*/
|
||||
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable;
|
||||
|
||||
/**
|
||||
* Returns a file system provider for a certain scheme.
|
||||
*/
|
||||
getProvider(scheme: string): IFileSystemProvider | undefined;
|
||||
|
||||
/**
|
||||
* Tries to activate a provider with the given scheme.
|
||||
*/
|
||||
@@ -112,7 +119,7 @@ export interface IFileService {
|
||||
/**
|
||||
* Read the contents of the provided resource buffered as stream.
|
||||
*/
|
||||
readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent>;
|
||||
readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent>;
|
||||
|
||||
/**
|
||||
* Updates the content replacing its previous value.
|
||||
@@ -170,13 +177,13 @@ export interface IFileService {
|
||||
* move the file to trash. The optional recursive parameter allows to delete
|
||||
* non-empty folders recursively.
|
||||
*/
|
||||
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
|
||||
del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Find out if a delete operation is possible given the arguments. No changes on disk will
|
||||
* be performed. Returns an Error if the operation cannot be done.
|
||||
*/
|
||||
canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<Error | true>;
|
||||
canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true>;
|
||||
|
||||
/**
|
||||
* Allows to start a watcher that reports file/folder change events on the provided resource.
|
||||
@@ -192,7 +199,22 @@ export interface IFileService {
|
||||
}
|
||||
|
||||
export interface FileOverwriteOptions {
|
||||
overwrite: boolean;
|
||||
|
||||
/**
|
||||
* Set to `true` to overwrite a file if it exists. Will
|
||||
* throw an error otherwise if the file does exist.
|
||||
*/
|
||||
readonly overwrite: boolean;
|
||||
}
|
||||
|
||||
export interface FileUnlockOptions {
|
||||
|
||||
/**
|
||||
* Set to `true` to try to remove any write locks the file might
|
||||
* have. A file that is write locked will throw an error for any
|
||||
* attempt to write to unless `unlock: true` is provided.
|
||||
*/
|
||||
readonly unlock: boolean;
|
||||
}
|
||||
|
||||
export interface FileReadStreamOptions {
|
||||
@@ -218,59 +240,159 @@ export interface FileReadStreamOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileWriteOptions {
|
||||
overwrite: boolean;
|
||||
create: boolean;
|
||||
export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOptions {
|
||||
|
||||
/**
|
||||
* Set to `true` to create a file when it does not exist. Will
|
||||
* throw an error otherwise if the file does not exist.
|
||||
*/
|
||||
readonly create: boolean;
|
||||
}
|
||||
|
||||
export interface FileOpenOptions {
|
||||
create: boolean;
|
||||
export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions;
|
||||
|
||||
export function isFileOpenForWriteOptions(options: FileOpenOptions): options is FileOpenForWriteOptions {
|
||||
return options.create === true;
|
||||
}
|
||||
|
||||
export interface FileOpenForReadOptions {
|
||||
|
||||
/**
|
||||
* A hint that the file should be opened for reading only.
|
||||
*/
|
||||
readonly create: false;
|
||||
}
|
||||
|
||||
export interface FileOpenForWriteOptions extends FileUnlockOptions {
|
||||
|
||||
/**
|
||||
* A hint that the file should be opened for reading and writing.
|
||||
*/
|
||||
readonly create: true;
|
||||
}
|
||||
|
||||
export interface FileDeleteOptions {
|
||||
recursive: boolean;
|
||||
useTrash: boolean;
|
||||
|
||||
/**
|
||||
* Set to `true` to recursively delete any children of the file. This
|
||||
* only applies to folders and can lead to an error unless provided
|
||||
* if the folder is not empty.
|
||||
*/
|
||||
readonly recursive: boolean;
|
||||
|
||||
/**
|
||||
* Set to `true` to attempt to move the file to trash
|
||||
* instead of deleting it permanently from disk. This
|
||||
* option maybe not be supported on all providers.
|
||||
*/
|
||||
readonly useTrash: boolean;
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
|
||||
/**
|
||||
* File is unknown (neither file, directory nor symbolic link).
|
||||
*/
|
||||
Unknown = 0,
|
||||
|
||||
/**
|
||||
* File is a normal file.
|
||||
*/
|
||||
File = 1,
|
||||
|
||||
/**
|
||||
* File is a directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
|
||||
/**
|
||||
* File is a symbolic link.
|
||||
*
|
||||
* Note: even when the file is a symbolic link, you can test for
|
||||
* `FileType.File` and `FileType.Directory` to know the type of
|
||||
* the target the link points to.
|
||||
*/
|
||||
SymbolicLink = 64
|
||||
}
|
||||
|
||||
export interface IStat {
|
||||
type: FileType;
|
||||
|
||||
/**
|
||||
* The file type.
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
*/
|
||||
mtime: number;
|
||||
readonly mtime: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
*/
|
||||
ctime: number;
|
||||
readonly ctime: number;
|
||||
|
||||
/**
|
||||
* The size of the file in bytes.
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface IWatchOptions {
|
||||
recursive: boolean;
|
||||
|
||||
/**
|
||||
* Set to `true` to watch for changes recursively in a folder
|
||||
* and all of its children.
|
||||
*/
|
||||
readonly recursive: boolean;
|
||||
|
||||
/**
|
||||
* A set of paths to exclude from watching.
|
||||
*/
|
||||
excludes: string[];
|
||||
}
|
||||
|
||||
export const enum FileSystemProviderCapabilities {
|
||||
|
||||
/**
|
||||
* Provider supports unbuffered read/write.
|
||||
*/
|
||||
FileReadWrite = 1 << 1,
|
||||
|
||||
/**
|
||||
* Provider supports open/read/write/close low level file operations.
|
||||
*/
|
||||
FileOpenReadWriteClose = 1 << 2,
|
||||
|
||||
/**
|
||||
* Provider supports stream based reading.
|
||||
*/
|
||||
FileReadStream = 1 << 4,
|
||||
|
||||
/**
|
||||
* Provider supports copy operation.
|
||||
*/
|
||||
FileFolderCopy = 1 << 3,
|
||||
|
||||
/**
|
||||
* Provider is path case sensitive.
|
||||
*/
|
||||
PathCaseSensitive = 1 << 10,
|
||||
|
||||
/**
|
||||
* All files of the provider are readonly.
|
||||
*/
|
||||
Readonly = 1 << 11,
|
||||
|
||||
Trash = 1 << 12
|
||||
/**
|
||||
* Provider supports to delete via trash.
|
||||
*/
|
||||
Trash = 1 << 12,
|
||||
|
||||
/**
|
||||
* Provider support to unlock files for writing.
|
||||
*/
|
||||
FileWriteUnlock = 1 << 13
|
||||
}
|
||||
|
||||
export interface IFileSystemProvider {
|
||||
@@ -345,6 +467,7 @@ export enum FileSystemProviderErrorCode {
|
||||
FileIsADirectory = 'EntryIsADirectory',
|
||||
FileExceedsMemoryLimit = 'EntryExceedsMemoryLimit',
|
||||
FileTooLarge = 'EntryTooLarge',
|
||||
FileWriteLocked = 'EntryWriteLocked',
|
||||
NoPermissions = 'NoPermissions',
|
||||
Unavailable = 'Unavailable',
|
||||
Unknown = 'Unknown'
|
||||
@@ -404,6 +527,7 @@ export function toFileSystemProviderErrorCode(error: Error | undefined | null):
|
||||
case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
|
||||
case FileSystemProviderErrorCode.FileExceedsMemoryLimit: return FileSystemProviderErrorCode.FileExceedsMemoryLimit;
|
||||
case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
|
||||
case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
|
||||
case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
|
||||
case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
|
||||
}
|
||||
@@ -426,6 +550,8 @@ export function toFileOperationResult(error: Error): FileOperationResult {
|
||||
return FileOperationResult.FILE_IS_DIRECTORY;
|
||||
case FileSystemProviderErrorCode.FileNotADirectory:
|
||||
return FileOperationResult.FILE_NOT_DIRECTORY;
|
||||
case FileSystemProviderErrorCode.FileWriteLocked:
|
||||
return FileOperationResult.FILE_WRITE_LOCKED;
|
||||
case FileSystemProviderErrorCode.NoPermissions:
|
||||
return FileOperationResult.FILE_PERMISSION_DENIED;
|
||||
case FileSystemProviderErrorCode.FileExists:
|
||||
@@ -440,18 +566,18 @@ export function toFileOperationResult(error: Error): FileOperationResult {
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderRegistrationEvent {
|
||||
added: boolean;
|
||||
scheme: string;
|
||||
provider?: IFileSystemProvider;
|
||||
readonly added: boolean;
|
||||
readonly scheme: string;
|
||||
readonly provider?: IFileSystemProvider;
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderCapabilitiesChangeEvent {
|
||||
provider: IFileSystemProvider;
|
||||
scheme: string;
|
||||
readonly provider: IFileSystemProvider;
|
||||
readonly scheme: string;
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderActivationEvent {
|
||||
scheme: string;
|
||||
readonly scheme: string;
|
||||
join(promise: Promise<void>): void;
|
||||
}
|
||||
|
||||
@@ -479,9 +605,9 @@ export class FileOperationEvent {
|
||||
* Possible changes that can occur to a file.
|
||||
*/
|
||||
export const enum FileChangeType {
|
||||
UPDATED = 0,
|
||||
ADDED = 1,
|
||||
DELETED = 2
|
||||
UPDATED,
|
||||
ADDED,
|
||||
DELETED
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -702,13 +828,13 @@ interface IBaseStat {
|
||||
/**
|
||||
* The unified resource identifier of this file or folder.
|
||||
*/
|
||||
resource: URI;
|
||||
readonly resource: URI;
|
||||
|
||||
/**
|
||||
* The name which is the last segment
|
||||
* of the {{path}}.
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The size of the file.
|
||||
@@ -716,7 +842,7 @@ interface IBaseStat {
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
size?: number;
|
||||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
@@ -724,7 +850,7 @@ interface IBaseStat {
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
mtime?: number;
|
||||
readonly mtime?: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
@@ -732,7 +858,7 @@ interface IBaseStat {
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
ctime?: number;
|
||||
readonly ctime?: number;
|
||||
|
||||
/**
|
||||
* A unique identifier thet represents the
|
||||
@@ -741,15 +867,10 @@ interface IBaseStat {
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
etag?: string;
|
||||
readonly etag?: string;
|
||||
}
|
||||
|
||||
export interface IBaseStatWithMetadata extends IBaseStat {
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
etag: string;
|
||||
size: number;
|
||||
}
|
||||
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
|
||||
|
||||
/**
|
||||
* A file resource with meta information.
|
||||
@@ -759,17 +880,20 @@ export interface IFileStat extends IBaseStat {
|
||||
/**
|
||||
* The resource is a file.
|
||||
*/
|
||||
isFile: boolean;
|
||||
readonly isFile: boolean;
|
||||
|
||||
/**
|
||||
* The resource is a directory.
|
||||
*/
|
||||
isDirectory: boolean;
|
||||
readonly isDirectory: boolean;
|
||||
|
||||
/**
|
||||
* The resource is a symbolic link.
|
||||
* The resource is a symbolic link. Note: even when the
|
||||
* file is a symbolic link, you can test for `FileType.File`
|
||||
* and `FileType.Directory` to know the type of the target
|
||||
* the link points to.
|
||||
*/
|
||||
isSymbolicLink: boolean;
|
||||
readonly isSymbolicLink: boolean;
|
||||
|
||||
/**
|
||||
* The children of the file stat or undefined if none.
|
||||
@@ -778,20 +902,20 @@ export interface IFileStat extends IBaseStat {
|
||||
}
|
||||
|
||||
export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata {
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
etag: string;
|
||||
size: number;
|
||||
children?: IFileStatWithMetadata[];
|
||||
readonly mtime: number;
|
||||
readonly ctime: number;
|
||||
readonly etag: string;
|
||||
readonly size: number;
|
||||
readonly children?: IFileStatWithMetadata[];
|
||||
}
|
||||
|
||||
export interface IResolveFileResult {
|
||||
stat?: IFileStat;
|
||||
success: boolean;
|
||||
readonly stat?: IFileStat;
|
||||
readonly success: boolean;
|
||||
}
|
||||
|
||||
export interface IResolveFileResultWithMetadata extends IResolveFileResult {
|
||||
stat?: IFileStatWithMetadata;
|
||||
readonly stat?: IFileStatWithMetadata;
|
||||
}
|
||||
|
||||
export interface IFileContent extends IBaseStatWithMetadata {
|
||||
@@ -799,7 +923,7 @@ export interface IFileContent extends IBaseStatWithMetadata {
|
||||
/**
|
||||
* The content of a file as buffer.
|
||||
*/
|
||||
value: VSBuffer;
|
||||
readonly value: VSBuffer;
|
||||
}
|
||||
|
||||
export interface IFileStreamContent extends IBaseStatWithMetadata {
|
||||
@@ -807,10 +931,10 @@ export interface IFileStreamContent extends IBaseStatWithMetadata {
|
||||
/**
|
||||
* The content of a file as stream.
|
||||
*/
|
||||
value: VSBufferReadableStream;
|
||||
readonly value: VSBufferReadableStream;
|
||||
}
|
||||
|
||||
export interface IReadFileOptions extends FileReadStreamOptions {
|
||||
export interface IBaseReadFileOptions extends FileReadStreamOptions {
|
||||
|
||||
/**
|
||||
* The optional etag parameter allows to return early from resolving the resource if
|
||||
@@ -821,6 +945,28 @@ export interface IReadFileOptions extends FileReadStreamOptions {
|
||||
readonly etag?: string;
|
||||
}
|
||||
|
||||
export interface IReadFileStreamOptions extends IBaseReadFileOptions { }
|
||||
|
||||
export interface IReadFileOptions extends IBaseReadFileOptions {
|
||||
|
||||
/**
|
||||
* The optional `atomic` flag can be used to make sure
|
||||
* the `readFile` method is not running in parallel with
|
||||
* any `write` operations in the same process.
|
||||
*
|
||||
* Typically you should not need to use this flag but if
|
||||
* for example you are quickly reading a file right after
|
||||
* a file event occured and the file changes a lot, there
|
||||
* is a chance that a read returns an empty or partial file
|
||||
* because a pending write has not finished yet.
|
||||
*
|
||||
* Note: this does not prevent the file from being written
|
||||
* to from a different process. If you need such atomic
|
||||
* operations, you better use a real database as storage.
|
||||
*/
|
||||
readonly atomic?: boolean;
|
||||
}
|
||||
|
||||
export interface IWriteFileOptions {
|
||||
|
||||
/**
|
||||
@@ -832,6 +978,11 @@ export interface IWriteFileOptions {
|
||||
* The etag of the file. This can be used to prevent dirty writes.
|
||||
*/
|
||||
readonly etag?: string;
|
||||
|
||||
/**
|
||||
* Whether to attempt to unlock a file before writing.
|
||||
*/
|
||||
readonly unlock?: boolean;
|
||||
}
|
||||
|
||||
export interface IResolveFileOptions {
|
||||
@@ -883,7 +1034,7 @@ export const enum FileOperationResult {
|
||||
FILE_NOT_MODIFIED_SINCE,
|
||||
FILE_MODIFIED_SINCE,
|
||||
FILE_MOVE_CONFLICT,
|
||||
FILE_READ_ONLY,
|
||||
FILE_WRITE_LOCKED,
|
||||
FILE_PERMISSION_DENIED,
|
||||
FILE_TOO_LARGE,
|
||||
FILE_INVALID_PATH,
|
||||
@@ -892,6 +1043,10 @@ export const enum FileOperationResult {
|
||||
FILE_OTHER_ERROR
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Settings
|
||||
|
||||
export const AutoSaveConfiguration = {
|
||||
OFF: 'off',
|
||||
AFTER_DELAY: 'afterDelay',
|
||||
@@ -911,7 +1066,7 @@ export const FILES_EXCLUDE_CONFIG = 'files.exclude';
|
||||
export interface IFilesConfiguration {
|
||||
files: {
|
||||
associations: { [filepattern: string]: string };
|
||||
exclude: glob.IExpression;
|
||||
exclude: IExpression;
|
||||
watcherExclude: { [filepattern: string]: boolean };
|
||||
encoding: string;
|
||||
autoGuessEncoding: boolean;
|
||||
@@ -926,6 +1081,10 @@ export interface IFilesConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Utilities
|
||||
|
||||
export enum FileKind {
|
||||
FILE,
|
||||
FOLDER,
|
||||
@@ -972,6 +1131,7 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
|
||||
* Helper to format a raw byte size into a human readable label.
|
||||
*/
|
||||
export class ByteSize {
|
||||
|
||||
static readonly KB = 1024;
|
||||
static readonly MB = ByteSize.KB * ByteSize.KB;
|
||||
static readonly GB = ByteSize.MB * ByteSize.KB;
|
||||
@@ -1001,3 +1161,24 @@ export class ByteSize {
|
||||
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
// Native only: Arch limits
|
||||
|
||||
export interface IArchLimits {
|
||||
readonly maxFileSize: number;
|
||||
readonly maxHeapSize: number;
|
||||
}
|
||||
|
||||
export const enum Arch {
|
||||
IA32,
|
||||
OTHER
|
||||
}
|
||||
|
||||
export function getPlatformLimits(arch: Arch): IArchLimits {
|
||||
return {
|
||||
maxFileSize: arch === Arch.IA32 ? 300 * ByteSize.MB : 16 * ByteSize.GB, // https://github.com/microsoft/vscode/issues/30180
|
||||
maxHeapSize: arch === Arch.IA32 ? 700 * ByteSize.MB : 2 * 700 * ByteSize.MB, // https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
export interface ICreateReadStreamOptions extends FileReadStreamOptions {
|
||||
|
||||
@@ -46,7 +47,11 @@ export async function readFileIntoStream<T>(
|
||||
error = options.errorTransformer(error);
|
||||
}
|
||||
|
||||
target.end(error);
|
||||
if (typeof error !== 'undefined') {
|
||||
target.error(error);
|
||||
}
|
||||
|
||||
target.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +128,7 @@ function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptio
|
||||
// Return early if file is too large to load and we have configured limits
|
||||
if (options?.limits) {
|
||||
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
|
||||
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
|
||||
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", product.nameShort), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
|
||||
}
|
||||
|
||||
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {
|
||||
|
||||
197
src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal file
197
src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, IFileSystemProviderWithFileReadWriteCapability, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, FileReadStreamOptions, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
|
||||
interface IFileChangeDto {
|
||||
resource: UriComponents;
|
||||
type: FileChangeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract file system provider that delegates all calls to a provided
|
||||
* `IChannel` via IPC communication.
|
||||
*/
|
||||
export abstract class IPCFileSystemProvider extends Disposable implements
|
||||
IFileSystemProviderWithFileReadWriteCapability,
|
||||
IFileSystemProviderWithOpenReadWriteCloseCapability,
|
||||
IFileSystemProviderWithFileReadStreamCapability,
|
||||
IFileSystemProviderWithFileFolderCopyCapability {
|
||||
|
||||
private readonly session: string = generateUuid();
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile = this._onDidChange.event;
|
||||
|
||||
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
|
||||
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
|
||||
|
||||
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
|
||||
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
|
||||
|
||||
private _capabilities = FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.FileOpenReadWriteClose
|
||||
| FileSystemProviderCapabilities.FileReadStream
|
||||
| FileSystemProviderCapabilities.FileFolderCopy
|
||||
| FileSystemProviderCapabilities.FileWriteUnlock;
|
||||
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(eventsOrError => {
|
||||
if (Array.isArray(eventsOrError)) {
|
||||
const events = eventsOrError;
|
||||
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
|
||||
} else {
|
||||
const error = eventsOrError;
|
||||
this._onDidWatchErrorOccur.fire(error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected setCaseSensitive(isCaseSensitive: boolean) {
|
||||
if (isCaseSensitive) {
|
||||
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
} else {
|
||||
this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
}
|
||||
|
||||
this._onDidChangeCapabilities.fire(undefined);
|
||||
}
|
||||
|
||||
// --- forwarding calls
|
||||
|
||||
stat(resource: URI): Promise<IStat> {
|
||||
return this.channel.call('stat', [resource]);
|
||||
}
|
||||
|
||||
open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
return this.channel.call('open', [resource, opts]);
|
||||
}
|
||||
|
||||
close(fd: number): Promise<void> {
|
||||
return this.channel.call('close', [fd]);
|
||||
}
|
||||
|
||||
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
|
||||
|
||||
// copy back the data that was written into the buffer on the remote
|
||||
// side. we need to do this because buffers are not referenced by
|
||||
// pointer, but only by value and as such cannot be directly written
|
||||
// to from the other process.
|
||||
data.set(bytes.buffer.slice(0, bytesRead), offset);
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
|
||||
|
||||
return buff.buffer;
|
||||
}
|
||||
|
||||
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
|
||||
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
|
||||
|
||||
// Reading as file stream goes through an event to the remote side
|
||||
const listener = this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
|
||||
|
||||
// data
|
||||
if (dataOrErrorOrEnd instanceof VSBuffer) {
|
||||
stream.write(dataOrErrorOrEnd.buffer);
|
||||
}
|
||||
|
||||
// end or error
|
||||
else {
|
||||
if (dataOrErrorOrEnd === 'end') {
|
||||
stream.end();
|
||||
} else {
|
||||
|
||||
// Since we receive data through a IPC channel, it is likely
|
||||
// that the error was not serialized, or only partially. To
|
||||
// ensure our API use is correct, we convert the data to an
|
||||
// error here to forward it properly.
|
||||
let error = dataOrErrorOrEnd;
|
||||
if (!(error instanceof Error)) {
|
||||
error = new Error(toErrorMessage(error));
|
||||
}
|
||||
|
||||
stream.error(error);
|
||||
stream.end();
|
||||
}
|
||||
|
||||
// Signal to the remote side that we no longer listen
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Support cancellation
|
||||
token.onCancellationRequested(() => {
|
||||
|
||||
// Ensure to end the stream properly with an error
|
||||
// to indicate the cancellation.
|
||||
stream.error(canceled());
|
||||
stream.end();
|
||||
|
||||
// Ensure to dispose the listener upon cancellation. This will
|
||||
// bubble through the remote side as event and allows to stop
|
||||
// reading the file.
|
||||
listener.dispose();
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
|
||||
}
|
||||
|
||||
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
|
||||
}
|
||||
|
||||
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
return this.channel.call('delete', [resource, opts]);
|
||||
}
|
||||
|
||||
mkdir(resource: URI): Promise<void> {
|
||||
return this.channel.call('mkdir', [resource]);
|
||||
}
|
||||
|
||||
readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
return this.channel.call('readdir', [resource]);
|
||||
}
|
||||
|
||||
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.channel.call('rename', [resource, target, opts]);
|
||||
}
|
||||
|
||||
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.channel.call('copy', [resource, target, opts]);
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
const req = Math.random();
|
||||
this.channel.call('watch', [this.session, req, resource, opts]);
|
||||
|
||||
return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
|
||||
super(logService, options);
|
||||
}
|
||||
|
||||
get capabilities(): FileSystemProviderCapabilities {
|
||||
override get capabilities(): FileSystemProviderCapabilities {
|
||||
if (!this._capabilities) {
|
||||
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
|
||||
return this._capabilities;
|
||||
}
|
||||
|
||||
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
|
||||
protected override async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
|
||||
if (!opts.useTrash) {
|
||||
return super.doDelete(filePath, opts);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
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 { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } 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';
|
||||
@@ -30,7 +30,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
usePolling: boolean;
|
||||
usePolling: boolean | string[];
|
||||
}
|
||||
|
||||
export interface IDiskFileSystemProviderOptions {
|
||||
@@ -64,7 +64,8 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
FileSystemProviderCapabilities.FileReadWrite |
|
||||
FileSystemProviderCapabilities.FileOpenReadWriteClose |
|
||||
FileSystemProviderCapabilities.FileReadStream |
|
||||
FileSystemProviderCapabilities.FileFolderCopy;
|
||||
FileSystemProviderCapabilities.FileFolderCopy |
|
||||
FileSystemProviderCapabilities.FileWriteUnlock;
|
||||
|
||||
if (isLinux) {
|
||||
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
@@ -188,12 +189,12 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
}
|
||||
|
||||
// Open
|
||||
handle = await this.open(resource, { create: true });
|
||||
handle = await this.open(resource, { create: true, unlock: opts.unlock });
|
||||
|
||||
// Write content at once
|
||||
await this.write(handle, 0, content, 0, content.byteLength);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
throw await this.toFileSystemProviderWriteError(resource, error);
|
||||
} finally {
|
||||
if (typeof handle === 'number') {
|
||||
await this.close(handle);
|
||||
@@ -203,15 +204,28 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
private readonly mapHandleToPos: Map<number, number> = new Map();
|
||||
|
||||
private readonly writeHandles: Set<number> = new Set();
|
||||
private readonly writeHandles = new Map<number, URI>();
|
||||
private canFlush: boolean = true;
|
||||
|
||||
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
// Determine wether to unlock the file (write only)
|
||||
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
|
||||
try {
|
||||
const { stat } = await SymlinkSupport.stat(filePath);
|
||||
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
|
||||
await promises.chmod(filePath, stat.mode | 0o200);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.trace(error); // ignore any errors here and try to just write
|
||||
}
|
||||
}
|
||||
|
||||
// Determine file flags for opening (read vs write)
|
||||
let flags: string | undefined = undefined;
|
||||
if (opts.create) {
|
||||
if (isFileOpenForWriteOptions(opts)) {
|
||||
if (isWindows) {
|
||||
try {
|
||||
// On Windows and if the file exists, we use a different strategy of saving the file
|
||||
@@ -252,13 +266,17 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
this.mapHandleToPos.set(handle, 0);
|
||||
|
||||
// remember that this handle was used for writing
|
||||
if (opts.create) {
|
||||
this.writeHandles.add(handle);
|
||||
if (isFileOpenForWriteOptions(opts)) {
|
||||
this.writeHandles.set(handle, resource);
|
||||
}
|
||||
|
||||
return handle;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
if (isFileOpenForWriteOptions(opts)) {
|
||||
throw await this.toFileSystemProviderWriteError(resource, error);
|
||||
} else {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +406,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
return bytesWritten;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
|
||||
} finally {
|
||||
this.updatePos(fd, normalizedPos, bytesWritten);
|
||||
}
|
||||
@@ -690,9 +708,29 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
return createFileSystemProviderError(error, code);
|
||||
}
|
||||
|
||||
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
|
||||
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
|
||||
|
||||
// If the write error signals permission issues, we try
|
||||
// to read the file's mode to see if the file is write
|
||||
// locked.
|
||||
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
|
||||
try {
|
||||
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
|
||||
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
|
||||
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.trace(error); // ignore - return original error
|
||||
}
|
||||
}
|
||||
|
||||
return fileSystemProviderWriteError;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
dispose(this.recursiveWatcher);
|
||||
|
||||
@@ -124,7 +124,7 @@ export class FileWatcher extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nsfw from 'vscode-nsfw';
|
||||
import * as nsfw from 'nsfw';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
@@ -61,9 +61,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
});
|
||||
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
|
||||
}
|
||||
this.debug(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
|
||||
|
||||
// Stop watching some roots
|
||||
rootsToStopWatching.forEach(root => {
|
||||
@@ -133,9 +131,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching with nsfw: ${request.path}`);
|
||||
}
|
||||
this.debug(`Start watching with nsfw: ${request.path}`);
|
||||
|
||||
nsfw(request.path, events => {
|
||||
for (const e of events) {
|
||||
@@ -249,4 +245,8 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
private error(message: string) {
|
||||
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
|
||||
}
|
||||
|
||||
private debug(message: string) {
|
||||
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
suite('NSFW Watcher Service', async () => {
|
||||
|
||||
// Load `nsfwWatcherService` within the suite to prevent all tests
|
||||
// from failing to start if `vscode-nsfw` was not properly installed
|
||||
// from failing to start if `nsfw` was not properly installed
|
||||
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
|
||||
|
||||
class TestNsfwWatcherService extends NsfwWatcherService {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new NsfwWatcherService();
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
server.registerChannel('watcher', ProxyChannel.fromService(service));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
@@ -61,7 +61,7 @@ export class FileWatcher extends Disposable {
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
|
||||
this.service.setVerboseLogging(this.verboseLogging);
|
||||
|
||||
@@ -91,7 +91,7 @@ export class FileWatcher extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -49,7 +49,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
||||
get wacherCount() { return this._watcherCount; }
|
||||
|
||||
private pollingInterval?: number;
|
||||
private usePolling?: boolean;
|
||||
private usePolling?: boolean | string[];
|
||||
private verboseLogging: boolean | undefined;
|
||||
|
||||
private spamCheckStartTime: number | undefined;
|
||||
@@ -101,7 +101,11 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
||||
|
||||
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
|
||||
const pollingInterval = this.pollingInterval || 5000;
|
||||
const usePolling = this.usePolling;
|
||||
let usePolling = this.usePolling; // boolean or a list of path patterns
|
||||
if (Array.isArray(usePolling)) {
|
||||
// switch to polling if one of the paths matches with a watched path
|
||||
usePolling = usePolling.some(pattern => requests.some(r => glob.match(pattern, r.path)));
|
||||
}
|
||||
|
||||
const watcherOpts: chokidar.WatchOptions = {
|
||||
ignoreInitial: true,
|
||||
@@ -142,9 +146,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
||||
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
|
||||
}
|
||||
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
|
||||
}
|
||||
this.debug(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
|
||||
|
||||
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
|
||||
this._watcherCount++;
|
||||
@@ -297,6 +299,10 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
|
||||
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
|
||||
private debug(message: string) {
|
||||
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
|
||||
private warn(message: string) {
|
||||
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IWatcherRequest {
|
||||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
usePolling?: boolean;
|
||||
usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling
|
||||
verboseLogging?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new ChokidarWatcherService();
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
server.registerChannel('watcher', ProxyChannel.fromService(service));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
@@ -62,7 +62,7 @@ export class FileWatcher extends Disposable {
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
|
||||
|
||||
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
@@ -92,7 +92,7 @@ export class FileWatcher extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IDiskFileChange {
|
||||
}
|
||||
|
||||
export interface ILogMessage {
|
||||
type: 'trace' | 'warn' | 'error';
|
||||
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
import * as assert from 'assert';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
|
||||
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent, FileOpenOptions, FileReadStreamOptions, IStat, FileType } from 'vs/platform/files/common/files';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
|
||||
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
suite('File Service', () => {
|
||||
|
||||
@@ -20,6 +22,7 @@ suite('File Service', () => {
|
||||
const provider = new NullFileSystemProvider();
|
||||
|
||||
assert.strictEqual(service.canHandleResource(resource), false);
|
||||
assert.strictEqual(service.getProvider(resource.scheme), undefined);
|
||||
|
||||
const registrations: IFileSystemProviderRegistrationEvent[] = [];
|
||||
service.onDidChangeFileSystemProviderRegistrations(e => {
|
||||
@@ -31,7 +34,7 @@ suite('File Service', () => {
|
||||
capabilityChanges.push(e);
|
||||
});
|
||||
|
||||
let registrationDisposable: IDisposable | undefined = undefined;
|
||||
let registrationDisposable: IDisposable | undefined;
|
||||
let callCount = 0;
|
||||
service.onWillActivateFileSystemProvider(e => {
|
||||
callCount++;
|
||||
@@ -48,6 +51,7 @@ suite('File Service', () => {
|
||||
await service.activateProvider('test');
|
||||
|
||||
assert.strictEqual(service.canHandleResource(resource), true);
|
||||
assert.strictEqual(service.getProvider(resource.scheme), provider);
|
||||
|
||||
assert.strictEqual(registrations.length, 1);
|
||||
assert.strictEqual(registrations[0].scheme, 'test');
|
||||
@@ -126,4 +130,82 @@ suite('File Service', () => {
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
|
||||
testReadErrorBubbles(true);
|
||||
});
|
||||
|
||||
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060)', async () => {
|
||||
testReadErrorBubbles(false);
|
||||
});
|
||||
|
||||
async function testReadErrorBubbles(async: boolean) {
|
||||
const service = new FileService(new NullLogService());
|
||||
|
||||
const provider = new class extends NullFileSystemProvider {
|
||||
override async stat(resource: URI): Promise<IStat> {
|
||||
return {
|
||||
mtime: Date.now(),
|
||||
ctime: Date.now(),
|
||||
size: 100,
|
||||
type: FileType.File
|
||||
};
|
||||
}
|
||||
|
||||
override readFile(resource: URI): Promise<Uint8Array> {
|
||||
if (async) {
|
||||
return timeout(5).then(() => { throw new Error('failed'); });
|
||||
}
|
||||
|
||||
throw new Error('failed');
|
||||
}
|
||||
|
||||
override open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
if (async) {
|
||||
return timeout(5).then(() => { throw new Error('failed'); });
|
||||
}
|
||||
|
||||
throw new Error('failed');
|
||||
}
|
||||
|
||||
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
|
||||
if (async) {
|
||||
const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);
|
||||
timeout(5).then(() => stream.error(new Error('failed')));
|
||||
|
||||
return stream;
|
||||
|
||||
}
|
||||
|
||||
throw new Error('failed');
|
||||
}
|
||||
};
|
||||
|
||||
const disposable = service.registerProvider('test', provider);
|
||||
|
||||
for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) {
|
||||
provider.setCapabilities(capabilities);
|
||||
|
||||
let e1;
|
||||
try {
|
||||
await service.readFile(URI.parse('test://foo/bar'));
|
||||
} catch (error) {
|
||||
e1 = error;
|
||||
}
|
||||
|
||||
assert.ok(e1);
|
||||
|
||||
let e2;
|
||||
try {
|
||||
const stream = await service.readFileStream(URI.parse('test://foo/bar'));
|
||||
await consumeStream(stream.value, chunk => chunk[0]);
|
||||
} catch (error) {
|
||||
e2 = error;
|
||||
}
|
||||
|
||||
assert.ok(e2);
|
||||
}
|
||||
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
|
||||
disposables.add(userdataFileProvider);
|
||||
};
|
||||
|
||||
setup(async () => {
|
||||
setup(async function () {
|
||||
this.timeout(15000);
|
||||
await reload();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ import { tmpdir } from 'os';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils';
|
||||
import { join, basename, dirname, posix } from 'vs/base/common/path';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs';
|
||||
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions } from 'vs/platform/files/common/files';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -59,13 +58,14 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
private smallStatSize: boolean = false;
|
||||
|
||||
private _testCapabilities!: FileSystemProviderCapabilities;
|
||||
get capabilities(): FileSystemProviderCapabilities {
|
||||
override get capabilities(): FileSystemProviderCapabilities {
|
||||
if (!this._testCapabilities) {
|
||||
this._testCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite |
|
||||
FileSystemProviderCapabilities.FileOpenReadWriteClose |
|
||||
FileSystemProviderCapabilities.FileReadStream |
|
||||
FileSystemProviderCapabilities.Trash |
|
||||
FileSystemProviderCapabilities.FileWriteUnlock |
|
||||
FileSystemProviderCapabilities.FileFolderCopy;
|
||||
|
||||
if (isLinux) {
|
||||
@@ -76,7 +76,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
return this._testCapabilities;
|
||||
}
|
||||
|
||||
set capabilities(capabilities: FileSystemProviderCapabilities) {
|
||||
override set capabilities(capabilities: FileSystemProviderCapabilities) {
|
||||
this._testCapabilities = capabilities;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
this.smallStatSize = enabled;
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
override async stat(resource: URI): Promise<IStat> {
|
||||
const res = await super.stat(resource);
|
||||
|
||||
if (this.invalidStatSize) {
|
||||
@@ -100,7 +100,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
return res;
|
||||
}
|
||||
|
||||
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
override async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
const bytesRead = await super.read(fd, pos, data, offset, length);
|
||||
|
||||
this.totalBytesRead += bytesRead;
|
||||
@@ -108,7 +108,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
override async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const res = await super.readFile(resource);
|
||||
|
||||
this.totalBytesRead += res.byteLength;
|
||||
@@ -1181,8 +1181,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
|
||||
});
|
||||
|
||||
async function testReadFile(resource: URI): Promise<void> {
|
||||
const content = await service.readFile(resource);
|
||||
test('readFile - atomic', async () => {
|
||||
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream);
|
||||
|
||||
return testReadFile(URI.file(join(testDir, 'lorem.txt')), { atomic: true });
|
||||
});
|
||||
|
||||
async function testReadFile(resource: URI, options?: IReadFileOptions): Promise<void> {
|
||||
const content = await service.readFile(resource, options);
|
||||
|
||||
assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString());
|
||||
}
|
||||
@@ -1584,6 +1590,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
|
||||
}
|
||||
|
||||
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => {
|
||||
const link = URI.file(join(testDir, 'small.js-link'));
|
||||
await promises.symlink(join(testDir, 'small.js'), link.fsPath);
|
||||
|
||||
let error: FileOperationError | undefined = undefined;
|
||||
try {
|
||||
await service.readFile(link);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
});
|
||||
|
||||
test('createFile', async () => {
|
||||
return assertCreateFile(contents => VSBuffer.fromString(contents));
|
||||
});
|
||||
@@ -1740,19 +1760,23 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
assert.ok(error!);
|
||||
}
|
||||
|
||||
test('writeFile (large file) - multiple parallel writes queue up', async () => {
|
||||
test('writeFile (large file) - multiple parallel writes queue up and atomic read support', async () => {
|
||||
const resource = URI.file(join(testDir, 'lorem.txt'));
|
||||
|
||||
const content = readFileSync(resource.fsPath);
|
||||
const newContent = content.toString() + content.toString();
|
||||
|
||||
await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
|
||||
const writePromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
|
||||
const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent));
|
||||
assert.strictEqual(fileStat.name, 'lorem.txt');
|
||||
}));
|
||||
|
||||
const fileContent = readFileSync(resource.fsPath).toString();
|
||||
assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent));
|
||||
const readPromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async () => {
|
||||
const fileContent = await service.readFile(resource, { atomic: true });
|
||||
assert.ok(fileContent.value.byteLength > 0); // `atomic: true` ensures we never read a truncated file
|
||||
}));
|
||||
|
||||
await Promise.all([writePromises, readPromises]);
|
||||
});
|
||||
|
||||
test('writeFile (readable) - default', async () => {
|
||||
@@ -1875,6 +1899,63 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
assert.strictEqual(readFileSync(resource.fsPath).toString(), content);
|
||||
});
|
||||
|
||||
test('writeFile - locked files and unlocking', async () => {
|
||||
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileWriteUnlock);
|
||||
|
||||
return testLockedFiles(false);
|
||||
});
|
||||
|
||||
test('writeFile (stream) - locked files and unlocking', async () => {
|
||||
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileWriteUnlock);
|
||||
|
||||
return testLockedFiles(false);
|
||||
});
|
||||
|
||||
test('writeFile - locked files and unlocking throws error when missing capability', async () => {
|
||||
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
|
||||
|
||||
return testLockedFiles(true);
|
||||
});
|
||||
|
||||
test('writeFile (stream) - locked files and unlocking throws error when missing capability', async () => {
|
||||
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
|
||||
|
||||
return testLockedFiles(true);
|
||||
});
|
||||
|
||||
async function testLockedFiles(expectError: boolean) {
|
||||
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
|
||||
|
||||
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
|
||||
|
||||
const stats = await promises.stat(lockedFile.fsPath);
|
||||
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
|
||||
|
||||
let error;
|
||||
const newContent = 'Updates to locked file';
|
||||
try {
|
||||
await service.writeFile(lockedFile, VSBuffer.fromString(newContent));
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
error = undefined;
|
||||
|
||||
if (expectError) {
|
||||
try {
|
||||
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
} else {
|
||||
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
|
||||
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
|
||||
}
|
||||
}
|
||||
|
||||
test('writeFile (error when folder is encountered)', async () => {
|
||||
const resource = URI.file(testDir);
|
||||
|
||||
@@ -2272,7 +2353,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
const resource = URI.file(join(testDir, 'lorem.txt'));
|
||||
|
||||
const buffer = VSBuffer.alloc(1024);
|
||||
const fdWrite = await fileProvider.open(resource, { create: true });
|
||||
const fdWrite = await fileProvider.open(resource, { create: true, unlock: false });
|
||||
const fdRead = await fileProvider.open(resource, { create: false });
|
||||
|
||||
let posInFileWrite = 0;
|
||||
|
||||
Reference in New Issue
Block a user