Home Reference Source

src/file-sharing/FileUploadManager.js

import {Map} from 'immutable';

import Logs from '../Logs';
import {FileUpload} from './FileUpload';
import {bindMethods} from '../utils/bindMethods';
import {EventEmitter} from '../eventemitter';
import {FileUploadStatus} from './FileUploadStatus';

const log = Logs.instance.getLogger('SippoJS/FileUploadManager');

/**
 * FileUploadManager objects allows creating and receiving file transfers
 * They are obtained by calling the {@link Session#createFileSharingManager}
 * method of a {@link Session} object
 * @experimental
 */
export class FileUploadManager {
	/** @protected */
	static newInstance(fileSharingService) {
		const fileSharingManager = new FileUploadManager(fileSharingService);
		fileSharingManager.initialize();
		return Promise.resolve(fileSharingManager);
	}

	/** @private */
	constructor(fileSharingService) {
		/** @private */
		this.fileSharingService = fileSharingService;
		/** @private */
		this.fileUploads = new Map();

		/** @type {EventEmitter} */
		this.emitter = new EventEmitter({
			wildcard: true,
			delimiter: '/',
		});

		bindMethods(this, [
			'onStart',
			'onStream',
			'onComplete',
			'onError',
			'onAbort',
			'onFile',
		]);
	}

	/** @private */
	initialize() {
		this.fileSharingService.emitter.on('start', this.onStart);
		this.fileSharingService.emitter.on('stream', this.onStream);
		this.fileSharingService.emitter.on('complete', this.onComplete);
		this.fileSharingService.emitter.on('error', this.onError);
		this.fileSharingService.emitter.on('abort', this.onAbort);
		this.fileSharingService.emitter.on('file', this.onFile);
	}

	/** @private */
	onStart(fileInfo) {
		log.log('start', fileInfo);
		const id = fileInfo.uploadId;
		const status = FileUploadStatus.SENDING;
		const fileUpload = this.fileUploads.get(id).with({status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		this.emitter.emit([id, 'start'], fileUpload);
	}

	/** @private */
	onStream(fileInfo) {
		log.log('stream', fileInfo);
		const id = fileInfo.uploadId;
		const bytesSent = fileInfo.sent;
		const status = FileUploadStatus.SENDING;
		const fileUpload = this.fileUploads.get(id).with({bytesSent, status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		this.emitter.emit([id, 'progress'], fileUpload);
	}

	/** @private */
	onComplete(fileInfo) {
		log.log('complete', fileInfo);
		const id = fileInfo.uploadId;
		const bytesSent = fileInfo.wrote;
		const status = FileUploadStatus.SENDING;
		const fileUpload = this.fileUploads.get(id).with({bytesSent, status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		this.emitter.emit([id, 'progress'], fileUpload);
	}

	/** @private */
	onError(error, fileInfo) {
		log.error('error', error);
		const id = fileInfo.uploadId;
		const status = FileUploadStatus.ERROR;
		const fileUpload = this.fileUploads.get(id).with({status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		this.emitter.emit([id, 'error'], fileUpload);
	}

	/** @private */
	onAbort(fileInfo) {
		log.log('abort', fileInfo);
		const id = fileInfo.uploadId;
		const bytesSent = fileInfo.sent;
		const status = FileUploadStatus.ABORTED;
		const fileUpload = this.fileUploads.get(id).with({bytesSent, status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		this.emitter.emit([id, 'abort'], fileUpload);
	}

	/** @private */
	async onFile(event) {
		log.log('file', event);
		const id = event.uploadId;
		if (event.file.size <= 0) {
			const fileUpload = this.fileUploads.get(id).with({status: FileUploadStatus.ERROR});
			this.fileUploads = this.fileUploads.set(id, fileUpload);
			this.emitter.emit([id, 'error'], fileUpload);
		} else {
			const url = await this.fileSharingService.createSharedFile(event.file.key);
			const fileUpload = this.fileUploads.get(id).with({status: FileUploadStatus.COMPLETED, url});
			this.fileUploads = this.fileUploads.set(id, fileUpload);
			this.emitter.emit([id, 'end'], fileUpload);
		}
	}

	waitForEnd(id) {
		let onEnd, onAbort, onError;
		const endPromise = new Promise((resolve) => {
			onEnd = resolve;
			this.emitter.on([id, 'end'], onEnd);
		});
		const abortPromise = new Promise((resolve) => {
			onAbort = resolve;
			this.emitter.on([id, 'abort'], onAbort);
		});
		const errorPromise = new Promise((resolve, reject) => {
			onError = reject;
			this.emitter.on([id, 'error'], onError);
		});
		return Promise.race([endPromise, abortPromise, errorPromise]).finally(() => {
			this.emitter.off([id, 'end'], onEnd);
			this.emitter.off([id, 'abort'], onAbort);
			this.emitter.off([id, 'error'], onError);
		});
	}

	/**
	 * Returns an immutable map with file uploads
	 * @return {InmutableMap<string, FileUpload}
	 */
	getFileUploads() {
		return this.fileUploads;
	}

	/**
	 * Starts uploading a new file
	 * @param {File} file Local file
	 * @return {Promise<string>} id of the new file upload
	 */
	async create(file) {
		const id = await this.fileSharingService.upload(file);
		const name = file.name;
		const type = file.type;
		const size = file.size;
		const status = FileUploadStatus.LOADING;
		const fileUpload = FileUpload.valueOf({id, name, type, size, status});
		this.fileUploads = this.fileUploads.set(id, fileUpload);
		return id;
	}

	/**
	 * Aborts a currently in progress file upload
	 * @param {FileUpload} fileUpload
	 * @return {Promise<void>}
	 */
	abort(fileUpload) {
		const id = fileUpload.getId();
		return Promise.all([this.fileSharingService.abort(id), this.waitForEnd(id)]);
	}

	/**
	 * Removes a file upload from the list
	 * @param {FileUpload} fileUpload
	 */
	remove(fileUpload) {
		if (!fileUpload.hasStatus(FileUploadStatus.COMPLETED, FileUploadStatus.ABORTED, FileUploadStatus.ERROR)) {
			throw new Error(`File Upload in ${fileUpload.getStatus().toString()} status can't be removed`);
		}
		this.fileUploads = this.fileUploads.delete(fileUpload.getId());
	}
}