upload.ts 11.8 KB
Newer Older
A
Asher 已提交
1
import { DesktopDragAndDropData } from "vs/base/browser/ui/list/listView";
A
Asher 已提交
2
import { VSBuffer, VSBufferReadableStream } from "vs/base/common/buffer";
A
Asher 已提交
3 4 5
import { Disposable } from "vs/base/common/lifecycle";
import * as path from "vs/base/common/path";
import { URI } from "vs/base/common/uri";
A
Asher 已提交
6
import { generateUuid } from "vs/base/common/uuid";
A
Asher 已提交
7
import { IFileService } from "vs/platform/files/common/files";
A
Asher 已提交
8
import { createDecorator, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
A
Asher 已提交
9
import { INotificationService, Severity } from "vs/platform/notification/common/notification";
A
Asher 已提交
10 11 12
import { IProgress, IProgressService, IProgressStep, ProgressLocation } from "vs/platform/progress/common/progress";
import { IWindowsService } from "vs/platform/windows/common/windows";
import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace";
A
Asher 已提交
13 14 15 16 17 18 19 20 21 22
import { ExplorerItem } from "vs/workbench/contrib/files/common/explorerModel";
import { IEditorGroup } from "vs/workbench/services/editor/common/editorGroupsService";
import { IEditorService } from "vs/workbench/services/editor/common/editorService";

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>;
A
Asher 已提交
23 24
}

A
Asher 已提交
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
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);
	}
A
Asher 已提交
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
}

/**
 * There doesn't seem to be a provided type for entries, so here is an
 * incomplete version.
 */
interface IEntry {
	name: string;
	isFile: boolean;
	file: (cb: (file: File) => void) => void;
	createReader: () => ({
		readEntries: (cb: (entries: Array<IEntry>) => void) => void;
	});
}

/**
 * Handles file uploads.
 */
A
Asher 已提交
79
class Upload {
A
Asher 已提交
80
	private readonly maxParallelUploads = 100;
A
Asher 已提交
81 82 83
	private readonly uploadingFiles = new Map<string, Reader | undefined>();
	private readonly fileQueue = new Map<string, File>();
	private progress: IProgress<IProgressStep> | undefined;
A
Asher 已提交
84 85
	private uploadPromise: Promise<string[]> | undefined;
	private resolveUploadPromise: (() => void) | undefined;
A
Asher 已提交
86
	private uploadedFilePaths = <string[]>[];
A
Asher 已提交
87 88 89
	private _total = 0;
	private _uploaded = 0;
	private lastPercent = 0;
A
Asher 已提交
90

A
Asher 已提交
91
	public constructor(
A
Asher 已提交
92 93 94
		@INotificationService private notificationService: INotificationService,
		@IProgressService private progressService: IProgressService,
		@IFileService private fileService: IFileService,
A
Asher 已提交
95
	) {}
A
Asher 已提交
96 97 98 99 100 101

	/**
	 * 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.
	 */
A
Asher 已提交
102
	public async uploadDropped(event: DragEvent, uploadDir: URI): Promise<string[]> {
A
Asher 已提交
103 104
		await this.queueFiles(event, uploadDir);
		if (!this.uploadPromise) {
A
Asher 已提交
105 106 107 108 109
			this.uploadPromise = this.progressService.withProgress({
				cancellable: true,
				location: ProgressLocation.Notification,
				title: "Uploading files...",
			}, (progress) => {
A
Asher 已提交
110 111 112 113 114 115 116
				return new Promise((resolve): void => {
					this.progress = progress;
					this.resolveUploadPromise = (): void => {
						const uploaded = this.uploadedFilePaths;
						this.uploadPromise = undefined;
						this.resolveUploadPromise = undefined;
						this.uploadedFilePaths = [];
A
Asher 已提交
117 118 119
						this.lastPercent = 0;
						this._uploaded = 0;
						this._total = 0;
A
Asher 已提交
120 121 122
						resolve(uploaded);
					};
				});
A
Asher 已提交
123
			}, () => this.cancel());
A
Asher 已提交
124 125 126 127 128 129 130 131 132
		}
		this.uploadFiles();
		return this.uploadPromise;
	}

