MOBILE-3270 core: Show warning if app crashed when uploading big file

main
Dani Palou 2020-04-08 08:27:43 +02:00
parent ec3a1bdb56
commit 4deaf3be80
8 changed files with 198 additions and 29 deletions

View File

@ -1373,6 +1373,7 @@
"core.confirmgotabrootdefault": "local_moodlemobileapp", "core.confirmgotabrootdefault": "local_moodlemobileapp",
"core.confirmloss": "local_moodlemobileapp", "core.confirmloss": "local_moodlemobileapp",
"core.confirmopeninbrowser": "local_moodlemobileapp", "core.confirmopeninbrowser": "local_moodlemobileapp",
"core.confirmreadfiletoobig": "local_moodlemobileapp",
"core.considereddigitalminor": "moodle", "core.considereddigitalminor": "moodle",
"core.content": "moodle", "core.content": "moodle",
"core.contenteditingsynced": "local_moodlemobileapp", "core.contenteditingsynced": "local_moodlemobileapp",

View File

@ -1373,6 +1373,7 @@
"core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", "core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
"core.confirmloss": "Are you sure? All changes will be lost.", "core.confirmloss": "Are you sure? All changes will be lost.",
"core.confirmopeninbrowser": "Do you want to open it in a web browser?", "core.confirmopeninbrowser": "Do you want to open it in a web browser?",
"core.confirmreadfiletoobig": "It seems the app previously crashed when trying to read a file as big as this one. Are you sure you want to continue?<br><br>If this file is stored in a repository like Google Drive, please try to download the file to your device first and use the downloaded file.",
"core.considereddigitalminor": "You are too young to create an account on this site.", "core.considereddigitalminor": "You are too young to create an account on this site.",
"core.content": "Content", "core.content": "Content",
"core.contenteditingsynced": "The content you are editing has been synced.", "core.contenteditingsynced": "The content you are editing has been synced.",

View File

