MOBILE-1748 core: Read file data in chunks to prevent crashes
parent
8248890189
commit
14eb35927c
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
// Get unique name for the copy.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue