ngx-upload-steroids
Version:
Angular 2+ File Uploader
272 lines (233 loc) • 9.65 kB
text/typescript
import {EventEmitter} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import {Subscriber} from 'rxjs/Subscriber';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/mergeAll';
import 'rxjs/add/operator/combineLatest';
export enum UploadStatus {
Queue,
Uploading,
Done,
Canceled
}
export interface UploadProgress {
status: UploadStatus;
data?: {
percentage: number;
speed: number;
speedHuman: string;
startTime: number | null;
endTime: number | null;
};
}
export interface UploadFile {
id: string;
fileIndex: number;
lastModifiedDate: Date;
name: string;
size: number;
type: string;
progress: UploadProgress;
response?: any;
}
export interface UploadOutput {
type: 'addedToQueue' | 'allAddedToQueue' | 'uploading' | 'done' | 'removed' | 'start' | 'cancelled' | 'dragOver' | 'dragOut' | 'drop' | 'clearedAll';
file?: UploadFile;
}
export interface UploadInput {
type: 'uploadAll' | 'uploadFile' | 'cancel' | 'cancelAll' | 'clearAll';
url?: string;
method?: string;
id?: string;
fieldName?: string;
fileIndex?: number;
file?: UploadFile;
data?: { [key: string]: string | Blob };
headers?: { [key: string]: string };
concurrency?: number;
withCredentials?: boolean;
}
export function humanizeBytes(bytes: number): string {
if (bytes === 0) {
return '0 Byte';
}
const k = 1024;
const sizes: string[] = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i: number = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
export class NgUploaderService {
fileList: FileList;
files: UploadFile[];
uploads: { file?: UploadFile, files?: UploadFile[], sub: { instance: Subscription } }[];
serviceEvents: EventEmitter<UploadOutput>;
constructor() {
this.files = [];
this.serviceEvents = new EventEmitter<any>();
this.uploads = [];
}
handleFiles(files: FileList): void {
this.fileList = files;
this.files = [].map.call(files, (file: File, i: number) => {
const uploadFile: UploadFile = {
fileIndex: i,
id: this.generateId(),
name: file.name,
size: file.size,
type: file.type,
progress: {
status: UploadStatus.Queue,
data: {
percentage: 0,
speed: null,
speedHuman: null,
startTime: null,
endTime: null
}
},
lastModifiedDate: file.lastModifiedDate
};
this.serviceEvents.emit({type: 'addedToQueue', file: uploadFile});
return uploadFile;
});
/*FORCING ONLY last file on the queue*/
if (this.files.length > 1) {
this.files = [this.files[this.files.length - 1]];
}
this.serviceEvents.emit({type: 'allAddedToQueue', file: this.files[0]});
}
initInputEvents(input: EventEmitter<UploadInput>): void {
input.subscribe((event: UploadInput) => {
switch (event.type) {
case 'uploadFile':
this.serviceEvents.emit({type: 'start', file: event.file});
let sub: { instance: Subscription } = {instance: null};
this.uploads.push({file: event.file, sub: sub});
sub.instance = this.uploadFile(event.file, event).subscribe(data => {
this.serviceEvents.emit(data);
});
break;
case 'uploadAll':
const concurrency = event.concurrency > 0 ? event.concurrency : Number.POSITIVE_INFINITY;
const subscriber = Subscriber.create((data: UploadOutput) => {
this.serviceEvents.emit(data);
});
this.uploads = this.uploads.concat(this.files.map(file => {
return {file: file, sub: null};
}));
const subscription = Observable.from(this.files.map(file => this.uploadFile(file, event)))
.mergeAll(concurrency)
.combineLatest(data => data)
.subscribe(subscriber);
break;
case 'cancel':
const id = event.id || null;
if (!id) {
return;
}
const index = this.uploads.findIndex(upload => upload.file.id === id);
if (index !== -1) {
if (this.uploads[index].sub && this.uploads[index].sub.instance) {
this.uploads[index].sub.instance.unsubscribe();
}
this.serviceEvents.emit({type: 'cancelled', file: this.uploads[index].file});
this.uploads[index].file.progress.status = UploadStatus.Canceled;
}
break;
case 'cancelAll':
this.uploads.forEach(upload => {
upload.file.progress.status = UploadStatus.Canceled;
this.serviceEvents.emit({type: 'cancelled', file: upload.file});
});
break;
case 'clearAll':
this.files = [];
this.fileList = null;
this.serviceEvents.emit({type: 'clearedAll'});
break
}
});
}
uploadFile(file: UploadFile, event: UploadInput): Observable<UploadOutput> {
return new Observable(observer => {
const url = event.url;
const method = event.method || 'POST';
const data = event.data || {};
const headers = event.headers || {};
const reader = new FileReader();
const xhr = new XMLHttpRequest();
let time: number = new Date().getTime();
let speed = 0;
xhr.upload.addEventListener('progress', (e: ProgressEvent) => {
if (e.lengthComputable) {
const percentage = Math.round((e.loaded * 100) / e.total);
const diff = new Date().getTime() - time;
speed = Math.round(e.loaded / diff * 1000);
file.progress = {
status: UploadStatus.Uploading,
data: {
percentage: percentage,
speed: speed,
speedHuman: `${humanizeBytes(speed)}/s`,
startTime: file.progress.data.startTime || new Date().getTime(),
endTime: null
}
};
observer.next({type: 'uploading', file: file});
}
}, false);
xhr.upload.addEventListener('error', (e: Event) => {
observer.error(e);
observer.complete();
});
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
const speedAverage = Math.round(file.size / (new Date().getTime() - file.progress.data.startTime) * 1000);
file.progress = {
status: UploadStatus.Done,
data: {
percentage: 100,
speed: speedAverage,
speedHuman: `${humanizeBytes(speedAverage)}/s`,
startTime: file.progress.data.startTime,
endTime: new Date().getTime()
}
};
try {
file.response = JSON.parse(xhr.response);
} catch (e) {
file.response = xhr.response;
}
observer.next({type: 'done', file: file});
observer.complete();
}
};
xhr.open(method, url, true);
xhr.withCredentials = event.withCredentials ? true : false;
const form = new FormData();
try {
const uploadFile = this.fileList.item(file.fileIndex);
const uploadIndex = this.uploads.findIndex(upload => upload.file.size === uploadFile.size);
if (this.uploads[uploadIndex].file.progress.status === UploadStatus.Canceled) {
observer.complete();
}
form.append(event.fieldName || 'file', uploadFile, uploadFile.name);
Object.keys(data).forEach(key => form.append(key, data[key]));
Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
this.serviceEvents.emit({type: 'start', file: file});
xhr.send(form);
} catch (e) {
observer.complete();
}
return () => {
xhr.abort();
reader.abort();
};
});
}
generateId(): string {
return Math.random().toString(36).substring(7);
}
}