提交 bfe321b7 编写于 作者: B Benjamin Pasero

files2 - first cut buffered read impl

上级 ffe5db3e
......@@ -11,7 +11,7 @@ let textDecoder: TextDecoder | null;
export class VSBuffer {
public static alloc(byteLength: number): VSBuffer {
static alloc(byteLength: number): VSBuffer {
if (hasBuffer) {
return new VSBuffer(Buffer.allocUnsafe(byteLength));
} else {
......@@ -19,7 +19,7 @@ export class VSBuffer {
}
}
public static wrap(actual: Uint8Array): VSBuffer {
static wrap(actual: Uint8Array): VSBuffer {
if (hasBuffer && !(Buffer.isBuffer(actual))) {
// https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length
// Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array
......@@ -28,7 +28,7 @@ export class VSBuffer {
return new VSBuffer(actual);
}
public static fromString(source: string): VSBuffer {
static fromString(source: string): VSBuffer {
if (hasBuffer) {
return new VSBuffer(Buffer.from(source));
} else {
......@@ -39,7 +39,7 @@ export class VSBuffer {
}
}
public static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
if (typeof totalLength === 'undefined') {
totalLength = 0;
for (let i = 0, len = buffers.length; i < len; i++) {
......@@ -58,15 +58,15 @@ export class VSBuffer {
return ret;
}
public readonly buffer: Uint8Array;
public readonly byteLength: number;
readonly buffer: Uint8Array;
readonly byteLength: number;
private constructor(buffer: Uint8Array) {
this.buffer = buffer;
this.byteLength = this.buffer.byteLength;
}
public toString(): string {
toString(): string {
if (hasBuffer) {
return this.buffer.toString();
} else {
......@@ -77,30 +77,29 @@ export class VSBuffer {
}
}
public slice(start?: number, end?: number): VSBuffer {
slice(start?: number, end?: number): VSBuffer {
return new VSBuffer(this.buffer.slice(start, end));
}
public set(array: VSBuffer, offset?: number): void {
set(array: VSBuffer, offset?: number): void {
this.buffer.set(array.buffer, offset);
}
public readUint32BE(offset: number): number {
readUint32BE(offset: number): number {
return readUint32BE(this.buffer, offset);
}
public writeUint32BE(value: number, offset: number): void {
writeUint32BE(value: number, offset: number): void {
writeUint32BE(this.buffer, value, offset);
}
public readUint8(offset: number): number {
readUint8(offset: number): number {
return readUint8(this.buffer, offset);
}
public writeUint8(value: number, offset: number): void {
writeUint8(value: number, offset: number): void {
writeUint8(this.buffer, value, offset);
}
}
function readUint32BE(source: Uint8Array, offset: number): number {
......@@ -139,6 +138,27 @@ export interface VSBufferReadable {
read(): VSBuffer | null;
}
export interface VSBufferReadableStream {
/**
* The 'data' event is emitted whenever the stream is
* relinquishing ownership of a chunk of data to a consumer.
*/
on(event: 'data', callback: (chunk: VSBuffer) => void): void;
/**
* Emitted when any error occurs.
*/
on(event: 'error', callback: (err: any) => void): void;
/**
* The 'end' event is emitted when there is no more data
* to be consumed from the stream. The 'end' event will
* not be emitted unless the data is completely consumed.
*/
on(event: 'end', callback: () => void): void;
}
/**
* Helper to fully read a VSBuffer readable into a single buffer.
*/
......@@ -158,6 +178,7 @@ export function readableToBuffer(readable: VSBufferReadable): VSBuffer {
*/
export function bufferToReadable(buffer: VSBuffer): VSBufferReadable {
let done = false;
return {
read: () => {
if (done) {
......@@ -169,4 +190,215 @@ export function bufferToReadable(buffer: VSBuffer): VSBufferReadable {
return buffer;
}
};
}
/**
* Helper to fully read a VSBuffer stream into a single buffer.
*/
export function streamToBuffer(stream: VSBufferReadableStream): Promise<VSBuffer> {
return new Promise((resolve, reject) => {
const chunks: VSBuffer[] = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('error', error => reject(error));
stream.on('end', () => resolve(VSBuffer.concat(chunks)));
});
}
/**
* Helper to create a VSBufferStream from an existing VSBuffer.
*/
export function bufferToStream(buffer: VSBuffer): VSBufferReadableStream {
const stream = writeableBufferStream();
stream.end(buffer);
return stream;
}
/**
* Helper to create a VSBufferStream that can be pushed
* buffers to. Will only start to emit data when a listener
* is added.
*/
export function writeableBufferStream(): VSBufferWriteableStream {
return new VSBufferWriteableStreamImpl();
}
export interface VSBufferWriteableStream extends VSBufferReadableStream {
data(chunk: VSBuffer): void;
error(error: Error): void;
end(result?: VSBuffer | Error): void;
}
class VSBufferWriteableStreamImpl implements VSBufferWriteableStream {
private readonly state = {
flowing: false,
ended: false,
finished: false
};
private readonly buffer = {
data: [] as VSBuffer[],
error: [] as Error[]
};
private readonly listeners = {
data: [] as { (chunk: VSBuffer): void }[],
error: [] as { (error: Error): void }[],
end: [] as { (): void }[]
};
data(chunk: VSBuffer): void {
if (this.state.finished) {
return;
}
// flowing: directly send the data to listeners
if (this.state.flowing) {
this.listeners.data.forEach(listener => listener(chunk));
}
// not yet flowing: buffer data until flowing
else {
this.buffer.data.push(chunk);
}
}
error(error: Error): void {
if (this.state.finished) {
return;
}
// flowing: directly send the error to listeners
if (this.state.flowing) {
this.listeners.error.forEach(listener => listener(error));
}
// not yet flowing: buffer errors until flowing
else {
this.buffer.error.push(error);
}
}
end(result?: VSBuffer | Error): void {
if (this.state.finished) {
return;
}
// end with data or error if provided
if (result instanceof Error) {
this.error(result);
} else if (result) {
this.data(result);
}
// flowing: send end event to listeners
if (this.state.flowing) {
this.listeners.end.forEach(listener => listener());
this.finish();
}
// not yet flowing: remember state
else {
this.state.ended = true;
}
}
on(event: 'data', callback: (chunk: VSBuffer) => void): void;
on(event: 'error', callback: (err: any) => void): void;
on(event: 'end', callback: () => void): void;
on(event: 'data' | 'error' | 'end', callback: (arg0?: any) => void): void {
if (this.state.finished) {
return;
}
switch (event) {
case 'data':
this.listeners.data.push(callback);
// switch into flowing mode as soon as the first 'data'
// listener is added and we are not yet in flowing mode
if (!this.state.flowing) {
this.state.flowing = true;
// emit buffered events
this.flowData();
this.flowErrors();
this.flowEnd();
}
break;
case 'end':
this.listeners.end.push(callback);
// emit 'end' event directly if we are flowing
// and the end has already been reached
//
// finish() when it went through
if (this.state.flowing && this.flowEnd()) {
this.finish();
}
break;
case 'error':
this.listeners.error.push(callback);
// emit buffered 'error' events unless done already
// now that we know that we have at least one listener
if (this.state.flowing) {
this.flowErrors();
}
break;
}
}
private flowData(): void {
if (this.buffer.data.length > 0) {
const fullDataBuffer = VSBuffer.concat(this.buffer.data);
this.listeners.data.forEach(listener => listener(fullDataBuffer));
this.buffer.data.length = 0;
}
}
private flowErrors(): void {
if (this.listeners.error.length > 0) {
for (const error of this.buffer.error) {
this.listeners.error.forEach(listener => listener(error));
}
this.buffer.error.length = 0;
}
}
private flowEnd(): boolean {
if (this.state.ended) {
this.listeners.end.forEach(listener => listener());
return this.listeners.end.length > 0;
}
return false;
}
private finish(): void {
if (!this.state.finished) {
this.state.finished = true;
this.state.ended = true;
this.buffer.data.length = 0;
this.buffer.error.length = 0;
this.listeners.data.length = 0;
this.listeners.error.length = 0;
this.listeners.end.length = 0;
}
}
}
\ No newline at end of file
......@@ -3,18 +3,221 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { hasBuffer, VSBuffer } from 'vs/base/common/buffer';
import { VSBuffer, bufferToReadable, readableToBuffer, bufferToStream, streamToBuffer, writeableBufferStream } from 'vs/base/common/buffer';
import { timeout } from 'vs/base/common/async';
suite('Buffer', () => {
if (hasBuffer) {
test('issue #71993 - VSBuffer#toString returns numbers', () => {
const data = new Uint8Array([1, 2, 3, 'h'.charCodeAt(0), 'i'.charCodeAt(0), 4, 5]).buffer;
const buffer = VSBuffer.wrap(new Uint8Array(data, 3, 2));
assert.deepEqual(buffer.toString(), 'hi');
test('issue #71993 - VSBuffer#toString returns numbers', () => {
const data = new Uint8Array([1, 2, 3, 'h'.charCodeAt(0), 'i'.charCodeAt(0), 4, 5]).buffer;
const buffer = VSBuffer.wrap(new Uint8Array(data, 3, 2));
assert.deepEqual(buffer.toString(), 'hi');
});
test('bufferToReadable / readableToBuffer', () => {
const content = 'Hello World';
const readable = bufferToReadable(VSBuffer.fromString(content));
assert.equal(readableToBuffer(readable).toString(), content);
});
test('bufferToStream / streamToBuffer', async () => {
const content = 'Hello World';
const stream = bufferToStream(VSBuffer.fromString(content));
assert.equal((await streamToBuffer(stream)).toString(), content);
});
test('bufferWriteableStream - basics (no error)', async () => {
const stream = writeableBufferStream();
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
let ended = false;
stream.on('end', () => {
ended = true;
});
let errors: Error[] = [];
stream.on('error', error => {
errors.push(error);
});
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.end(VSBuffer.fromString('World'));
assert.equal(chunks.length, 2);
assert.equal(chunks[0].toString(), 'Hello');
assert.equal(chunks[1].toString(), 'World');
assert.equal(ended, true);
assert.equal(errors.length, 0);
});
test('bufferWriteableStream - basics (error)', async () => {
const stream = writeableBufferStream();
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
let ended = false;
stream.on('end', () => {
ended = true;
});
let errors: Error[] = [];
stream.on('error', error => {
errors.push(error);
});
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.end(new Error());
assert.equal(chunks.length, 1);
assert.equal(chunks[0].toString(), 'Hello');
assert.equal(ended, true);
assert.equal(errors.length, 1);
});
test('bufferWriteableStream - buffers data when no listener', async () => {
const stream = writeableBufferStream();
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.end(VSBuffer.fromString('World'));
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
let ended = false;
stream.on('end', () => {
ended = true;
});
let errors: Error[] = [];
stream.on('error', error => {
errors.push(error);
});
assert.equal(chunks.length, 1);
assert.equal(chunks[0].toString(), 'HelloWorld');
assert.equal(ended, true);
assert.equal(errors.length, 0);
});
test('bufferWriteableStream - buffers errors when no listener', async () => {
const stream = writeableBufferStream();
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.error(new Error());
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
let errors: Error[] = [];
stream.on('error', error => {
errors.push(error);
});
let ended = false;
stream.on('end', () => {
ended = true;
});
stream.end();
assert.equal(chunks.length, 1);
assert.equal(chunks[0].toString(), 'Hello');
assert.equal(ended, true);
assert.equal(errors.length, 1);
});
test('bufferWriteableStream - buffers end when no listener', async () => {
const stream = writeableBufferStream();
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.end(VSBuffer.fromString('World'));
let ended = false;
stream.on('end', () => {
ended = true;
});
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
}
let errors: Error[] = [];
stream.on('error', error => {
errors.push(error);
});
assert.equal(chunks.length, 1);
assert.equal(chunks[0].toString(), 'HelloWorld');
assert.equal(ended, true);
assert.equal(errors.length, 0);
});
test('bufferWriteableStream - nothing happens after end()', async () => {
const stream = writeableBufferStream();
let chunks: VSBuffer[] = [];
stream.on('data', data => {
chunks.push(data);
});
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.end(VSBuffer.fromString('World'));
let dataCalledAfterEnd = false;
stream.on('data', data => {
dataCalledAfterEnd = true;
});
let errorCalledAfterEnd = false;
stream.on('error', error => {
errorCalledAfterEnd = true;
});
let endCalledAfterEnd = false;
stream.on('end', () => {
endCalledAfterEnd = true;
});
await timeout(0);
stream.data(VSBuffer.fromString('Hello'));
await timeout(0);
stream.error(new Error());
await timeout(0);
stream.end(VSBuffer.fromString('World'));
assert.equal(dataCalledAfterEnd, false);
assert.equal(errorCalledAfterEnd, false);
assert.equal(endCalledAfterEnd, false);
assert.equal(chunks.length, 2);
assert.equal(chunks[0].toString(), 'Hello');
assert.equal(chunks[1].toString(), 'World');
});
});
......@@ -13,7 +13,7 @@ import { startsWithIgnoreCase } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isEqualOrParent, isEqual } from 'vs/base/common/resources';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer';
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
export const IFileService = createDecorator<IFileService>('fileService');
......@@ -106,19 +106,25 @@ export interface IFileService {
exists(resource: URI): Promise<boolean>;
/**
* Resolve the contents of a file identified by the resource.
*
* The returned object contains properties of the file and the full value as string.
* @deprecated use readFile() instead.
*/
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent>;
/**
* Resolve the contents of a file identified by the resource.
*
* The returned object contains properties of the file and the value as a readable stream.
* @deprecated use readFileStream() instead.
*/
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent>;
/**
* Read the contents of the provided resource unbuffered.
*/
readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent>;
/**
* Read the contents of the provided resource buffered as stream.
*/
readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent>;
/**
* Updates the content replacing its previous value.
*/
......@@ -332,6 +338,13 @@ export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderE
}
export function toFileOperationResult(error: Error): FileOperationResult {
// FileSystemProviderError comes with the result already
if (error instanceof FileOperationError) {
return error.fileOperationResult;
}
// Otherwise try to find from code
switch (toFileSystemProviderErrorCode(error)) {
case FileSystemProviderErrorCode.FileNotFound:
return FileOperationResult.FILE_NOT_FOUND;
......@@ -619,6 +632,22 @@ export interface IContent extends IBaseStatWithMetadata {
encoding: string;
}
export interface IFileContent extends IBaseStatWithMetadata {
/**
* The content of a file as buffer.
*/
value: VSBuffer;
}
export interface IFileStreamContent extends IBaseStatWithMetadata {
/**
* The content of a file as stream.
*/
value: VSBufferReadableStream;
}
// this should eventually replace IContent such
// that we have a clear separation between content
// and metadata (TODO@Joh, TODO@Ben)
......@@ -737,13 +766,7 @@ export interface IStreamContent extends IBaseStatWithMetadata {
encoding: string;
}
export interface IResolveContentOptions {
/**
* The optional acceptTextOnly parameter allows to fail this request early if the file
* contents are not textual.
*/
acceptTextOnly?: boolean;
export interface IReadFileOptions {
/**
* The optional etag parameter allows to return early from resolving the resource if
......@@ -753,6 +776,29 @@ export interface IResolveContentOptions {
*/
etag?: string;
/**
* Is an integer specifying where to begin reading from in the file. If position is null,
* data will be read from the current file position.
*/
position?: number;
/**
* If provided, the size of the file will be checked against the limits.
*/
limits?: {
size?: number;
memory?: number;
};
}
export interface IResolveContentOptions extends IReadFileOptions {
/**
* The optional acceptTextOnly parameter allows to fail this request early if the file
* contents are not textual.
*/
acceptTextOnly?: boolean;
/**
* The optional encoding parameter allows to specify the desired encoding when resolving
* the contents of the file.
......@@ -763,12 +809,6 @@ export interface IResolveContentOptions {
* The optional guessEncoding parameter allows to guess encoding from content of the file.
*/
autoGuessEncoding?: boolean;
/**
* Is an integer specifying where to begin reading from in the file. If position is null,
* data will be read from the current file position.
*/
position?: number;
}
export interface IWriteFileOptions {
......
......@@ -52,31 +52,6 @@ suite('LegacyFileService', () => {
return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE);
});
test('resolveContent - large file', function () {
const resource = uri.file(path.join(testDir, 'lorem.txt'));
return service.resolveContent(resource).then(c => {
assert.ok(c.value.length > 64000);
});
});
test('resolveContent - Files are intermingled #38331', function () {
let resource1 = uri.file(path.join(testDir, 'lorem.txt'));
let resource2 = uri.file(path.join(testDir, 'some_utf16le.css'));
let value1: string;
let value2: string;
// load in sequence and keep data
return service.resolveContent(resource1).then(c => value1 = c.value).then(() => {
return service.resolveContent(resource2).then(c => value2 = c.value);
}).then(() => {
// load in parallel in expect the same result
return Promise.all([
service.resolveContent(resource1).then(c => assert.equal(c.value, value1)),
service.resolveContent(resource2).then(c => assert.equal(c.value, value2))
]);
});
});
test('resolveContent - FILE_IS_BINARY', function () {
const resource = uri.file(path.join(testDir, 'binary.txt'));
......@@ -89,44 +64,6 @@ suite('LegacyFileService', () => {
});
});
test('resolveContent - FILE_IS_DIRECTORY', function () {
const resource = uri.file(path.join(testDir, 'deep'));
return service.resolveContent(resource).then(undefined, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY);
});
});
test('resolveContent - FILE_NOT_FOUND', function () {
const resource = uri.file(path.join(testDir, '404.html'));
return service.resolveContent(resource).then(undefined, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
});
});
test('resolveContent - FILE_NOT_MODIFIED_SINCE', function () {
const resource = uri.file(path.join(testDir, 'index.html'));
return service.resolveContent(resource).then(c => {
return service.resolveContent(resource, { etag: c.etag }).then(undefined, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
});
});
});
// test('resolveContent - FILE_MODIFIED_SINCE', function () {
// const resource = uri.file(path.join(testDir, 'index.html'));
// return service.resolveContent(resource).then(c => {
// fs.writeFileSync(resource.fsPath, 'Updates Incoming!');
// return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => {
// assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
// });
// });
// });
test('resolveContent - encoding picked up', function () {
const resource = uri.file(path.join(testDir, 'index.html'));
const encoding = 'windows1252';
......@@ -243,20 +180,4 @@ suite('LegacyFileService', () => {
});
});
});
test('resolveContent - from position (ASCII)', function () {
const resource = uri.file(path.join(testDir, 'small.txt'));
return service.resolveContent(resource, { position: 6 }).then(content => {
assert.equal(content.value, 'File');
});
});
test('resolveContent - from position (with umlaut)', function () {
const resource = uri.file(path.join(testDir, 'small_umlaut.txt'));
return service.resolveContent(resource, { position: Buffer.from('Small File with Ü').length }).then(content => {
assert.equal(content.value, 'mlaut');
});
});
});
......@@ -71,7 +71,7 @@ export class FileService3 extends FileService2 {
return super.resolveStreamContent(resource, options);
}
protected throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T): T {
// we really do not want to allow for changes currently
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
}
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
......@@ -14,8 +14,9 @@ import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { getBaseLabel } from 'vs/base/common/labels';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable } from 'vs/base/common/buffer';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, writeableBufferStream, VSBufferWriteableStream } from 'vs/base/common/buffer';
import { Queue } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
export class FileService2 extends Disposable implements IFileService {
......@@ -153,6 +154,16 @@ export class FileService2 extends Disposable implements IFileService {
return provider;
}
private async withReadWriteProvider(resource: URI): Promise<IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability> {
const provider = await this.withProvider(resource);
if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) {
return provider;
}
throw new Error('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the operation.');
}
//#endregion
private _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
......@@ -306,13 +317,13 @@ export class FileService2 extends Disposable implements IFileService {
}
async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource));
// validate write
const stat = await this.validateWriteFile(provider, resource, options);
const provider = this.throwIfFileSystemIsReadonly(await this.withReadWriteProvider(resource));
try {
// validate write
const stat = await this.validateWriteFile(provider, resource, options);
// mkdir recursively as needed
if (!stat) {
await this.mkdirp(provider, dirname(resource));
......@@ -324,13 +335,8 @@ export class FileService2 extends Disposable implements IFileService {
}
// write file: unbuffered
else if (hasReadWriteCapability(provider)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadable);
}
// give up if provider has insufficient capabilities
else {
return Promise.reject('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support creating a file.');
await this.doWriteUnbuffered(provider, resource, bufferOrReadable);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Failed to write file {0}", resource.toString(false)), toFileOperationResult(error), options);
......@@ -349,7 +355,7 @@ export class FileService2 extends Disposable implements IFileService {
// file cannot be directory
if ((stat.type & FileType.Directory) !== 0) {
throw new Error(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString()));
throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString()), FileOperationResult.FILE_IS_DIRECTORY, options);
}
// Dirty write prevention: if the file on disk has been changed and does not match our expected
......@@ -371,6 +377,157 @@ export class FileService2 extends Disposable implements IFileService {
return stat;
}
async readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const stream = await this.readFileStream(resource, options);
return {
...stream,
value: await streamToBuffer(stream.value)
};
}
async readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent> {
const provider = await this.withReadWriteProvider(resource);
// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
// the content of the file while resolving metadata
// but still cancel the operation in certain cases.
const cancellableSource = new CancellationTokenSource();
// validate read operation
const statPromise = this.validateReadFile(resource, options).then(stat => stat, error => {
cancellableSource.cancel();
throw error;
});
try {
// if the etag is provided, we await the result of the validation
// due to the likelyhood of hitting a NOT_MODIFIED_SINCE result.
// otherwise, we let it run in parallel to the file reading for
// optimal startup performance.
if (options && options.etag) {
await statPromise;
}
let fileStreamPromise: Promise<VSBufferReadableStream>;
// read buffered
if (hasOpenReadWriteCloseCapability(provider)) {
fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options));
}
// read unbuffered
else {
fileStreamPromise = this.readFileUnbuffered(provider, resource, options);
}
const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]);
return {
...fileStat,
value: fileStream
};
} catch (error) {
throw new FileOperationError(localize('err.read', "Failed to read file {0}", resource.toString(false)), toFileOperationResult(error), options);
}
}
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options?: IReadFileOptions): VSBufferReadableStream {
const stream = writeableBufferStream();
// do not await reading but simply return
// the stream directly since it operates
// via events. finally end the stream and
// send through the possible error
let error: Error | undefined = undefined;
this.doReadFileBuffered(provider, resource, stream, token, options).then(undefined, err => error = err).finally(() => stream.end(error));
return stream;
}
private async doReadFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, stream: VSBufferWriteableStream, token: CancellationToken, options?: IReadFileOptions): Promise<void> {
// open handle through provider
const handle = await provider.open(resource, { create: false });
try {
let buffer = VSBuffer.alloc(this.BUFFER_SIZE);
let totalBytesRead = 0;
let posInFile = options && typeof options.position === 'number' ? options.position : 0;
let bytesRead = 0;
let posInBuffer = 0;
do {
// read from source (handle) at current position (pos) into buffer (buffer) at
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
bytesRead = await provider.read(handle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
posInFile += bytesRead;
posInBuffer += bytesRead;
totalBytesRead += bytesRead;
// when buffer full, create a new one and emit it through stream
if (posInBuffer === buffer.byteLength) {
stream.data(buffer);
buffer = VSBuffer.alloc(this.BUFFER_SIZE);
posInBuffer = 0;
}
} while (bytesRead > 0 && this.throwIfCancelled(token) && this.throwIfTooLarge(totalBytesRead, options));
// wrap up with last buffer
if (posInBuffer > 0) {
stream.data(buffer.slice(0, posInBuffer));
}
} catch (error) {
throw error;
} finally {
await provider.close(handle);
}
}
private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise<VSBufferReadableStream> {
let buffer = await provider.readFile(resource);
// respect position option
if (options && typeof options.position === 'number') {
buffer = buffer.slice(options.position);
}
return bufferToStream(VSBuffer.wrap(buffer));
}
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
const stat = await this.resolve(resource, { resolveMetadata: true });
// Return early if resource is a directory
if (stat.isDirectory) {
throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString()), FileOperationResult.FILE_IS_DIRECTORY, options);
}
// Return early if file not modified since
if (options && options.etag && options.etag === stat.etag) {
throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options);
}
// Return early if file is too large to load
if (options && options.limits) {
if (typeof options.limits.memory === 'number' && stat.size > options.limits.memory) {
throw new FileOperationError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileOperationResult.FILE_EXCEED_MEMORY_LIMIT);
}
if (typeof options.limits.size === 'number' && stat.size > options.limits.size) {
throw new FileOperationError(localize('fileTooLargeError', "File is too large to open"), FileOperationResult.FILE_TOO_LARGE);
}
}
return stat;
}
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent> {
return this.joinOnLegacy.then(legacy => legacy.resolveContent(resource, options));
}
......@@ -384,8 +541,8 @@ export class FileService2 extends Disposable implements IFileService {
//#region Move/Copy/Delete/Create Folder
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(source));
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(target));
const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withReadWriteProvider(source));
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withReadWriteProvider(target));
// move
const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', overwrite);
......@@ -398,8 +555,8 @@ export class FileService2 extends Disposable implements IFileService {
}
async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
const sourceProvider = await this.withProvider(source);
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(target));
const sourceProvider = await this.withReadWriteProvider(source);
const targetProvider = this.throwIfFileSystemIsReadonly(await this.withReadWriteProvider(target));
// copy
const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);
......@@ -411,7 +568,7 @@ export class FileService2 extends Disposable implements IFileService {
return fileStat;
}
private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<'move' | 'copy'> {
private async doMoveCopy(sourceProvider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<'move' | 'copy'> {
// validation
const { exists, isCaseChange } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, overwrite);
......@@ -432,14 +589,6 @@ export class FileService2 extends Disposable implements IFileService {
return sourceProvider.copy(source, target, { overwrite: !!overwrite }).then(() => mode);
}
// otherwise, ensure we got the capabilities to do this
if (
!(hasOpenReadWriteCloseCapability(sourceProvider) || hasReadWriteCapability(sourceProvider)) ||
!(hasOpenReadWriteCloseCapability(targetProvider) || hasReadWriteCapability(targetProvider))
) {
throw new Error('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support copy.');
}
// when copying via buffer/unbuffered, we have to manually
// traverse the source if it is a folder and not a file
const sourceFile = await this.resolve(source);
......@@ -763,6 +912,7 @@ export class FileService2 extends Disposable implements IFileService {
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));
}
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
let sourceHandle: number | undefined = undefined;
let targetHandle: number | undefined = undefined;
......@@ -874,7 +1024,7 @@ export class FileService2 extends Disposable implements IFileService {
}
}
protected throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T): T {
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
}
......@@ -882,5 +1032,29 @@ export class FileService2 extends Disposable implements IFileService {
return provider;
}
private throwIfCancelled(token: CancellationToken): boolean {
if (token.isCancellationRequested) {
throw new Error('cancelled');
}
return true;
}
private throwIfTooLarge(totalBytesRead: number, options?: IReadFileOptions): boolean {
// Return early if file is too large to load
if (options && options.limits) {
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
throw new FileOperationError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileOperationResult.FILE_EXCEED_MEMORY_LIMIT);
}
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {
throw new FileOperationError(localize('fileTooLargeError', "File is too large to open"), FileOperationResult.FILE_TOO_LARGE);
}
}
return true;
}
//#endregion
}
\ No newline at end of file
......@@ -60,6 +60,8 @@ function toLineByLineReadable(content: string): VSBufferReadable {
export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
totalBytesRead: number = 0;
private _testCapabilities: FileSystemProviderCapabilities;
get capabilities(): FileSystemProviderCapabilities {
if (!this._testCapabilities) {
......@@ -79,6 +81,22 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
set capabilities(capabilities: FileSystemProviderCapabilities) {
this._testCapabilities = capabilities;
}
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;
return bytesRead;
}
async readFile(resource: URI): Promise<Uint8Array> {
const res = await super.readFile(resource);
this.totalBytesRead += res.byteLength;
return res;
}
}
suite('Disk File Service', () => {
......@@ -800,6 +818,248 @@ suite('Disk File Service', () => {
}
});
test('readFile - small file - buffered', () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - small file - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - large file - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
});
test('readFile - large file - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
});
async function testReadFile(resource: URI): Promise<void> {
const content = await service.readFile(resource);
assert.equal(content.value.toString(), readFileSync(resource.fsPath));
}
test('readFile - Files are intermingled #38331 - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
let resource1 = URI.file(join(testDir, 'lorem.txt'));
let resource2 = URI.file(join(testDir, 'some_utf16le.css'));
// load in sequence and keep data
const value1 = await service.readFile(resource1);
const value2 = await service.readFile(resource2);
// load in parallel in expect the same result
const result = await Promise.all([
service.readFile(resource1),
service.readFile(resource2)
]);
assert.equal(result[0].value.toString(), value1.value.toString());
assert.equal(result[1].value.toString(), value2.value.toString());
});
test('readFile - Files are intermingled #38331 - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
let resource1 = URI.file(join(testDir, 'lorem.txt'));
let resource2 = URI.file(join(testDir, 'some_utf16le.css'));
// load in sequence and keep data
const value1 = await service.readFile(resource1);
const value2 = await service.readFile(resource2);
// load in parallel in expect the same result
const result = await Promise.all([
service.readFile(resource1),
service.readFile(resource2)
]);
assert.equal(result[0].value.toString(), value1.value.toString());
assert.equal(result[1].value.toString(), value2.value.toString());
});
test('readFile - from position (ASCII) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'small.txt'));
const contents = await service.readFile(resource, { position: 6 });
assert.equal(contents.value.toString(), 'File');
});
test('readFile - from position (with umlaut) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'small_umlaut.txt'));
const contents = await service.readFile(resource, { position: Buffer.from('Small File with Ü').length });
assert.equal(contents.value.toString(), 'mlaut');
});
test('readFile - from position (ASCII) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'small.txt'));
const contents = await service.readFile(resource, { position: 6 });
assert.equal(contents.value.toString(), 'File');
});
test('readFile - from position (with umlaut) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'small_umlaut.txt'));
const contents = await service.readFile(resource, { position: Buffer.from('Small File with Ü').length });
assert.equal(contents.value.toString(), 'mlaut');
});
test('readFile - FILE_IS_DIRECTORY', async () => {
const resource = URI.file(join(testDir, 'deep'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource);
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY);
});
test('readFile - FILE_NOT_FOUND', async () => {
const resource = URI.file(join(testDir, '404.html'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource);
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
});
test('readFile - FILE_NOT_MODIFIED_SINCE - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'index.html'));
const contents = await service.readFile(resource);
fileProvider.totalBytesRead = 0;
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { etag: contents.etag });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
assert.equal(fileProvider.totalBytesRead, 0);
});
test('readFile - FILE_NOT_MODIFIED_SINCE - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'index.html'));
const contents = await service.readFile(resource);
fileProvider.totalBytesRead = 0;
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { etag: contents.etag });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
assert.equal(fileProvider.totalBytesRead, 0);
});
test('readFile - FILE_EXCEED_MEMORY_LIMIT - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'index.html'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { limits: { memory: 10 } });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_EXCEED_MEMORY_LIMIT);
});
test('readFile - FILE_EXCEED_MEMORY_LIMIT - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'index.html'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { limits: { memory: 10 } });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_EXCEED_MEMORY_LIMIT);
});
test('readFile - FILE_TOO_LARGE - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'index.html'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { limits: { size: 10 } });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
});
test('readFile - FILE_TOO_LARGE - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'index.html'));
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(resource, { limits: { size: 10 } });
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
});
test('createFile', async () => {
let event: FileOperationEvent;
disposables.push(service.onAfterOperation(e => event = e));
......@@ -851,7 +1111,37 @@ suite('Disk File Service', () => {
assert.equal(event!.target!.resource.fsPath, resource.fsPath);
});
test('writeFile', async () => {
test('writeFile - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
assert.equal(content, 'Small File');
const newContent = 'Updates to the small file';
await service.writeFile(resource, VSBuffer.fromString(newContent));
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (large file) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent));
assert.equal(fileStat.name, 'lorem.txt');
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
......@@ -863,7 +1153,9 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (large file)', async () => {
test('writeFile (large file) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
......@@ -890,7 +1182,37 @@ suite('Disk File Service', () => {
assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent));
});
test('writeFile (readable)', async () => {
test('writeFile (readable) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
assert.equal(content, 'Small File');
const newContent = 'Updates to the small file';
await service.writeFile(resource, toLineByLineReadable(newContent));
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (large file - readable) - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent));
assert.equal(fileStat.name, 'lorem.txt');
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (readable) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
......@@ -902,7 +1224,9 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (large file - readable)', async () => {
test('writeFile (large file - readable) - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
......
......@@ -25,7 +25,7 @@ import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/serv
import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, Workspace } from 'vs/platform/workspace/common/workspace';
import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } from 'vs/platform/files/common/files';
import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
......@@ -982,6 +982,39 @@ export class TestFileService implements IFileService {
});
}
readFile(resource: URI, options?: IReadFileOptions | undefined): Promise<IFileContent> {
return Promise.resolve({
resource: resource,
value: VSBuffer.fromString(this.content),
etag: 'index.txt',
encoding: 'utf8',
mtime: Date.now(),
name: resources.basename(resource),
size: 1
});
}
readFileStream(resource: URI, options?: IReadFileOptions | undefined): Promise<IFileStreamContent> {
return Promise.resolve({
resource: resource,
value: {
on: (event: string, callback: Function): void => {
if (event === 'data') {
callback(this.content);
}
if (event === 'end') {
callback();
}
}
},
etag: 'index.txt',
encoding: 'utf8',
mtime: Date.now(),
size: 1,
name: resources.basename(resource)
});
}
writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
return timeout(0).then(() => ({
resource,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册