files - add support for tracking bytes written and adopt for upload in web

This commit is contained in:
Benjamin Pasero 2020-06-08 12:46:15 +02:00
parent 0c73b69495
commit cbb1610c12
6 changed files with 108 additions and 67 deletions

View file

@ -76,12 +76,6 @@ suite('Stream', () => {
}; };
stream.on('error', errorListener); stream.on('error', errorListener);
let end = false;
const endListener = () => {
end = true;
};
stream.on('end', endListener);
let data = false; let data = false;
const dataListener = () => { const dataListener = () => {
data = true; data = true;

View file

@ -330,12 +330,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) // write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStream instanceof VSBuffer)) { if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStream instanceof VSBuffer)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStream); await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStream, options);
} }
// write file: buffered // write file: buffered
else { else {
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStream) : bufferOrReadableOrStream); await this.doWriteBuffered(provider, resource, bufferOrReadableOrStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStream) : bufferOrReadableOrStream, options);
} }
} catch (error) { } catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@ -953,7 +953,7 @@ export class FileService extends Disposable implements IFileService {
return isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(); return isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase();
} }
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStream: VSBufferReadable | VSBufferReadableStream): Promise<void> { private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStream: VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(async () => { return this.ensureWriteQueue(provider, resource).queue(async () => {
// open handle // open handle
@ -962,9 +962,9 @@ export class FileService extends Disposable implements IFileService {
// write into handle until all bytes from buffer have been written // write into handle until all bytes from buffer have been written
try { try {
if (isReadableStream(readableOrStream)) { if (isReadableStream(readableOrStream)) {
await this.doWriteStreamBufferedQueued(provider, handle, readableOrStream); await this.doWriteStreamBufferedQueued(provider, handle, readableOrStream, options);
} else { } else {
await this.doWriteReadableBufferedQueued(provider, handle, readableOrStream); await this.doWriteReadableBufferedQueued(provider, handle, readableOrStream, options);
} }
} catch (error) { } catch (error) {
throw ensureFileSystemProviderError(error); throw ensureFileSystemProviderError(error);
@ -976,7 +976,7 @@ export class FileService extends Disposable implements IFileService {
}); });
} }
private doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, stream: VSBufferReadableStream): Promise<void> { private doWriteStreamBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, stream: VSBufferReadableStream, options?: IWriteFileOptions): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let posInFile = 0; let posInFile = 0;
@ -986,7 +986,7 @@ export class FileService extends Disposable implements IFileService {
stream.pause(); stream.pause();
try { try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0); await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0, options);
} catch (error) { } catch (error) {
return reject(error); return reject(error);
} }
@ -1005,30 +1005,35 @@ export class FileService extends Disposable implements IFileService {
}); });
} }
private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable): Promise<void> { private async doWriteReadableBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: VSBufferReadable, options?: IWriteFileOptions): Promise<void> {
let posInFile = 0; let posInFile = 0;
let chunk: VSBuffer | null; let chunk: VSBuffer | null;
while ((chunk = readable.read()) !== null) { while ((chunk = readable.read()) !== null) {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0); await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0, options);
posInFile += chunk.byteLength; posInFile += chunk.byteLength;
} }
} }
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> { private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number, options?: IWriteFileOptions): Promise<void> {
let totalBytesWritten = 0; let totalBytesWritten = 0;
while (totalBytesWritten < length) { while (totalBytesWritten < length) {
// Write through the provider
const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten); const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
totalBytesWritten += bytesWritten; totalBytesWritten += bytesWritten;
// report progress as needed
options?.progress?.(bytesWritten);
} }
} }
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<void> { private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStream)); return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStream, options));
} }
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<void> { private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<void> {
let buffer: VSBuffer; let buffer: VSBuffer;
if (bufferOrReadableOrStream instanceof VSBuffer) { if (bufferOrReadableOrStream instanceof VSBuffer) {
buffer = bufferOrReadableOrStream; buffer = bufferOrReadableOrStream;
@ -1038,7 +1043,11 @@ export class FileService extends Disposable implements IFileService {
buffer = readableToBuffer(bufferOrReadableOrStream); buffer = readableToBuffer(bufferOrReadableOrStream);
} }
return provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true }); // Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
// Report progress as needed
options?.progress?.(buffer.byteLength);
} }
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> { private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {

View file

@ -731,6 +731,13 @@ export interface IWriteFileOptions {
* The etag of the file. This can be used to prevent dirty writes. * The etag of the file. This can be used to prevent dirty writes.
*/ */
readonly etag?: string; readonly etag?: string;
/**
* The progress callback can be used to get accurate information how many
* bytes have been written. Each call carries the length of bytes written
* since the last call was made.
*/
readonly progress?: (byteLength: number) => void;
} }
export interface IResolveFileOptions { export interface IResolveFileOptions {
@ -865,3 +872,33 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr
*/ */
export const MIN_MAX_MEMORY_SIZE_MB = 2048; export const MIN_MAX_MEMORY_SIZE_MB = 2048;
export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
/**
* Helper to format a raw byte size into a human readable label.
*/
export class BinarySize {
static readonly KB = 1024;
static readonly MB = BinarySize.KB * BinarySize.KB;
static readonly GB = BinarySize.MB * BinarySize.KB;
static readonly TB = BinarySize.GB * BinarySize.KB;
static formatSize(size: number): string {
if (size < BinarySize.KB) {
return localize('sizeB', "{0}B", size);
}
if (size < BinarySize.MB) {
return localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2));
}
if (size < BinarySize.GB) {
return localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2));
}
if (size < BinarySize.TB) {
return localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2));
}
return localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2));
}
}