@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper'; import { CoreFileUploaderHelperProvider } from './helper';
import { CoreFileUploaderProvider } from './fileuploader'; import { CoreFileUploaderProvider } from './fileuploader';
import { TranslateService } from '@ngx-translate/core';
/** /**
* Handler to upload any type of file. * Handler to upload any type of file.
*/ */
@ -28,9 +29,13 @@ export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderFile'; name = 'CoreFileUploaderFile';
priority = 1200; priority = 1200;
constructor(private appProvider: CoreAppProvider, private platform: Platform, private timeUtils: CoreTimeUtilsProvider, constructor(protected appProvider: CoreAppProvider,
private uploaderHelper: CoreFileUploaderHelperProvider, private uploaderProvider: CoreFileUploaderProvider, protected platform: Platform,
private domUtils: CoreDomUtilsProvider) { } protected timeUtils: CoreTimeUtilsProvider,
protected uploaderHelper: CoreFileUploaderHelperProvider,
protected uploaderProvider: CoreFileUploaderProvider,
protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService) { }
/** /**
* Whether or not the handler is enabled on a site level. * Whether or not the handler is enabled on a site level.
@ -107,9 +112,8 @@ export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
this.uploaderHelper.uploadFileObject(file, maxSize, upload, allowOffline, fileName).then((result) => { this.uploaderHelper.uploadFileObject(file, maxSize, upload, allowOffline, fileName).then((result) => {
this.uploaderHelper.fileUploaded(result); this.uploaderHelper.fileUploaded(result);
}).catch((error) => { }).catch((error) => {
if (error) { this.domUtils.showErrorModalDefault(error,
this.domUtils.showErrorModal(error); this.translate.instant('core.fileuploader.errorreadingfile'));
}
}); });
}); });

View File

@ -19,6 +19,7 @@ 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, CoreFileProgressEvent } from '@providers/file'; import { CoreFileProvider, CoreFileProgressEvent } from '@providers/file';
import { CoreFileHelperProvider } from '@providers/file-helper';
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';
@ -36,11 +37,19 @@ export class CoreFileUploaderHelperProvider {
protected filePickerDeferred: PromiseDefer; protected filePickerDeferred: PromiseDefer;
protected actionSheet: ActionSheet; protected actionSheet: ActionSheet;
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private translate: TranslateService, constructor(logger: CoreLoggerProvider,
private fileUploaderProvider: CoreFileUploaderProvider, private domUtils: CoreDomUtilsProvider, protected appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider, protected translate: TranslateService,
private actionSheetCtrl: ActionSheetController, private uploaderDelegate: CoreFileUploaderDelegate, protected fileUploaderProvider: CoreFileUploaderProvider,
private camera: Camera, private platform: Platform) { protected domUtils: CoreDomUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected fileProvider: CoreFileProvider,
protected utils: CoreUtilsProvider,
protected actionSheetCtrl: ActionSheetController,
protected uploaderDelegate: CoreFileUploaderDelegate,
protected camera: Camera,
protected platform: Platform,
protected fileHelper: CoreFileHelperProvider) {
this.logger = logger.getInstance('CoreFileUploaderProvider'); this.logger = logger.getInstance('CoreFileUploaderProvider');
} }
@ -99,14 +108,14 @@ export class CoreFileUploaderHelperProvider {
const filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName); const filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
// Write the data into the file. // Write the data into the file.
return this.fileProvider.writeFileDataInFile(file, filePath, (progress: CoreFileProgressEvent) => { return this.fileHelper.writeFileDataInFile(file, filePath, (progress: CoreFileProgressEvent) => {
this.showProgressModal(modal, 'core.fileuploader.readingfileperc', progress); 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();
return Promise.reject(this.translate.instant('core.fileuploader.errorreadingfile')); return Promise.reject(error);
}).then((fileEntry) => { }).then((fileEntry) => {
modal.dismiss(); modal.dismiss();
@ -319,9 +328,7 @@ export class CoreFileUploaderHelperProvider {
// Success uploading or picking, return the result. // Success uploading or picking, return the result.
this.fileUploaded(result); this.fileUploaded(result);
}).catch((error) => { }).catch((error) => {
if (error) { this.domUtils.showErrorModalDefault(error, this.translate.instant('core.fileuploader.errorreadingfile'));
this.domUtils.showErrorModal(error);
}
}); });
// Do not close the action sheet, it will be closed if success. // Do not close the action sheet, it will be closed if success.

View File

@ -43,6 +43,7 @@
"confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", "confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
"confirmloss": "Are you sure? All changes will be lost.", "confirmloss": "Are you sure? All changes will be lost.",
"confirmopeninbrowser": "Do you want to open it in a web browser?", "confirmopeninbrowser": "Do you want to open it in a web browser?",
"confirmreadfiletoobig": "It seems the app previously crashed when trying to read a file as big as this one. Are you sure you want to continue?<br><br>If this file is stored in a repository like Google Drive, please try to download the file to your device first and use the downloaded file.",
"considereddigitalminor": "You are too young to create an account on this site.", "considereddigitalminor": "You are too young to create an account on this site.",
"content": "Content", "content": "Content",
"contenteditingsynced": "The content you are editing has been synced.", "contenteditingsynced": "The content you are editing has been synced.",

View File

@ -15,12 +15,15 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from './app'; import { CoreAppProvider } from './app';
import { CoreFileProvider } from './file'; import { CoreConfigProvider } from './config';
import { CoreFileProvider, CoreFileProgressFunction } from './file';
import { CoreFilepoolProvider } from './filepool'; import { CoreFilepoolProvider } from './filepool';
import { CoreSitesProvider } from './sites'; import { CoreSitesProvider } from './sites';
import { CoreWSProvider } from './ws'; import { CoreWSProvider } from './ws';
import { CoreDomUtilsProvider } from './utils/dom';
import { CoreUtilsProvider } from './utils/utils'; import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { FileEntry } from '@ionic-native/file';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
/** /**
@ -29,9 +32,23 @@ import { makeSingleton } from '@singletons/core.singletons';
@Injectable() @Injectable()
export class CoreFileHelperProvider { export class CoreFileHelperProvider {
constructor(private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, // Variables for reading files in chunks.
private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private translate: TranslateService, protected MAX_CHUNK_SIZE_NAME = 'file_max_chunk_size';
private utils: CoreUtilsProvider, private wsProvider: CoreWSProvider) { } protected READ_CHUNK_ATTEMPT_NAME = 'file_read_chunk_attempt';
protected maxChunkSize = -1;
constructor(protected fileProvider: CoreFileProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected sitesProvider: CoreSitesProvider,
protected appProvider: CoreAppProvider,
protected translate: TranslateService,
protected utils: CoreUtilsProvider,
protected wsProvider: CoreWSProvider,
protected configProvider: CoreConfigProvider,
protected domUtils: CoreDomUtilsProvider) {
this.initMaxChunkSize();
}
/** /**
* Convenience function to open a file, downloading it if needed. * Convenience function to open a file, downloading it if needed.
@ -240,6 +257,34 @@ export class CoreFileHelperProvider {
return file.timemodified || 0; return file.timemodified || 0;
} }
/**
* Initialize the max chunk size.
*
* @return Promise resolved when done.
*/
protected async initMaxChunkSize(): Promise<void> {
const sizes = await Promise.all([
await this.configProvider.get(this.READ_CHUNK_ATTEMPT_NAME, -1), // Check if there is any attempt pending.
await this.configProvider.get(this.MAX_CHUNK_SIZE_NAME, -1), // Retrieve current max chunk size from DB.
]);
const attemptSize = sizes[0];
const maxChunkSize = sizes[1];
if (attemptSize != -1 && (maxChunkSize == -1 || attemptSize < maxChunkSize)) {
// Store the attempt's size as the max size.
this.storeMaxChunkSize(attemptSize);
} else {
// No attempt or the max size is already lower. Keep current max size.
this.maxChunkSize = maxChunkSize;
}
if (attemptSize != -1) {
// Clean pending attempt.
await this.configProvider.delete(this.READ_CHUNK_ATTEMPT_NAME);
}
}
/** /**
* Check if a state is downloaded or outdated. * Check if a state is downloaded or outdated.
* *
@ -334,6 +379,99 @@ export class CoreFileHelperProvider {
throw new Error('Couldn\'t determine file size: ' + file.fileurl); throw new Error('Couldn\'t determine file size: ' + file.fileurl);
} }
/**
* Save max chunk size.
*
* @param size Size to store.
* @return Promise resolved when done.
*/
protected async storeMaxChunkSize(size: number): Promise<void> {
this.maxChunkSize = size;
await this.configProvider.set(this.MAX_CHUNK_SIZE_NAME, size);
}
/**
* Write some file data into a filesystem file.
* It's done in chunks to prevent crashing the app for big files.
*
* @param file The data to write.
* @param path Path where to store the data.
* @param onProgress Function to call on progress.
* @param offset Offset where to start reading from.
* @param append Whether to append the data to the end of the file.
* @return Promise resolved when done.
*/
async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0,
append?: boolean): Promise<FileEntry> {
offset = offset || 0;
// Get the chunk to read and write.
const readWholeFile = offset === 0 && CoreFileProvider.CHUNK_SIZE >= file.size;
const chunk = readWholeFile ? file : file.slice(offset, Math.min(offset + CoreFileProvider.CHUNK_SIZE, file.size));
try {
const fileEntry = await this.fileProvider.writeFileDataInFileChunk(chunk, path, append);
offset += CoreFileProvider.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);
} catch (error) {
if (readWholeFile || !error || error.name != 'NotReadableError') {
return Promise.reject(error);
}
// Permission error when reading file in chunks. This usually happens with Google Drive files.
// Try to read the whole file at once.
return this.writeBigFileDataInFile(file, path, onProgress);
}
}
/**
* Writes a big file data into a filesystem file without using chunks.
* The app can crash when doing this with big files, so this function will try to control the max size that works
* and warn the user if he's trying to upload a file that is too big.
*
* @param file The data to write.
* @param path Path where to store the data.
* @param onProgress Function to call on progress.
* @return Promise resolved with the file.
*/
protected async writeBigFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction): Promise<FileEntry> {
if (this.maxChunkSize != -1 && file.size >= this.maxChunkSize) {
// The file size is bigger than the max allowed size. Ask the user to confirm.
await this.domUtils.showConfirm(this.translate.instant('core.confirmreadfiletoobig'));
}
// Store the "attempt".
await this.configProvider.set(this.READ_CHUNK_ATTEMPT_NAME, file.size);
// Write the whole file.
const fileEntry = await this.fileProvider.writeFileDataInFileChunk(file, path, false);
// Success, remove the attempt and increase the max chunk size if needed.
await this.configProvider.delete(this.READ_CHUNK_ATTEMPT_NAME);
if (file.size > this.maxChunkSize) {
await this.storeMaxChunkSize(file.size + 1);
}
return fileEntry;
}
} }
export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {} export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {}