	/**
	 * Cancel all file uploads.
	 */
	public async cancel(): Promise<void> {
A
Asher 已提交
133 134
		this.fileQueue.clear();
		this.uploadingFiles.forEach((r) => r && r.abort());
A
Asher 已提交
135 136
	}

A
Asher 已提交
137 138 139 140 141
	private get total(): number { return this._total; }
	private set total(total: number) {
		this._total = total;
		this.updateProgress();
	}
A
Asher 已提交
142

A
Asher 已提交
143 144 145 146
	private get uploaded(): number { return this._uploaded; }
	private set uploaded(uploaded: number) {
		this._uploaded = uploaded;
		this.updateProgress();
A
Asher 已提交
147 148
	}

A
Asher 已提交
149 150 151 152 153 154
	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;
		}
A
Asher 已提交
155 156 157
	}

	/**
A
Asher 已提交
158 159
	 * Upload as many files as possible. When finished, resolve the upload
	 * promise.
A
Asher 已提交
160 161
	 */
	private uploadFiles(): void {
A
Asher 已提交
162 163 164 165 166 167 168 169 170 171 172 173 174
		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);
				}).finally(() => {
					this.uploadingFiles.delete(path);
					this.uploadFiles();
				});
A
Asher 已提交
175 176
			}
		}
A
Asher 已提交
177
		if (this.fileQueue.size === 0 && this.uploadingFiles.size === 0) {
A
Asher 已提交
178 179 180 181 182
			this.resolveUploadPromise!();
		}
	}

	/**
A
Asher 已提交
183
	 * Upload a file, asking to override if necessary.
A
Asher 已提交
184
	 */
A
Asher 已提交
185 186 187 188
	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 => {
A
Asher 已提交
189 190
				this.notificationService.prompt(
					Severity.Error,
A
Asher 已提交
191 192 193 194 195 196
					`${filePath} already exists. Overwrite?`,
					[
						{ label: "Yes", run: (): void => resolve(true)  },
						{ label: "No",  run: (): void => resolve(false) },
					],
					{ onCancel: () => resolve(false) },
A
Asher 已提交
197 198
				);
			});
A
Asher 已提交
199
			if (!overwrite) {
A
Asher 已提交
200 201 202
				return;
			}
		}
A
Asher 已提交
203 204 205 206 207 208 209
		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);
A
Asher 已提交
210 211
		reader.on("data", (data) => {
			if (data && data.byteLength > 0) {
A
Asher 已提交
212 213 214 215 216 217
				this.uploaded += data.byteLength;
			}
		});
		this.uploadingFiles.set(filePath, reader);
		await this.fileService.writeFile(tempUri, reader);
		if (reader.aborted) {
A
Asher 已提交
218
			this.uploaded += (file.size - reader.offset);
A
Asher 已提交
219 220 221 222 223
			await this.fileService.del(tempUri);
		} else {
			await this.fileService.move(tempUri, uri, true);
			this.uploadedFilePaths.push(filePath);
		}
A
Asher 已提交
224 225 226 227 228 229
	}

	/**
	 * 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.
	 */
A
Asher 已提交
230
	private async queueFiles(event: DragEvent, uploadDir: URI): Promise<void> {
A
Asher 已提交
231
		const promises: Array<Promise<void>> = [];
A
Asher 已提交
232
		for (let i = 0; event.dataTransfer && event.dataTransfer.items && i < event.dataTransfer.items.length; ++i) {
A
Asher 已提交
233 234
			const item = event.dataTransfer.items[i];
			if (typeof item.webkitGetAsEntry === "function") {
A
Asher 已提交
235
				promises.push(this.traverseItem(item.webkitGetAsEntry(), uploadDir.fsPath));
A
Asher 已提交
236 237 238
			} else {
				const file = item.getAsFile();
				if (file) {
A
Asher 已提交
239
					this.addFile(uploadDir.fsPath + "/" + file.name, file);
A
Asher 已提交
240 241 242 243 244 245 246 247 248
				}
			}
		}
		await Promise.all(promises);
	}

	/**
	 * Traverses an entry and add files to the queue.
	 */