View file

@ -1681,9 +1681,11 @@ suite('Disk File Service', function () {
assert.equal(content, 'Small File'); assert.equal(content, 'Small File');
const newContent = 'Updates to the small file'; const newContent = 'Updates to the small file';
await service.writeFile(resource, VSBuffer.fromString(newContent)); let totalBytes = 0;
await service.writeFile(resource, VSBuffer.fromString(newContent), { progress: byteLength => totalBytes += byteLength });
assert.equal(readFileSync(resource.fsPath), newContent); assert.equal(readFileSync(resource.fsPath), newContent);
assert.equal(totalBytes, newContent.length);
} }
test('writeFile (large file) - default', async () => { test('writeFile (large file) - default', async () => {
@ -1708,10 +1710,12 @@ suite('Disk File Service', function () {
const content = readFileSync(resource.fsPath); const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString(); const newContent = content.toString() + content.toString();
const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent)); let totalBytes = 0;
const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent), { progress: byteLength => totalBytes += byteLength });
assert.equal(fileStat.name, 'lorem.txt'); assert.equal(fileStat.name, 'lorem.txt');
assert.equal(readFileSync(resource.fsPath), newContent); assert.equal(readFileSync(resource.fsPath), newContent);
assert.equal(totalBytes, newContent.length);
} }
test('writeFile - buffered - readonly throws', async () => { test('writeFile - buffered - readonly throws', async () => {
@ -1782,9 +1786,11 @@ suite('Disk File Service', function () {
assert.equal(content, 'Small File'); assert.equal(content, 'Small File');
const newContent = 'Updates to the small file'; const newContent = 'Updates to the small file';
await service.writeFile(resource, toLineByLineReadable(newContent)); let totalBytes = 0;
await service.writeFile(resource, toLineByLineReadable(newContent), { progress: byteLength => totalBytes += byteLength });
assert.equal(readFileSync(resource.fsPath), newContent); assert.equal(readFileSync(resource.fsPath), newContent);
assert.equal(totalBytes, newContent.length);
} }
test('writeFile (large file - readable) - default', async () => { test('writeFile (large file - readable) - default', async () => {
@ -1809,10 +1815,12 @@ suite('Disk File Service', function () {
const content = readFileSync(resource.fsPath); const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString(); const newContent = content.toString() + content.toString();
const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent)); let totalBytes = 0;
const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent), { progress: byteLength => totalBytes += byteLength });
assert.equal(fileStat.name, 'lorem.txt'); assert.equal(fileStat.name, 'lorem.txt');
assert.equal(readFileSync(resource.fsPath), newContent); assert.equal(readFileSync(resource.fsPath), newContent);
assert.equal(totalBytes, newContent.length);
} }
test('writeFile (stream) - default', async () => { test('writeFile (stream) - default', async () => {
@ -1835,10 +1843,13 @@ suite('Disk File Service', function () {
const source = URI.file(join(testDir, 'small.txt')); const source = URI.file(join(testDir, 'small.txt'));
const target = URI.file(join(testDir, 'small-copy.txt')); const target = URI.file(join(testDir, 'small-copy.txt'));
const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); let totalBytes = 0;
const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath)), { progress: byteLength => totalBytes += byteLength });
assert.equal(fileStat.name, 'small-copy.txt'); assert.equal(fileStat.name, 'small-copy.txt');
assert.equal(readFileSync(source.fsPath).toString(), readFileSync(target.fsPath).toString()); const targetContents = readFileSync(target.fsPath).toString();
assert.equal(readFileSync(source.fsPath).toString(), targetContents);
assert.equal(totalBytes, targetContents.length);
} }
test('writeFile (large file - stream) - default', async () => { test('writeFile (large file - stream) - default', async () => {
@ -1861,10 +1872,13 @@ suite('Disk File Service', function () {
const source = URI.file(join(testDir, 'lorem.txt')); const source = URI.file(join(testDir, 'lorem.txt'));
const target = URI.file(join(testDir, 'lorem-copy.txt')); const target = URI.file(join(testDir, 'lorem-copy.txt'));
const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); let totalBytes = 0;
const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath)), { progress: byteLength => totalBytes += byteLength });
assert.equal(fileStat.name, 'lorem-copy.txt'); assert.equal(fileStat.name, 'lorem-copy.txt');
assert.equal(readFileSync(source.fsPath).toString(), readFileSync(target.fsPath).toString()); const targetContents = readFileSync(target.fsPath).toString();
assert.equal(readFileSync(source.fsPath).toString(), targetContents);
assert.equal(totalBytes, targetContents.length);
} }
test('writeFile (file is created including parents)', async () => { test('writeFile (file is created including parents)', async () => {

View file

@ -20,6 +20,7 @@ import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/commo
import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageService } from 'vs/platform/storage/common/storage';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
import { BinarySize } from 'vs/platform/files/common/files';
export interface IOpenCallbacks { export interface IOpenCallbacks {
openInternal: (input: EditorInput, options: EditorOptions | undefined) => Promise<void>; openInternal: (input: EditorInput, options: EditorOptions | undefined) => Promise<void>;
@ -169,33 +170,6 @@ export interface IResourceDescriptor {
readonly mime: string; readonly mime: string;
} }
class BinarySize {
static readonly KB = 1024;
static readonly MB = BinarySize.KB * BinarySize.KB;
static readonly GB = BinarySize.MB * BinarySize.KB;
static readonly TB = BinarySize.GB * BinarySize.KB;
static formatSize(size: number): string {
if (size < BinarySize.KB) {
return nls.localize('sizeB', "{0}B", size);
}
if (size < BinarySize.MB) {
return nls.localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2));
}
if (size < BinarySize.GB) {
return nls.localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2));
}
if (size < BinarySize.TB) {
return nls.localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2));
}
return nls.localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2));
}
}
interface ResourceViewerContext extends IDisposable { interface ResourceViewerContext extends IDisposable {
layout?(dimension: Dimension): void; layout?(dimension: Dimension): void;
} }

