Archived
1
0

Implement file uploads

This commit is contained in:
Asher 2019-07-19 15:10:43 -05:00
parent 2be452d83e
commit e8cb6ffaa0
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
2 changed files with 356 additions and 237 deletions

View File

@ -1,3 +1,30 @@
diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts
index 7b4e9cc8d6..7722cb12c6 100644
--- a/src/vs/base/common/buffer.ts
+++ b/src/vs/base/common/buffer.ts
@@ -138,7 +138,7 @@ export interface VSBufferReadable {
* Read data from the underlying source. Will return
* null to indicate that no more data can be read.
*/
- read(): VSBuffer | null;
+ read(): VSBuffer | null | Promise<VSBuffer | null>;
}
/**
@@ -185,11 +185,11 @@ export interface VSBufferReadableStream {
/**
* Helper to fully read a VSBuffer readable into a single buffer.
*/
-export function readableToBuffer(readable: VSBufferReadable): VSBuffer {
+export async function readableToBuffer(readable: VSBufferReadable): Promise<VSBuffer> {
const chunks: VSBuffer[] = [];
let chunk: VSBuffer | null;
- while (chunk = readable.read()) {
+ while (chunk = await readable.read()) {
chunks.push(chunk);
}
diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts
index c175034f96..de7e29906a 100644 index c175034f96..de7e29906a 100644
--- a/src/vs/editor/browser/services/openerService.ts --- a/src/vs/editor/browser/services/openerService.ts
@ -311,6 +338,36 @@ index 8e1b68eb36..2b6a0d5b15 100644
+ return true; + return true;
+ } + }
+} +}
diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts
index 2054ceece3..f99dfd0b73 100644
--- a/src/vs/workbench/browser/dnd.ts
+++ b/src/vs/workbench/browser/dnd.ts
@@ -31,6 +31,7 @@ import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsSe
import { IRecentFile } from 'vs/platform/history/common/history';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { withNullAsUndefined } from 'vs/base/common/types';
+import { IUploadService } from 'vs/server/src/upload';
export interface IDraggedResource {
resource: URI;
@@ -166,14 +167,15 @@ export class ResourcesDropHandler {
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService,
- @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService
+ @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
+ @IUploadService private readonly uploadService: IUploadService,
) {
}
async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise<void> {
const untitledOrFileResources = extractResources(event).filter(r => this.fileService.canHandleResource(r.resource) || r.resource.scheme === Schemas.untitled);
if (!untitledOrFileResources.length) {
- return;
+ return this.uploadService.handleDrop(event, resolveTargetGroup, afterDrop, targetIndex);
}
// Make the window active to handle the drop properly within
diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts
index 1986fb6642..1bf169a4b4 100644 index 1986fb6642..1bf169a4b4 100644
--- a/src/vs/workbench/browser/web.main.ts --- a/src/vs/workbench/browser/web.main.ts
@ -357,10 +414,10 @@ index 1986fb6642..1bf169a4b4 100644
\ No newline at end of file \ No newline at end of file
+} +}
diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts
index b253e573ae..bde667d045 100644 index b253e573ae..e23d9c970e 100644
--- a/src/vs/workbench/browser/web.simpleservices.ts --- a/src/vs/workbench/browser/web.simpleservices.ts
+++ b/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts
@@ -53,6 +53,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur @@ -53,6 +53,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
import { IProcessEnvironment } from 'vs/base/common/platform'; import { IProcessEnvironment } from 'vs/base/common/platform';
@ -369,10 +426,13 @@ index b253e573ae..bde667d045 100644
+import { ExtensionGalleryChannelClient } from 'vs/platform/extensionManagement/node/extensionGalleryIpc'; +import { ExtensionGalleryChannelClient } from 'vs/platform/extensionManagement/node/extensionGalleryIpc';
+import { TelemetryChannelClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryChannelClient } from 'vs/platform/telemetry/node/telemetryIpc';
+import { IProductService } from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/product';
+import { IUploadService, UploadService } from 'vs/server/src/upload';
+
+registerSingleton(IUploadService, UploadService, true);
//#region Backup File //#region Backup File
@@ -125,13 +130,11 @@ export class SimpleClipboardService implements IClipboardService { @@ -125,13 +133,11 @@ export class SimpleClipboardService implements IClipboardService {
writeText(text: string, type?: string): void { } writeText(text: string, type?: string): void { }
readText(type?: string): string { readText(type?: string): string {
@ -388,7 +448,7 @@ index b253e573ae..bde667d045 100644
} }
writeFindText(text: string): void { } writeFindText(text: string): void { }
@@ -239,7 +242,17 @@ export class SimpleExtensionGalleryService implements IExtensionGalleryService { @@ -239,7 +245,17 @@ export class SimpleExtensionGalleryService implements IExtensionGalleryService {
} }
} }
@ -407,7 +467,7 @@ index b253e573ae..bde667d045 100644
//#endregion //#endregion
@@ -262,7 +275,7 @@ export class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchSer @@ -262,7 +278,7 @@ export class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchSer
checkForUpdates: any; checkForUpdates: any;
allowedBadgeProviders: string[]; allowedBadgeProviders: string[];
} }
@ -416,7 +476,7 @@ index b253e573ae..bde667d045 100644
//#endregion //#endregion
//#region ICommentService //#region ICommentService
@@ -375,7 +388,10 @@ export class SimpleExtensionTipsService implements IExtensionTipsService { @@ -375,7 +391,10 @@ export class SimpleExtensionTipsService implements IExtensionTipsService {
} }
getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } {
@ -428,7 +488,7 @@ index b253e573ae..bde667d045 100644
} }
} }
@@ -436,7 +452,16 @@ export class SimpleExtensionManagementService implements IExtensionManagementSer @@ -436,7 +455,16 @@ export class SimpleExtensionManagementService implements IExtensionManagementSer
} }
} }
@ -446,7 +506,7 @@ index b253e573ae..bde667d045 100644
//#endregion //#endregion
@@ -680,7 +705,15 @@ export class SimpleTelemetryService implements ITelemetryService { @@ -680,7 +708,15 @@ export class SimpleTelemetryService implements ITelemetryService {
} }
} }
@ -463,7 +523,7 @@ index b253e573ae..bde667d045 100644
//#endregion //#endregion
@@ -1288,4 +1321,4 @@ class SimpleTunnelService implements ITunnelService { @@ -1288,4 +1324,4 @@ class SimpleTunnelService implements ITunnelService {
registerSingleton(ITunnelService, SimpleTunnelService); registerSingleton(ITunnelService, SimpleTunnelService);
@ -1050,6 +1110,49 @@ index c08a6e37c1..31640d7e66 100644
} }
return this._extensionAllowedBadgeProviders; return this._extensionAllowedBadgeProviders;
} }
diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts
index 88ad0027e9..17476d5f26 100644
--- a/src/vs/workbench/contrib/files/browser/files.contribution.ts
+++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts
@@ -200,7 +200,7 @@ configurationRegistry.registerConfiguration({
'files.exclude': {
'type': 'object',
'markdownDescription': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."),
- 'default': { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true },
+ 'default': { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true, '**/.code-server-partial-upload-*': true },
'scope': ConfigurationScope.RESOURCE,
'additionalProperties': {
'anyOf': [
diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
index 4592b3918e..346292d086 100644
--- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
+++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
@@ -46,6 +46,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
+import { IUploadService } from 'vs/server/src/upload';
export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {
@@ -453,7 +454,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
@IInstantiationService private instantiationService: IInstantiationService,
@ITextFileService private textFileService: ITextFileService,
@IWindowService private windowService: IWindowService,
- @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
+ @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
+ @IUploadService private readonly uploadService: IUploadService,
) {
this.toDispose = [];
@@ -615,6 +617,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
+ return this.uploadService.handleExternalDrop(data, target, originalEvent);
const droppedResources = extractResources(originalEvent, true);
// Check for dropped external files to be folders
const result = await this.fileService.resolveAll(droppedResources);
diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts
index 9235c739fb..32d203eb32 100644 index 9235c739fb..32d203eb32 100644
--- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts
@ -1148,6 +1251,35 @@ index 611ab9aec9..4e4bea89be 100644
-registerSingleton(IExtensionManagementServerService, ExtensionManagementServerService); -registerSingleton(IExtensionManagementServerService, ExtensionManagementServerService);
\ No newline at end of file \ No newline at end of file
+registerSingleton(IExtensionManagementServerService, ExtensionManagementServerService); +registerSingleton(IExtensionManagementServerService, ExtensionManagementServerService);
diff --git a/src/vs/workbench/services/files/common/fileService.ts b/src/vs/workbench/services/files/common/fileService.ts
index a788aadc1f..09e6947fb7 100644
--- a/src/vs/workbench/services/files/common/fileService.ts
+++ b/src/vs/workbench/services/files/common/fileService.ts
@@ -859,7 +859,7 @@ export class FileService extends Disposable implements IFileService {
let posInFile = 0;
let chunk: VSBuffer | null;
- while (chunk = readable.read()) {
+ while (chunk = await readable.read()) {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
posInFile += chunk.byteLength;
@@ -888,7 +888,7 @@ export class FileService extends Disposable implements IFileService {
if (bufferOrReadable instanceof VSBuffer) {
buffer = bufferOrReadable;
} else {
- buffer = readableToBuffer(bufferOrReadable);
+ buffer = await readableToBuffer(bufferOrReadable);
}
return provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
@@ -1026,4 +1026,4 @@ export class FileService extends Disposable implements IFileService {
}
//#endregion
-}
\ No newline at end of file
+}
diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts
index c28adc0ad9..4517c308da 100644 index c28adc0ad9..4517c308da 100644
--- a/src/vs/workbench/workbench.web.main.ts --- a/src/vs/workbench/workbench.web.main.ts

View File

@ -1,23 +1,64 @@
import { exec } from "child_process"; import { generateUuid } from "vs/base/common/uuid";
import { appendFile } from "fs"; import { DesktopDragAndDropData } from "vs/base/browser/ui/list/listView";
import { promisify } from "util"; import { VSBuffer, VSBufferReadable } from "vs/base/common/buffer";
import { logger } from "@coder/logger"; import { Emitter, Event } from "vs/base/common/event";
import { escapePath } from "@coder/protocol"; import { Disposable } from "vs/base/common/lifecycle";
import { NotificationService, INotificationService, ProgressService, IProgressService, IProgress, Severity } from "./fill/notification"; import * as path from "vs/base/common/path";
import { URI } from "vs/base/common/uri";
import { IFileService } from "vs/platform/files/common/files";
import { createDecorator, ServiceIdentifier, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from "vs/platform/notification/common/notification";
import { IProgress, IProgressStep, IProgressService, ProgressLocation } from "vs/platform/progress/common/progress";
import { ExplorerItem } from "vs/workbench/contrib/files/common/explorerModel";
import { IEditorGroup } from "vs/workbench/services/editor/common/editorGroupsService";
import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace";
import { IWindowsService } from "vs/platform/windows/common/windows";
import { IEditorService } from "vs/workbench/services/editor/common/editorService";
export interface IURI { export const IUploadService = createDecorator<IUploadService>("uploadService");
readonly path: string;
readonly fsPath: string; export interface IUploadService {
readonly scheme: string; _serviceBrand: ServiceIdentifier<any>;
handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise<void>;
handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void>;
} }
/** export class UploadService extends Disposable implements IUploadService {
* Represents an uploadable directory, so we can query for existing files once. public _serviceBrand: any;
*/ public upload: Upload;
interface IUploadableDirectory {
existingFiles: string[]; public constructor(
filesToUpload: Map<string, File>; @IInstantiationService instantiationService: IInstantiationService,
preparePromise?: Promise<void>; @IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWindowsService private readonly windowsService: IWindowsService,
@IEditorService private readonly editorService: IEditorService,
) {
super();
this.upload = instantiationService.createInstance(Upload);
}
public async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise<void> {
// TODO: should use the workspace for the editor it was dropped on?
const target =this.contextService.getWorkspace().folders[0].uri;
const uris = (await this.upload.uploadDropped(event, target)).map((u) => URI.file(u));
if (uris.length > 0) {
await this.windowsService.addRecentlyOpened(uris.map((u) => ({ fileUri: u })));
}
const editors = uris.map((uri) => ({
resource: uri,
options: {
pinned: true,
index: targetIndex,
},
}));
const targetGroup = resolveTargetGroup();
this.editorService.openEditors(editors, targetGroup);
afterDrop(targetGroup);
}
public async handleExternalDrop(_data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
await this.upload.uploadDropped(originalEvent, target.resource);
}
} }
/** /**
@ -36,55 +77,37 @@ interface IEntry {
/** /**
* Handles file uploads. * Handles file uploads.
*/ */
export class Upload { class Upload {
private readonly maxParallelUploads = 100; private readonly maxParallelUploads = 100;
private readonly readSize = 32000; // ~32kb max while reading in the file. private readonly uploadingFiles = new Map<string, Reader | undefined>();
private readonly packetSize = 32000; // ~32kb max when writing. private readonly fileQueue = new Map<string, File>();
private readonly logger = logger.named("Upload"); private progress: IProgress<IProgressStep> | undefined;
private readonly currentlyUploadingFiles = new Map<string, File>();
private readonly queueByDirectory = new Map<string, IUploadableDirectory>();
private progress: IProgress | undefined;
private uploadPromise: Promise<string[]> | undefined; private uploadPromise: Promise<string[]> | undefined;
private resolveUploadPromise: (() => void) | undefined; private resolveUploadPromise: (() => void) | undefined;
private finished = 0;
private uploadedFilePaths = <string[]>[]; private uploadedFilePaths = <string[]>[];
private total = 0; private _total = 0;
private _uploaded = 0;
private lastPercent = 0;
public constructor( public constructor(
private _notificationService: INotificationService, @INotificationService private notificationService: INotificationService,
private _progressService: IProgressService, @IProgressService private progressService: IProgressService,
@IFileService private fileService: IFileService,
) {} ) {}
public set notificationService(service: INotificationService) {
this._notificationService = service;
}
public get notificationService(): INotificationService {
return this._notificationService;
}
public set progressService(service: IProgressService) {
this._progressService = service;
}
public get progressService(): IProgressService {
return this._progressService;
}
/** /**
* Upload dropped files. This will try to upload everything it can. Errors * Upload dropped files. This will try to upload everything it can. Errors
* will show via notifications. If an upload operation is ongoing, the files * will show via notifications. If an upload operation is ongoing, the files
* will be added to that operation. * will be added to that operation.
*/ */
public async uploadDropped(event: DragEvent, uploadDir: IURI): Promise<string[]> { public async uploadDropped(event: DragEvent, uploadDir: URI): Promise<string[]> {
this.addDirectory(uploadDir.path);
await this.queueFiles(event, uploadDir); await this.queueFiles(event, uploadDir);
this.logger.debug( // -1 so we don't include the uploadDir itself.
`Uploading ${this.queueByDirectory.size - 1} directories and ${this.total} files`,
);
await this.prepareDirectories();
if (!this.uploadPromise) { if (!this.uploadPromise) {
this.uploadPromise = this.progressService.start("Uploading files...", (progress) => { this.uploadPromise = this.progressService.withProgress({
cancellable: true,
location: ProgressLocation.Notification,
title: "Uploading files...",
}, (progress) => {
return new Promise((resolve): void => { return new Promise((resolve): void => {
this.progress = progress; this.progress = progress;
this.resolveUploadPromise = (): void => { this.resolveUploadPromise = (): void => {
@ -92,17 +115,15 @@ export class Upload {
this.uploadPromise = undefined; this.uploadPromise = undefined;
this.resolveUploadPromise = undefined; this.resolveUploadPromise = undefined;
this.uploadedFilePaths = []; this.uploadedFilePaths = [];
this.finished = 0; this.lastPercent = 0;
this.total = 0; this._uploaded = 0;
this._total = 0;
resolve(uploaded); resolve(uploaded);
}; };
}); });
}, () => { }, () => this.cancel());
this.cancel();
});
} }
this.uploadFiles(); this.uploadFiles();
return this.uploadPromise; return this.uploadPromise;
} }
@ -110,180 +131,118 @@ export class Upload {
* Cancel all file uploads. * Cancel all file uploads.
*/ */
public async cancel(): Promise<void> { public async cancel(): Promise<void> {
this.currentlyUploadingFiles.clear(); this.fileQueue.clear();
this.queueByDirectory.clear(); this.uploadingFiles.forEach((r) => r && r.abort());
}
private get total(): number { return this._total; }
private set total(total: number) {
this._total = total;
this.updateProgress();
}
private get uploaded(): number { return this._uploaded; }
private set uploaded(uploaded: number) {
this._uploaded = uploaded;
this.updateProgress();
}
private updateProgress(): void {
if (this.progress && this.total > 0) {
const percent = Math.floor((this.uploaded / this.total) * 100);
this.progress.report({ increment: percent - this.lastPercent });
this.lastPercent = percent;
}
} }
/** /**
* Create directories and get existing files. * Upload as many files as possible. When finished, resolve the upload
* On failure, show the error and remove the failed directory from the queue. * promise.
*/
private async prepareDirectories(): Promise<void> {
await Promise.all(Array.from(this.queueByDirectory).map(([path, dir]) => {
if (!dir.preparePromise) {
dir.preparePromise = this.prepareDirectory(path, dir);
}
return dir.preparePromise;
}));
}
/**
* Create a directory and get existing files.
* On failure, show the error and remove the directory from the queue.
*/
private async prepareDirectory(path: string, dir: IUploadableDirectory): Promise<void> {
await Promise.all([
promisify(exec)(`mkdir -p ${escapePath(path)}`).catch((error) => {
const message = error.message.toLowerCase();
if (message.includes("file exists")) {
throw new Error(`Unable to create directory at ${path} because a file exists there`);
}
throw new Error(error.message || `Unable to upload ${path}`);
}),
// Only get files, so we don't show an override option that will just
// fail anyway.
promisify(exec)(`find ${escapePath(path)} -maxdepth 1 -not -type d`).then((stdio) => {
dir.existingFiles = stdio.stdout.split("\n");
}),
]).catch((error) => {
this.queueByDirectory.delete(path);
this.notificationService.error(error);
});
}
/**
* Upload as many files as possible. When finished, resolve the upload promise.
*/ */
private uploadFiles(): void { private uploadFiles(): void {
const finishFileUpload = (path: string): void => { while (this.fileQueue.size > 0 && this.uploadingFiles.size < this.maxParallelUploads) {
++this.finished; const [path, file] = this.fileQueue.entries().next().value;
this.currentlyUploadingFiles.delete(path); this.fileQueue.delete(path);
this.progress!.report(Math.floor((this.finished / this.total) * 100)); if (this.uploadingFiles.has(path)) {
this.uploadFiles(); this.notificationService.error(new Error(`Already uploading ${path}`));
}; } else {
while (this.queueByDirectory.size > 0 && this.currentlyUploadingFiles.size < this.maxParallelUploads) { this.uploadingFiles.set(path, undefined);
const [dirPath, dir] = this.queueByDirectory.entries().next().value; this.uploadFile(path, file).catch((error) => {
if (dir.filesToUpload.size === 0) {
this.queueByDirectory.delete(dirPath);
continue;
}
const [filePath, item] = dir.filesToUpload.entries().next().value;
this.currentlyUploadingFiles.set(filePath, item);
dir.filesToUpload.delete(filePath);
this.uploadFile(filePath, item, dir.existingFiles).then(() => {
finishFileUpload(filePath);
}).catch((error) => {
this.notificationService.error(error); this.notificationService.error(error);
finishFileUpload(filePath); }).finally(() => {
this.uploadingFiles.delete(path);
this.uploadFiles();
}); });
} }
if (this.queueByDirectory.size === 0 && this.currentlyUploadingFiles.size === 0) { }
if (this.fileQueue.size === 0 && this.uploadingFiles.size === 0) {
this.resolveUploadPromise!(); this.resolveUploadPromise!();
} }
} }
/** /**
* Upload a file. * Upload a file, asking to override if necessary.
*/ */
private async uploadFile(path: string, file: File, existingFiles: string[]): Promise<void> { private async uploadFile(filePath: string, file: File): Promise<void> {
if (existingFiles.includes(path)) { const uri = URI.file(filePath);
const shouldOverwrite = await new Promise((resolve): void => { if (await this.fileService.exists(uri)) {
const overwrite = await new Promise<boolean>((resolve): void => {
this.notificationService.prompt( this.notificationService.prompt(
Severity.Error, Severity.Error,
`${path} already exists. Overwrite?`, `${filePath} already exists. Overwrite?`,
[{ [
label: "Yes", { label: "Yes", run: (): void => resolve(true) },
run: (): void => resolve(true), { label: "No", run: (): void => resolve(false) },
}, { ],
label: "No", { onCancel: () => resolve(false) },
run: (): void => resolve(false),
}],
() => resolve(false),
); );
}); });
if (!shouldOverwrite) { if (!overwrite) {
return; return;
} }
} }
await new Promise(async (resolve, reject): Promise<void> => { const tempUri = uri.with({
let readOffset = 0; path: path.join(
const reader = new FileReader(); path.dirname(uri.path),
const seek = (): void => { `.code-server-partial-upload-${path.basename(uri.path)}-${generateUuid()}`,
const slice = file.slice(readOffset, readOffset + this.readSize); ),
readOffset += this.readSize;
reader.readAsArrayBuffer(slice);
};
const rm = async (): Promise<void> => {
await promisify(exec)(`rm -f ${escapePath(path)}`);
};
await rm();
const load = async (): Promise<void> => {
const buffer = new Uint8Array(reader.result as ArrayBuffer);
let bufferOffset = 0;
while (bufferOffset <= buffer.length) {
// Got canceled while sending data.
if (!this.currentlyUploadingFiles.has(path)) {
await rm();
return resolve();
}
const data = buffer.slice(bufferOffset, bufferOffset + this.packetSize);
try {
await promisify(appendFile)(path, data);
} catch (error) {
await rm();
const message = error.message.toLowerCase();
if (message.includes("no space")) {
return reject(new Error("You are out of disk space"));
} else if (message.includes("is a directory")) {
return reject(new Error(`Unable to upload ${path} because there is a directory there`));
}
return reject(new Error(error.message || `Unable to upload ${path}`));
}
bufferOffset += this.packetSize;
}
if (readOffset >= file.size) {
this.uploadedFilePaths.push(path);
return resolve();
}
seek();
};
reader.addEventListener("load", load);
seek();
}); });
const reader = new Reader(file);
reader.onData((data) => {
if (data && data.length > 0) {
this.uploaded += data.byteLength;
}
});
reader.onAbort(() => {
const remaining = file.size - reader.offset;
if (remaining > 0) {
this.uploaded += remaining;
}
});
this.uploadingFiles.set(filePath, reader);
await this.fileService.writeFile(tempUri, reader);
if (reader.aborted) {
await this.fileService.del(tempUri);
} else {
await this.fileService.move(tempUri, uri, true);
this.uploadedFilePaths.push(filePath);
}
} }
/** /**
* Queue files from a drop event. We have to get the files first; we can't do * Queue files from a drop event. We have to get the files first; we can't do
* it in tandem with uploading or the entries will disappear. * it in tandem with uploading or the entries will disappear.
*/ */
private async queueFiles(event: DragEvent, uploadDir: IURI): Promise<void> { private async queueFiles(event: DragEvent, uploadDir: URI): Promise<void> {
if (!event.dataTransfer || !event.dataTransfer.items) {
return;
}
const promises: Array<Promise<void>> = []; const promises: Array<Promise<void>> = [];
for (let i = 0; i < event.dataTransfer.items.length; i++) { for (let i = 0; event.dataTransfer && event.dataTransfer.items && i < event.dataTransfer.items.length; ++i) {
const item = event.dataTransfer.items[i]; const item = event.dataTransfer.items[i];
if (typeof item.webkitGetAsEntry === "function") { if (typeof item.webkitGetAsEntry === "function") {
promises.push(this.traverseItem(item.webkitGetAsEntry(), uploadDir.fsPath).catch(this.notificationService.error)); promises.push(this.traverseItem(item.webkitGetAsEntry(), uploadDir.fsPath));
} else { } else {
const file = item.getAsFile(); const file = item.getAsFile();
if (file) { if (file) {
this.addFile(uploadDir.fsPath, uploadDir.fsPath + "/" + file.name, file); this.addFile(uploadDir.fsPath + "/" + file.name, file);
} }
} }
} }
@ -293,23 +252,15 @@ export class Upload {
/** /**
* Traverses an entry and add files to the queue. * Traverses an entry and add files to the queue.
*/ */
private async traverseItem(entry: IEntry, parentPath: string): Promise<void> { private async traverseItem(entry: IEntry, path: string): Promise<void> {
if (entry.isFile) { if (entry.isFile) {
return new Promise<void>((resolve): void => { return new Promise<void>((resolve): void => {
entry.file((file) => { entry.file((file) => {
this.addFile( resolve(this.addFile(path + "/" + file.name, file));
parentPath,
parentPath + "/" + file.name,
file,
);
resolve();
}); });
}); });
} }
path += "/" + entry.name;
parentPath += "/" + entry.name;
this.addDirectory(parentPath);
await new Promise((resolve): void => { await new Promise((resolve): void => {
const promises: Array<Promise<void>> = []; const promises: Array<Promise<void>> = [];
const dirReader = entry.createReader(); const dirReader = entry.createReader();
@ -323,7 +274,7 @@ export class Upload {
resolve(); resolve();
}); });
} else { } else {
promises.push(...entries.map((child) => this.traverseItem(child, parentPath))); promises.push(...entries.map((c) => this.traverseItem(c, path)));
readEntries(); readEntries();
} }
}); });
@ -335,24 +286,60 @@ export class Upload {
/** /**
* Add a file to the queue. * Add a file to the queue.
*/ */
private addFile(parentPath: string, path: string, file: File): void { private addFile(path: string, file: File): void {
++this.total; this.total += file.size;
this.addDirectory(parentPath); this.fileQueue.set(path, file);
this.queueByDirectory.get(parentPath)!.filesToUpload.set(path, file); }
} }
/** class Reader implements VSBufferReadable {
* Add a directory to the queue. private _offset = 0;
*/ private readonly size = 32000; // ~32kb max while reading in the file.
private addDirectory(path: string): void { private readonly _onData = new Emitter<Uint8Array | null>();
if (!this.queueByDirectory.has(path)) { public readonly onData: Event<Uint8Array | null> = this._onData.event;
this.queueByDirectory.set(path, {
existingFiles: [], private _aborted = false;
filesToUpload: new Map(), private readonly _onAbort = new Emitter<void>();
public readonly onAbort: Event<void> = this._onAbort.event;
private readonly reader = new FileReader();
public constructor(private readonly file: File) {
this.reader.addEventListener("load", this.onLoad);
}
public get offset(): number { return this._offset; }
public get aborted(): boolean { return this._aborted; }
public abort = (): void => {
this._aborted = true;
this.reader.abort();
this.reader.removeEventListener("load", this.onLoad);
this._onAbort.fire();
}
public read = async (): Promise<VSBuffer | null> => {
return new Promise<VSBuffer | null>((resolve) => {
const disposables = [
this.onAbort(() => {
disposables.forEach((d) => d.dispose());
resolve(null);
}),
this.onData((data) => {
disposables.forEach((d) => d.dispose());
resolve(data && VSBuffer.wrap(data));
}),
];
if (this.aborted || this.offset >= this.file.size) {
return this._onData.fire(null);
}
const slice = this.file.slice(this.offset, this.offset + this.size);
this._offset += this.size;
this.reader.readAsArrayBuffer(slice);
}); });
} }
}
}
// Global instance. private onLoad = () => {
export const upload = new Upload(new NotificationService(), new ProgressService()); this._onData.fire(new Uint8Array(this.reader.result as ArrayBuffer));
}
}