View File

@ -44,6 +44,11 @@ export interface CoreFileProgressEvent {
total?: number; total?: number;
} }
/**
* Progress function.
*/
export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void;
/** /**
* Factory to interact with the file system. * Factory to interact with the file system.
*/ */
@ -60,14 +65,21 @@ export class CoreFileProvider {
static SITESFOLDER = 'sites'; static SITESFOLDER = 'sites';
static TMPFOLDER = 'tmp'; static TMPFOLDER = 'tmp';
static CHUNK_SIZE = 10485760; // 10 MB.
protected logger; protected logger;
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 textUtils: CoreTextUtilsProvider, private zip: Zip, private mimeUtils: CoreMimetypeUtilsProvider) { protected platform: Platform,
protected file: File,
protected appProvider: CoreAppProvider,
protected textUtils: CoreTextUtilsProvider,
protected zip: Zip,
protected mimeUtils: CoreMimetypeUtilsProvider) {
this.logger = logger.getInstance('CoreFileProvider'); this.logger = logger.getInstance('CoreFileProvider');
if (platform.is('android') && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { if (platform.is('android') && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) {
@ -634,20 +646,21 @@ export class CoreFileProvider {
* @param offset Offset where to start reading from. * @param offset Offset where to start reading from.
* @param append Whether to append the data to the end of the file. * @param append Whether to append the data to the end of the file.
* @return Promise resolved when done. * @return Promise resolved when done.
* @deprecated since 3.8.3. Please use CoreFileHelperProvider.writeFileDataInFile instead.
*/ */
async writeFileDataInFile(file: Blob, path: string, onProgress?: (event: CoreFileProgressEvent) => void, offset: number = 0, async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0,
append?: boolean): Promise<FileEntry> { append?: boolean): Promise<FileEntry> {
offset = offset || 0; offset = offset || 0;
// Get the chunk to read. // Get the chunk to read and write.
const readWholeFile = offset === 0 && this.CHUNK_SIZE >= file.size; const readWholeFile = offset === 0 && CoreFileProvider.CHUNK_SIZE >= file.size;
const chunk = readWholeFile ? file : file.slice(offset, Math.min(offset + this.CHUNK_SIZE, file.size)); const chunk = readWholeFile ? file : file.slice(offset, Math.min(offset + CoreFileProvider.CHUNK_SIZE, file.size));
try { try {
const fileEntry = await this.writeFileDataInFileChunk(chunk, path, append); const fileEntry = await this.writeFileDataInFileChunk(chunk, path, append);
offset += this.CHUNK_SIZE; offset += CoreFileProvider.CHUNK_SIZE;
onProgress && onProgress({ onProgress && onProgress({
lengthComputable: true, lengthComputable: true,
@ -681,7 +694,7 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file. * @param append Whether to append the data to the end of the file.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected writeFileDataInFileChunk(chunkData: any, path: string, append?: boolean): Promise<FileEntry> { writeFileDataInFileChunk(chunkData: Blob, path: string, append?: boolean): Promise<FileEntry> {
// Read the chunk data. // Read the chunk data.
return this.readFileData(chunkData, CoreFileProvider.FORMATARRAYBUFFER).then((fileData) => { return this.readFileData(chunkData, CoreFileProvider.FORMATARRAYBUFFER).then((fileData) => {
// Write the data in the file. // Write the data in the file.

View File

@ -1,6 +1,10 @@
This files describes API changes in the Moodle Mobile app, This files describes API changes in the Moodle Mobile app,
information provided here is intended especially for developers. information provided here is intended especially for developers.
=== 3.8.3 ===
- CoreFileProvider.writeFileDataInFile has been deprecated. Please use CoreFileHelperProvider.writeFileDataInFile instead.
=== 3.8.0 === === 3.8.0 ===
- CoreDomUtilsProvider.extractDownloadableFilesFromHtml and CoreDomUtilsProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects have been deprecated. Please use CoreFilepoolProvider.extractDownloadableFilesFromHtml and CoreFilepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects. We had to move them to prevent a circular dependency. - CoreDomUtilsProvider.extractDownloadableFilesFromHtml and CoreDomUtilsProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects have been deprecated. Please use CoreFilepoolProvider.extractDownloadableFilesFromHtml and CoreFilepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects. We had to move them to prevent a circular dependency.