View file

@ -9,7 +9,7 @@ import * as glob from 'vs/base/common/glob';
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, BinarySize } from 'vs/platform/files/common/files';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
@ -1023,12 +1023,25 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
} }
// Report progress // Report progress
let totalBytesUploaded = 0;
const reportProgress = (fileSize: number, bytesUploaded: number): void => {
totalBytesUploaded += bytesUploaded;
let message: string;
if (operation.total === 1 && entry.name) {
message = entry.name;
} else {
message = localize('uploadProgress', "{0} of {1} files", operation.worked, operation.total);
}
if (fileSize > BinarySize.MB) {
message = localize('uploadProgressDetail', "{0} ({1} of {2})", message, BinarySize.formatSize(totalBytesUploaded), BinarySize.formatSize(fileSize));
}
progress.report({ message });
};
operation.worked++; operation.worked++;
if (operation.total === 1) { reportProgress(0, 0);
progress.report({ message: entry.name });
} else {
progress.report({ message: localize('uploadProgress', "{0} of {1} files", operation.worked, operation.total) });
}
// Handle file upload // Handle file upload
if (entry.isFile) { if (entry.isFile) {
@ -1040,12 +1053,12 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
// Chrome/Edge/Firefox support stream method // Chrome/Edge/Firefox support stream method
if (typeof file.stream === 'function') { if (typeof file.stream === 'function') {
await this.doUploadWebFileEntryBuffered(resource, file); await this.doUploadWebFileEntryBuffered(resource, file, reportProgress);
} }
// Fallback to unbuffered upload for other browsers // Fallback to unbuffered upload for other browsers
else { else {
await this.doUploadWebFileEntryUnbuffered(resource, file); await this.doUploadWebFileEntryUnbuffered(resource, file, reportProgress);
} }
return { isFile: true, resource }; return { isFile: true, resource };
@ -1087,9 +1100,9 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
} }
} }
private async doUploadWebFileEntryBuffered(resource: URI, file: File): Promise<void> { private async doUploadWebFileEntryBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise<void> {
const writeableStream = newWriteableBufferStream(); const writeableStream = newWriteableBufferStream();
const writeFilePromise = this.fileService.writeFile(resource, writeableStream); const writeFilePromise = this.fileService.writeFile(resource, writeableStream, { progress: byteLength => progressReporter(file.size, byteLength) });
// Read the file in chunks using File.stream() web APIs // Read the file in chunks using File.stream() web APIs
try { try {
@ -1110,13 +1123,13 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
await writeFilePromise; await writeFilePromise;
} }
private doUploadWebFileEntryUnbuffered(resource: URI, file: File): Promise<void> { private doUploadWebFileEntryUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async event => { reader.onload = async event => {
try { try {
if (event.target?.result instanceof ArrayBuffer) { if (event.target?.result instanceof ArrayBuffer) {
await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result))); await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result)), { progress: byteLength => progressReporter(file.size, byteLength) });
} else { } else {
throw new Error('Could not read from dropped file.'); throw new Error('Could not read from dropped file.');
} }