MOBILE-1748 core: Read file data in chunks to prevent crashes
This commit is contained in:
		
							parent
							
								
									8248890189
								
							
						
					
					
						commit
						14eb35927c
					
				| @ -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", | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -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.
 |         // Get unique name for the copy.
 | ||||||
|         return this.fileProvider.readFileData(file, CoreFileProvider.FORMATARRAYBUFFER).then((data) => { |         return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name).then((newName) => { | ||||||
|             fileData = data; |  | ||||||
| 
 |  | ||||||
|             // Get unique name for the copy.
 |  | ||||||
|             return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name); |  | ||||||
|         }).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(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -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. | ||||||
|      * |      * | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user