MOBILE-1748 core: Read file data in chunks to prevent crashes

main
Dani Palou 2018-12-05 12:23:57 +01:00
parent 8248890189
commit 14eb35927c
5 changed files with 116 additions and 23 deletions

View File

@ -1309,6 +1309,7 @@
"core.fileuploader.more": "data",
"core.fileuploader.photoalbums": "local_moodlemobileapp",
"core.fileuploader.readingfile": "local_moodlemobileapp",
"core.fileuploader.readingfileperc": "local_moodlemobileapp",
"core.fileuploader.selectafile": "local_moodlemobileapp",
"core.fileuploader.uploadafile": "local_moodlemobileapp",
"core.fileuploader.uploading": "local_moodlemobileapp",

View File

@ -1309,6 +1309,7 @@
"core.fileuploader.more": "More",
"core.fileuploader.photoalbums": "Photo albums",
"core.fileuploader.readingfile": "Reading file",
"core.fileuploader.readingfileperc": "Reading file: {{$a}}%",
"core.fileuploader.selectafile": "Select a file",
"core.fileuploader.uploadafile": "Upload a file",
"core.fileuploader.uploading": "Uploading",

View File

@ -20,6 +20,7 @@
"more": "More",
"photoalbums": "Photo albums",
"readingfile": "Reading file",
"readingfileperc": "Reading file: {{$a}}%",
"selectafile": "Select a file",
"uploadafile": "Upload a file",
"uploading": "Uploading",

View File