A
Asher 已提交
249
	private async traverseItem(entry: IEntry, path: string): Promise<void> {
A
Asher 已提交
250 251 252
		if (entry.isFile) {
			return new Promise<void>((resolve): void => {
				entry.file((file) => {
A
Asher 已提交
253
					resolve(this.addFile(path + "/" + file.name, file));
A
Asher 已提交
254 255 256
				});
			});
		}
A
Asher 已提交
257
		path += "/" + entry.name;
A
Asher 已提交
258 259 260 261 262 263 264 265 266 267 268 269 270
		await new Promise((resolve): void => {
			const promises: Array<Promise<void>> = [];
			const dirReader = entry.createReader();
			// According to the spec, readEntries() must be called until it calls
			// the callback with an empty array.
			const readEntries = (): void => {
				dirReader.readEntries((entries) => {
					if (entries.length === 0) {
						Promise.all(promises).then(resolve).catch((error) => {
							this.notificationService.error(error);
							resolve();
						});
					} else {
A
Asher 已提交
271
						promises.push(...entries.map((c) => this.traverseItem(c, path)));
A
Asher 已提交
272 273 274 275 276 277 278 279 280 281 282
						readEntries();
					}
				});
			};
			readEntries();
		});
	}

	/**
	 * Add a file to the queue.
	 */
A
Asher 已提交
283 284 285
	private addFile(path: string, file: File): void {
		this.total += file.size;
		this.fileQueue.set(path, file);
A
Asher 已提交
286
	}
A
Asher 已提交
287
}
A
Asher 已提交
288

A
Asher 已提交
289
class Reader implements VSBufferReadableStream {
A
Asher 已提交
290 291 292 293
	private _offset = 0;
	private readonly size = 32000; // ~32kb max while reading in the file.
	private _aborted = false;
	private readonly reader = new FileReader();
A
Asher 已提交
294 295 296
	private paused = true;
	private buffer?: VSBuffer;
	private callbacks = new Map<string, Array<(...args: any[]) => void>>();
A
Asher 已提交
297 298 299 300 301 302 303 304

	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; }

A
Asher 已提交
305 306 307 308 309 310 311 312 313 314 315 316 317 318
	public on(event: "data" | "error" | "end", callback: (...args:any[]) => void): void {
		if (!this.callbacks.has(event)) {
			this.callbacks.set(event, []);
		}
		this.callbacks.get(event)!.push(callback);
		if (this.aborted) {
			return this.emit("error", new Error("stream has been aborted"));
		} else if (this.done) {
			return this.emit("error", new Error("stream has ended"));
		} else if (event === "end") { // Once this is being listened to we can safely start outputting data.
			this.resume();
		}
	}

A
Asher 已提交
319 320 321 322
	public abort = (): void => {
		this._aborted = true;
		this.reader.abort();
		this.reader.removeEventListener("load", this.onLoad);
A
Asher 已提交
323 324 325 326 327
		this.emit("end");
	}

	public pause(): void {
		this.paused = true;
A
Asher 已提交
328 329
	}

A
Asher 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
	public resume(): void {
		if (this.paused) {
			this.paused = false;
			this.readNextChunk();
		}
	}

	public destroy(): void {
		this.abort();
	}

	private onLoad = (): void => {
		this.buffer = VSBuffer.wrap(new Uint8Array(this.reader.result as ArrayBuffer));
		if (!this.paused) {
			this.readNextChunk();
		}
	}

	private readNextChunk(): void {
		if (this.buffer) {
			this._offset += this.buffer.byteLength;
			this.emit("data", this.buffer);
			this.buffer = undefined;
		}
		if (!this.paused) { // Could be paused during the data event.
			if (this.done) {
				this.emit("end");
			} else {
				this.reader.readAsArrayBuffer(this.file.slice(this.offset, this.offset + this.size));
A
Asher 已提交
359
			}
A
Asher 已提交
360 361 362 363 364 365 366
		}
	}

	private emit(event: "data" | "error" | "end", ...args: any[]): void {
		if (this.callbacks.has(event)) {
			this.callbacks.get(event)!.forEach((cb) => cb(...args));
		}
A
Asher 已提交
367
	}
A
Asher 已提交
368

A
Asher 已提交
369 370
	private get done(): boolean {
		return this.offset >= this.file.size;
A
Asher 已提交
371 372
	}
}