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.more": "data",
"core.fileuploader.photoalbums": "local_moodlemobileapp", "core.fileuploader.photoalbums": "local_moodlemobileapp",
"core.fileuploader.readingfile": "local_moodlemobileapp", "core.fileuploader.readingfile": "local_moodlemobileapp",
"core.fileuploader.readingfileperc": "local_moodlemobileapp",
"core.fileuploader.selectafile": "local_moodlemobileapp", "core.fileuploader.selectafile": "local_moodlemobileapp",
"core.fileuploader.uploadafile": "local_moodlemobileapp", "core.fileuploader.uploadafile": "local_moodlemobileapp",
"core.fileuploader.uploading": "local_moodlemobileapp", "core.fileuploader.uploading": "local_moodlemobileapp",

View File

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

View File

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

View File

@ -13,12 +13,12 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; 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 { MediaFile } from '@ionic-native/media-capture';
import { Camera, CameraOptions } from '@ionic-native/camera'; import { Camera, CameraOptions } from '@ionic-native/camera';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider, CoreFileProgressEvent } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -93,18 +93,15 @@ export class CoreFileUploaderHelperProvider {
name = name || file.name; name = name || file.name;
const modal = this.domUtils.showModalLoading('core.fileuploader.readingfile', true); 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. // Get unique name for the copy.
return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name); return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name).then((newName) => {
}).then((newName) => {
const filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 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) => { }).catch((error) => {
this.logger.error('Error reading file to upload.', error); this.logger.error('Error reading file to upload.', error);
modal.dismiss(); modal.dismiss();
@ -681,16 +678,7 @@ export class CoreFileUploaderHelperProvider {
return this.fileUploaderProvider.uploadFile(path, options, (progress: ProgressEvent) => { return this.fileUploaderProvider.uploadFile(path, options, (progress: ProgressEvent) => {
// Progress uploading. // Progress uploading.
if (progress && progress.lengthComputable) { this.showProgressModal(modal, 'core.fileuploader.uploadingperc', progress);
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();
}
}
}
}, siteId).catch((error) => { }, siteId).catch((error) => {
this.logger.error('Error uploading file.', 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 { CoreTextUtilsProvider } from './utils/text';
import { Zip } from '@ionic-native/zip'; 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. * Factory to interact with the file system.
*/ */
@ -41,6 +64,7 @@ export class CoreFileProvider {
protected initialized = false; protected initialized = false;
protected basePath = ''; protected basePath = '';
protected isHTMLAPI = false; protected isHTMLAPI = false;
protected CHUNK_SIZE = 10485760; // 10 MB.
constructor(logger: CoreLoggerProvider, private platform: Platform, private file: File, private appProvider: CoreAppProvider, constructor(logger: CoreLoggerProvider, private platform: Platform, private file: File, private appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider, private zip: Zip, private mimeUtils: CoreMimetypeUtilsProvider) { 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 {string} path Relative path to the file.
* @param {any} data Data to write. * @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. * @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(() => { return this.init().then(() => {
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
@ -560,13 +585,67 @@ export class CoreFileProvider {
data = new Blob([data], { type: type || 'text/plain' }); 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; 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. * Gets a file that might be outside the app's folder.
* *