Intro
When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable".[Angular][ASP.NET Core] Upload chunked files
So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>.
Call observables in order
I sended file in these steps.- Read file by FileReader
- Create directory for saving chunks
- send and saving chunks
- merge chunks to one file and delete chunks
So I could write by Promise.then like below.
this.executeStep1() // return Promise<UploadResult>
.then(result => this.executeStep2(result)) // return Promise<UploadResult>
.then(result => this.executeStep3(result)) // return Promise<UploadResult>
.catch(reason => console.log(reason));
Result
I could write with pipe & flatMap.file-uploader.service.ts
public upload(file: File): Observable<UploadResult> {
const reader = new FileReader();
const fileName = file.name;
reader.readAsArrayBuffer(file);
return fromEvent(reader, 'load')
.pipe(
flatMap(r => this.sendBufferData(reader.result, fileName)),
flatMap(r => this.sendChunks(r.result, r.file)),
flatMap(r => this.endSendingData(fileName, r)),
take(1) // for call oncomplete
);
}
Each flatMap arguments were former methods return values.FileReader.onload
I thought only DOM event like Button.onclick could be used by fromEvent.But fileReader.onload was also used.
Merge observables
When I sent chunks, I used promiseAll to await all sending chunks operations.This time, I used forkJoin.
file-uploader.service.ts
private sendChunks(startResult: UploadFileResult, buffer: Uint8Array): Observable<UploadFileResult>{
...
let fileIndex = 0;
const chunkSenders = new Array<Observable<UploadResult>>();
for (let i = 0; i < buffer.length; i += this.chunkSize) {
let indexTo = i + this.chunkSize;
if (indexTo >= buffer.length) {
// [2020-05-20 Update] end index of subarray was as same as buffer.length.
indexTo = buffer.length; // for last data.
}
const formData = new FormData();
formData.append('file', new Blob([buffer.subarray(i, indexTo)]));
const sender = this.httpClient.post<UploadResult>(
'http://localhost:5000/files/chunk',
formData, {
headers: {
tmpDirectory: startResult.tmpDirectoryName,
index: fileIndex.toString()
}
});
chunkSenders.push(sender);
fileIndex += 1;
}
return forkJoin(chunkSenders)
.pipe(
// to return Observable<UploadResult>
map(result => {
const failedResult = result.find(r => r.succeeded === false);
if (failedResult != null) {
return FileUploaderService.getFailedFileResult(failedResult.errorMessage);
}
return {
result: FileUploaderService.getSucceededResult(),
tmpDirectoryName: startResult.tmpDirectoryName
};
})
);
}
Codes
file-uploader.service.ts
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable, Observer, fromEvent, of, merge, forkJoin} from 'rxjs';
import { flatMap, map, mergeAll, catchError, take } from 'rxjs/operators';
import {UploadFileResult} from './upload-file-result';
import {UploadResult} from './upload-result';
@Injectable({
providedIn: 'root'
})
export class FileUploaderService {
constructor(private httpClient: HttpClient) { }
private chunkSize = 1048576;
private static getSucceededResult(): UploadResult {
return {
succeeded: true,
errorMessage: '',
};
}
private static getFailedResult(reason: string): UploadResult {
return {
succeeded: false,
errorMessage: reason,
};
}
private static getFailedFileResult(reason: any): UploadFileResult {
return {
result: this.getFailedResult(reason),
tmpDirectoryName: ''
};
}
public upload(file: File): Observable<UploadResult> {
const reader = new FileReader();
const fileName = file.name;
reader.readAsArrayBuffer(file);
return fromEvent(reader, 'load')
.pipe(
flatMap(r => this.sendBufferData(reader.result, fileName)),
flatMap(r => this.sendChunks(r.result, r.file)),
flatMap(r => this.endSendingData(fileName, r)),
take(1)
);
}
private sendBufferData(readResult: string|ArrayBuffer,
fileName: string): Observable<{ result: UploadFileResult,
file: Uint8Array|null }> {
if (readResult == null) {
return of({
result: FileUploaderService.getFailedFileResult('failed reading file'),
file: null });
}
const fileData = new Uint8Array(readResult as ArrayBuffer);
if (fileData == null || fileData.length <= 0) {
return of({
result: FileUploaderService.getFailedFileResult('failed reading file'),
file: null });
}
return this.startSendingData(fileName)
.pipe(
map(result => {
return {
result,
file: (result.result.succeeded === true)? fileData as Uint8Array: null,
};
}));
}
private startSendingData(fileName: string): Observable<UploadFileResult> {
const formData = new FormData();
formData.append('fileName', fileName);
return this.httpClient.post<UploadFileResult>(
'http://localhost:5000/files/start',
formData, { });
}
private sendChunks(startResult: UploadFileResult, buffer: Uint8Array): Observable<UploadFileResult>{
if (startResult.result.succeeded === false) {
return of(FileUploaderService.getFailedFileResult(startResult.result.errorMessage));
}
let fileIndex = 0;
const chunkSenders = new Array<Observable<UploadResult>>();
for (let i = 0; i < buffer.length; i += this.chunkSize) {
let indexTo = i + this.chunkSize;
if (indexTo >= buffer.length) {
indexTo = buffer.length; // for last data.
}
const formData = new FormData();
formData.append('file', new Blob([buffer.subarray(i, indexTo)]));
const sender = this.httpClient.post<UploadResult>(
'http://localhost:5000/files/chunk',
formData, {
headers: {
tmpDirectory: startResult.tmpDirectoryName,
index: fileIndex.toString()
}
});
chunkSenders.push(sender);
fileIndex += 1;
}
return forkJoin(chunkSenders)
.pipe(
map(result => {
const failedResult = result.find(r => r.succeeded === false);
if (failedResult != null) {
return FileUploaderService.getFailedFileResult(failedResult.errorMessage);
}
return {
result: FileUploaderService.getSucceededResult(),
tmpDirectoryName: startResult.tmpDirectoryName
};
})
);
}
private endSendingData(fileName: string, sendChunkResult: UploadFileResult): Observable<UploadResult> {
if (sendChunkResult.result.succeeded === false) {
return of(sendChunkResult.result);
}
const formData = new FormData();
formData.append('fileName', fileName);
formData.append('tmpDirectory', sendChunkResult.tmpDirectoryName);
return this.httpClient.post<UploadResult>(
'http://localhost:5000/files/end',
formData, { });
}
}
Resources
RxJS in ActionOperators - RxJS
Show loading screen
I wanted show the loading screen.If the page size was smaller than screen, I had no problems.
But now, the page height was bigger than the screen size, I had to set the loading screen as same as the page size or move it to scrolled position.
And I choised the later one.
Get scrolled position
I could get scrolled position by "window.scrollY".And I could set the value to "top" of CSS to set the loading screen positionY.
The problem was IE hadn't been able to understand "window.scrollY".
So I had had to use "window.pageYOffset".
Window.scrollY - Web APIs | MDN
loading-screen.component.css
.loading-background{
background-color: rgba(0, 0, 0, 0.6);
color: white;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
}
loading-screen.component.html
<div *ngIf="shown" class="loading-background"
[ngStyle]="{'top': screenPositionY}">
Uploading...
</div>
loading-event.service.ts
import {EventEmitter, Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
/** show/hide loading screen service */
export class LoadingEventService {
public loadingStarted = new EventEmitter();
public loadingStopped = new EventEmitter();
constructor() { }
public startLoading() {
this.loadingStarted.emit();
}
public stopLoading() {
this.loadingStopped.emit();
}
}
loading-screen.component.ts
import { Component, OnInit } from '@angular/core';
import {LoadingEventService} from './loading-event.service';
@Component({
selector: 'app-loading-screen',
templateUrl: './loading-screen.component.html',
styleUrls: ['./loading-screen.component.css']
})
export class LoadingScreenComponent implements OnInit {
public shown = false;
public screenPositionY = '0px';
constructor(private loadingEventService: LoadingEventService) {
}
ngOnInit() {
this.loadingEventService.loadingStarted
.subscribe(_ => this.startLoading());
this.loadingEventService.loadingStopped
.subscribe(_ => this.stopLoading());
}
private startLoading() {
// window.scrollY -> window.pageYOffset for IE
this.screenPositionY = `${window.pageYOffset}px`;
this.shown = true;
}
private stopLoading() {
this.shown = false;
}
}
Stop mouse scroll
I wanted to stop mouse scroll when the file had been uploading to avoid showing not masked area.So I added stopping mouse scroll function.
loading-screen.component.ts
...
export class LoadingScreenComponent implements OnInit {
public shown = false;
public screenPositionY = '0px';
...
private startLoading() {
// window.scrollY -> window.pageYOffset for IE
this.screenPositionY = `${window.pageYOffset}px`;
document.body.style.overflow = 'hidden';
this.shown = true;
}
private stopLoading() {
document.body.style.overflow = 'auto';
this.shown = false;
}
}
コメント
コメントを投稿