@ -13,12 +13,12 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { ActionSheetController, ActionSheet, Platform } from 'ionic-angular';
import { ActionSheetController, ActionSheet, Platform, Loading } from 'ionic-angular';
import { MediaFile } from '@ionic-native/media-capture';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreFileProvider } from '@providers/file';
import { CoreFileProvider, CoreFileProgressEvent } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -93,18 +93,15 @@ export class CoreFileUploaderHelperProvider {
name = name || file.name;
const modal = this.domUtils.showModalLoading('core.fileuploader.readingfile', true);
let fileData;
// We have the data of the file to be uploaded, but not its URL (needed). Create a copy of the file to upload it.
return this.fileProvider.readFileData(file, CoreFileProvider.FORMATARRAYBUFFER).then((data) => {
fileData = data;
// Get unique name for the copy.
return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name);
}).then((newName) => {
return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name).then((newName) => {
const filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
return this.fileProvider.writeFile(filePath, fileData);
// Write the data into the file.
return this.fileProvider.writeFileDataInFile(file, filePath, (progress: CoreFileProgressEvent) => {
this.showProgressModal(modal, 'core.fileuploader.readingfileperc', progress);
});
}).catch((error) => {
this.logger.error('Error reading file to upload.', error);
modal.dismiss();
@ -681,16 +678,7 @@ export class CoreFileUploaderHelperProvider {
return this.fileUploaderProvider.uploadFile(path, options, (progress: ProgressEvent) => {
// Progress uploading.
if (progress && progress.lengthComputable) {
const perc = Math.min((progress.loaded / progress.total) * 100, 100);
if (perc >= 0) {
modal.setContent(this.translate.instant('core.fileuploader.uploadingperc', { $a: perc.toFixed(1) }));
if (modal._cmp && modal._cmp.changeDetectorRef) {
// Force a change detection, otherwise the content is not updated.
modal._cmp.changeDetectorRef.detectChanges();
}
}
}
this.showProgressModal(modal, 'core.fileuploader.uploadingperc', progress);
}, siteId).catch((error) => {
this.logger.error('Error uploading file.', error);
@ -705,4 +693,27 @@ export class CoreFileUploaderHelperProvider {
});
});
}
/**
* Show a progress modal.
*
* @param {Loading} modal The modal where to show the progress.
* @param {string} stringKey The key of the string to display.
* @param {ProgressEvent|CoreFileProgressEvent} progress The progress event.
*/
protected showProgressModal(modal: Loading, stringKey: string, progress: ProgressEvent | CoreFileProgressEvent): void {
if (progress && progress.lengthComputable) {
// Calculate the progress percentage.
const perc = Math.min((progress.loaded / progress.total) * 100, 100);
if (perc >= 0) {
modal.setContent(this.translate.instant(stringKey, { $a: perc.toFixed(1) }));
if (modal._cmp && modal._cmp.changeDetectorRef) {
// Force a change detection, otherwise the content is not updated.
modal._cmp.changeDetectorRef.detectChanges();
}
}
}
}
}

View File

@ -22,6 +22,29 @@ import { CoreMimetypeUtilsProvider } from './utils/mimetype';
import { CoreTextUtilsProvider } from './utils/text';
import { Zip } from '@ionic-native/zip';
/**
* Progress event used when writing a file data into a file.
*/
export interface CoreFileProgressEvent {
/**
* Whether the values are reliabñe.
* @type {boolean}
*/
lengthComputable?: boolean;
/**
* Number of treated bytes.
* @type {number}
*/
loaded?: number;
/**
* Total of bytes.
* @type {number}
*/
total?: number;
}
/**
* Factory to interact with the file system.
*/
@ -41,6 +64,7 @@ export class CoreFileProvider {
protected initialized = false;
protected basePath = '';
protected isHTMLAPI = false;
protected CHUNK_SIZE = 10485760; // 10 MB.
constructor(logger: CoreLoggerProvider, private platform: Platform, private file: File, private appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider, private zip: Zip, private mimeUtils: CoreMimetypeUtilsProvider) {
@ -543,9 +567,10 @@ export class CoreFileProvider {
*
* @param {string} path Relative path to the file.
* @param {any} data Data to write.
* @param {boolean} [append] Whether to append the data to the end of the file.
* @return {Promise<any>} Promise to be resolved when the file is written.
*/
writeFile(path: string, data: any): Promise<any> {
writeFile(path: string, data: any, append?: boolean): Promise<any> {
return this.init().then(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
@ -560,13 +585,67 @@ export class CoreFileProvider {
data = new Blob([data], { type: type || 'text/plain' });
}
return this.file.writeFile(this.basePath, path, data, { replace: true }).then(() => {
return this.file.writeFile(this.basePath, path, data, { replace: !append, append: !!append }).then(() => {
return fileEntry;
});
});
});
}
/**
* Write some file data into a filesystem file.
* It's done in chunks to prevent crashing the app for big files.
*
* @param {any} file The data to write.
* @param {string} path Path where to store the data.
* @param {Function} [onProgress] Function to call on progress.
* @param {number} [offset=0] Offset where to start reading from.
* @param {boolean} [append] Whether to append the data to the end of the file.
* @return {Promise<any>} Promise resolved when done.
*/
writeFileDataInFile(file: any, path: string, onProgress?: (event: CoreFileProgressEvent) => any, offset: number = 0,
append?: boolean): Promise<any> {
offset = offset || 0;
// Get the chunk to read.
const blob = file.slice(offset, Math.min(offset + this.CHUNK_SIZE, file.size));
return this.writeFileDataInFileChunk(blob, path, append).then((fileEntry) => {
offset += this.CHUNK_SIZE;
onProgress && onProgress({
lengthComputable: true,
loaded: offset,
total: file.size
});
if (offset >= file.size) {
// Done, stop.
return fileEntry;
}
// Read the next chunk.
return this.writeFileDataInFile(file, path, onProgress, offset, true);
});
}
/**
* Write a chunk of data into a file.
*
* @param {any} chunkData The chunk of data.
* @param {string} path Path where to store the data.
* @param {boolean} [append] Whether to append the data to the end of the file.
* @return {Promise<any>} Promise resolved when done.
*/
protected writeFileDataInFileChunk(chunkData: any, path: string, append?: boolean): Promise<any> {
// Read the chunk data.
return this.readFileData(chunkData, CoreFileProvider.FORMATARRAYBUFFER).then((fileData) => {
// Write the data in the file.
return this.writeFile(path, fileData, append);
});
}
/**
* Gets a file that might be outside the app's folder.
*