Implement file uploads
This commit is contained in:
parent
2be452d83e
commit
e8cb6ffaa0
|
@ -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
|
||||
index c175034f96..de7e29906a 100644
|
||||
--- a/src/vs/editor/browser/services/openerService.ts
|
||||
|
@ -311,6 +338,36 @@ index 8e1b68eb36..2b6a0d5b15 100644
|
|||
+ 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
|
||||
index 1986fb6642..1bf169a4b4 100644
|
||||
--- a/src/vs/workbench/browser/web.main.ts
|
||||
|
@ -357,10 +414,10 @@ index 1986fb6642..1bf169a4b4 100644
|
|||
\ No newline at end of file
|
||||
+}
|
||||
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
|
||||
+++ 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 { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
|
@ -369,10 +426,13 @@ index b253e573ae..bde667d045 100644
|
|||
+import { ExtensionGalleryChannelClient } from 'vs/platform/extensionManagement/node/extensionGalleryIpc';
|
||||
+import { TelemetryChannelClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
+import { IProductService } from 'vs/platform/product/common/product';
|
||||
+import { IUploadService, UploadService } from 'vs/server/src/upload';
|
||||
+
|
||||
+registerSingleton(IUploadService, UploadService, true);
|
||||
|
||||
//#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 { }
|
||||
|
||||
readText(type?: string): string {
|
||||
|
@ -388,7 +448,7 @@ index b253e573ae..bde667d045 100644
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -262,7 +275,7 @@ export class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchSer
|
||||
@@ -262,7 +278,7 @@ export class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchSer
|
||||
checkForUpdates: any;
|
||||
allowedBadgeProviders: string[];
|
||||
}
|
||||
|
@ -416,7 +476,7 @@ index b253e573ae..bde667d045 100644
|
|||
//#endregion
|
||||
|
||||
//#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[]; } {
|
||||
|
@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1288,4 +1321,4 @@ class SimpleTunnelService implements ITunnelService {
|
||||
@@ -1288,4 +1324,4 @@ class SimpleTunnelService implements ITunnelService {
|
||||
|
||||
registerSingleton(ITunnelService, SimpleTunnelService);
|
||||
|
||||
|
@ -1050,6 +1110,49 @@ index c08a6e37c1..31640d7e66 100644
|
|||
}
|
||||
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
|
||||
index 9235c739fb..32d203eb32 100644
|
||||
--- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts
|
||||
|
@ -1148,6 +1251,35 @@ index 611ab9aec9..4e4bea89be 100644
|
|||
-registerSingleton(IExtensionManagementServerService, ExtensionManagementServerService);
|
||||
\ No newline at end of file
|
||||
+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
|
||||
index c28adc0ad9..4517c308da 100644
|
||||
--- a/src/vs/workbench/workbench.web.main.ts
|
||||
|
|
441
src/upload.ts
441
src/upload.ts
|
@ -1,23 +1,64 @@
|
|||
import { exec } from "child_process";
|
||||
import { appendFile } from "fs";
|
||||
import { promisify } from "util";
|
||||
import { logger } from "@coder/logger";
|
||||
import { escapePath } from "@coder/protocol";
|
||||
import { NotificationService, INotificationService, ProgressService, IProgressService, IProgress, Severity } from "./fill/notification";
|
||||
import { generateUuid } from "vs/base/common/uuid";
|
||||
import { DesktopDragAndDropData } from "vs/base/browser/ui/list/listView";
|
||||
import { VSBuffer, VSBufferReadable } from "vs/base/common/buffer";
|
||||
import { Emitter, Event } from "vs/base/common/event";
|
||||
import { Disposable } from "vs/base/common/lifecycle";
|
||||
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 {
|
||||
readonly path: string;
|
||||
readonly fsPath: string;
|
||||
readonly scheme: string;
|
||||
export const IUploadService = createDecorator<IUploadService>("uploadService");
|
||||
|
||||
export interface IUploadService {
|
||||
_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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an uploadable directory, so we can query for existing files once.
|
||||
*/
|
||||
interface IUploadableDirectory {
|
||||
existingFiles: string[];
|
||||
filesToUpload: Map<string, File>;
|
||||
preparePromise?: Promise<void>;
|
||||
export class UploadService extends Disposable implements IUploadService {
|
||||
public _serviceBrand: any;
|
||||
public upload: Upload;
|
||||
|
||||
public constructor(
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@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.
|
||||
*/
|
||||
export class Upload {
|
||||
class Upload {
|
||||
private readonly maxParallelUploads = 100;
|
||||
private readonly readSize = 32000; // ~32kb max while reading in the file.
|
||||
private readonly packetSize = 32000; // ~32kb max when writing.
|
||||
private readonly logger = logger.named("Upload");
|
||||
private readonly currentlyUploadingFiles = new Map<string, File>();
|
||||
private readonly queueByDirectory = new Map<string, IUploadableDirectory>();
|
||||
private progress: IProgress | undefined;
|
||||
private readonly uploadingFiles = new Map<string, Reader | undefined>();
|
||||
private readonly fileQueue = new Map<string, File>();
|
||||
private progress: IProgress<IProgressStep> | undefined;
|
||||
private uploadPromise: Promise<string[]> | undefined;
|
||||
private resolveUploadPromise: (() => void) | undefined;
|
||||
private finished = 0;
|
||||
private uploadedFilePaths = <string[]>[];
|
||||
private total = 0;
|
||||
private _total = 0;
|
||||
private _uploaded = 0;
|
||||
private lastPercent = 0;
|
||||
|
||||
public constructor(
|
||||
private _notificationService: INotificationService,
|
||||
private _progressService: IProgressService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@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
|
||||
* will show via notifications. If an upload operation is ongoing, the files
|
||||
* will be added to that operation.
|
||||
*/
|
||||
public async uploadDropped(event: DragEvent, uploadDir: IURI): Promise<string[]> {
|
||||
this.addDirectory(uploadDir.path);
|
||||
public async uploadDropped(event: DragEvent, uploadDir: URI): Promise<string[]> {
|
||||
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) {
|
||||
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 => {
|
||||
this.progress = progress;
|
||||
this.resolveUploadPromise = (): void => {
|
||||
|
@ -92,17 +115,15 @@ export class Upload {
|
|||
this.uploadPromise = undefined;
|
||||
this.resolveUploadPromise = undefined;
|
||||
this.uploadedFilePaths = [];
|
||||
this.finished = 0;
|
||||
this.total = 0;
|
||||
this.lastPercent = 0;
|
||||
this._uploaded = 0;
|
||||
this._total = 0;
|
||||
resolve(uploaded);
|
||||
};
|
||||
});
|
||||
}, () => {
|
||||
this.cancel();
|
||||
});
|
||||
}, () => this.cancel());
|
||||
}
|
||||
this.uploadFiles();
|
||||
|
||||
return this.uploadPromise;
|
||||
}
|
||||
|
||||
|
@ -110,180 +131,118 @@ export class Upload {
|
|||
* Cancel all file uploads.
|
||||
*/
|
||||
public async cancel(): Promise<void> {
|
||||
this.currentlyUploadingFiles.clear();
|
||||
this.queueByDirectory.clear();
|
||||
this.fileQueue.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.
|
||||
* On failure, show the error and remove the failed directory from the queue.
|
||||
*/
|
||||
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.
|
||||
* Upload as many files as possible. When finished, resolve the upload
|
||||
* promise.
|
||||
*/
|
||||
private uploadFiles(): void {
|
||||
const finishFileUpload = (path: string): void => {
|
||||
++this.finished;
|
||||
this.currentlyUploadingFiles.delete(path);
|
||||
this.progress!.report(Math.floor((this.finished / this.total) * 100));
|
||||
this.uploadFiles();
|
||||
};
|
||||
while (this.queueByDirectory.size > 0 && this.currentlyUploadingFiles.size < this.maxParallelUploads) {
|
||||
const [dirPath, dir] = this.queueByDirectory.entries().next().value;
|
||||
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) => {
|
||||
while (this.fileQueue.size > 0 && this.uploadingFiles.size < this.maxParallelUploads) {
|
||||
const [path, file] = this.fileQueue.entries().next().value;
|
||||
this.fileQueue.delete(path);
|
||||
if (this.uploadingFiles.has(path)) {
|
||||
this.notificationService.error(new Error(`Already uploading ${path}`));
|
||||
} else {
|
||||
this.uploadingFiles.set(path, undefined);
|
||||
this.uploadFile(path, file).catch((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!();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
* Upload a file, asking to override if necessary.
|
||||
*/
|
||||
private async uploadFile(path: string, file: File, existingFiles: string[]): Promise<void> {
|
||||
if (existingFiles.includes(path)) {
|
||||
const shouldOverwrite = await new Promise((resolve): void => {
|
||||
private async uploadFile(filePath: string, file: File): Promise<void> {
|
||||
const uri = URI.file(filePath);
|
||||
if (await this.fileService.exists(uri)) {
|
||||
const overwrite = await new Promise<boolean>((resolve): void => {
|
||||
this.notificationService.prompt(
|
||||
Severity.Error,
|
||||
`${path} already exists. Overwrite?`,
|
||||
[{
|
||||
label: "Yes",
|
||||
run: (): void => resolve(true),
|
||||
}, {
|
||||
label: "No",
|
||||
run: (): void => resolve(false),
|
||||
}],
|
||||
() => resolve(false),
|
||||
`${filePath} already exists. Overwrite?`,
|
||||
[
|
||||
{ label: "Yes", run: (): void => resolve(true) },
|
||||
{ label: "No", run: (): void => resolve(false) },
|
||||
],
|
||||
{ onCancel: () => resolve(false) },
|
||||
);
|
||||
});
|
||||
if (!shouldOverwrite) {
|
||||
if (!overwrite) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await new Promise(async (resolve, reject): Promise<void> => {
|
||||
let readOffset = 0;
|
||||
const reader = new FileReader();
|
||||
const seek = (): void => {
|
||||
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 tempUri = uri.with({
|
||||
path: path.join(
|
||||
path.dirname(uri.path),
|
||||
`.code-server-partial-upload-${path.basename(uri.path)}-${generateUuid()}`,
|
||||
),
|
||||
});
|
||||
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
|
||||
* it in tandem with uploading or the entries will disappear.
|
||||
*/
|
||||
private async queueFiles(event: DragEvent, uploadDir: IURI): Promise<void> {
|
||||
if (!event.dataTransfer || !event.dataTransfer.items) {
|
||||
return;
|
||||
}
|
||||
private async queueFiles(event: DragEvent, uploadDir: URI): 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];
|
||||
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 {
|
||||
const file = item.getAsFile();
|
||||
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.
|
||||
*/
|
||||
private async traverseItem(entry: IEntry, parentPath: string): Promise<void> {
|
||||
private async traverseItem(entry: IEntry, path: string): Promise<void> {
|
||||
if (entry.isFile) {
|
||||
return new Promise<void>((resolve): void => {
|
||||
entry.file((file) => {
|
||||
this.addFile(
|
||||
parentPath,
|
||||
parentPath + "/" + file.name,
|
||||
file,
|
||||
);
|
||||
resolve();
|
||||
resolve(this.addFile(path + "/" + file.name, file));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parentPath += "/" + entry.name;
|
||||
this.addDirectory(parentPath);
|
||||
|
||||
path += "/" + entry.name;
|
||||
await new Promise((resolve): void => {
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const dirReader = entry.createReader();
|
||||
|
@ -323,7 +274,7 @@ export class Upload {
|
|||
resolve();
|
||||
});
|
||||
} else {
|
||||
promises.push(...entries.map((child) => this.traverseItem(child, parentPath)));
|
||||
promises.push(...entries.map((c) => this.traverseItem(c, path)));
|
||||
readEntries();
|
||||
}
|
||||
});
|
||||
|
@ -335,24 +286,60 @@ export class Upload {
|
|||
/**
|
||||
* Add a file to the queue.
|
||||
*/
|
||||
private addFile(parentPath: string, path: string, file: File): void {
|
||||
++this.total;
|
||||
this.addDirectory(parentPath);
|
||||
this.queueByDirectory.get(parentPath)!.filesToUpload.set(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a directory to the queue.
|
||||
*/
|
||||
private addDirectory(path: string): void {
|
||||
if (!this.queueByDirectory.has(path)) {
|
||||
this.queueByDirectory.set(path, {
|
||||
existingFiles: [],
|
||||
filesToUpload: new Map(),
|
||||
});
|
||||
}
|
||||
private addFile(path: string, file: File): void {
|
||||
this.total += file.size;
|
||||
this.fileQueue.set(path, file);
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance.
|
||||
export const upload = new Upload(new NotificationService(), new ProgressService());
|
||||
class Reader implements VSBufferReadable {
|
||||
private _offset = 0;
|
||||
private readonly size = 32000; // ~32kb max while reading in the file.
|
||||
private readonly _onData = new Emitter<Uint8Array | null>();
|
||||
public readonly onData: Event<Uint8Array | null> = this._onData.event;
|
||||
|
||||
private _aborted = false;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private onLoad = () => {
|
||||
this._onData.fire(new Uint8Array(this.reader.result as ArrayBuffer));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue