diff --git a/scripts/langindex.json b/scripts/langindex.json
index 4810e4705..c275a91ed 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1373,6 +1373,7 @@
"core.confirmgotabrootdefault": "local_moodlemobileapp",
"core.confirmloss": "local_moodlemobileapp",
"core.confirmopeninbrowser": "local_moodlemobileapp",
+ "core.confirmreadfiletoobig": "local_moodlemobileapp",
"core.considereddigitalminor": "moodle",
"core.content": "moodle",
"core.contenteditingsynced": "local_moodlemobileapp",
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 080e14b1c..7966066ec 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -1373,6 +1373,7 @@
"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.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?
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.content": "Content",
"core.contenteditingsynced": "The content you are editing has been synced.",
diff --git a/src/core/fileuploader/providers/file-handler.ts b/src/core/fileuploader/providers/file-handler.ts
index 9d4c92462..c89eaa03d 100644
--- a/src/core/fileuploader/providers/file-handler.ts
+++ b/src/core/fileuploader/providers/file-handler.ts
@@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate';
import { CoreFileUploaderHelperProvider } from './helper';
import { CoreFileUploaderProvider } from './fileuploader';
+import { TranslateService } from '@ngx-translate/core';
/**
* Handler to upload any type of file.
*/
@@ -28,9 +29,13 @@ export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
name = 'CoreFileUploaderFile';
priority = 1200;
- constructor(private appProvider: CoreAppProvider, private platform: Platform, private timeUtils: CoreTimeUtilsProvider,
- private uploaderHelper: CoreFileUploaderHelperProvider, private uploaderProvider: CoreFileUploaderProvider,
- private domUtils: CoreDomUtilsProvider) { }
+ constructor(protected appProvider: CoreAppProvider,
+ protected platform: Platform,
+ 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.
@@ -107,9 +112,8 @@ export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler {
this.uploaderHelper.uploadFileObject(file, maxSize, upload, allowOffline, fileName).then((result) => {
this.uploaderHelper.fileUploaded(result);
}).catch((error) => {
- if (error) {
- this.domUtils.showErrorModal(error);
- }
+ this.domUtils.showErrorModalDefault(error,
+ this.translate.instant('core.fileuploader.errorreadingfile'));
});
});
diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts
index afaa6f909..903a5fda6 100644
--- a/src/core/fileuploader/providers/helper.ts
+++ b/src/core/fileuploader/providers/helper.ts
@@ -19,6 +19,7 @@ import { Camera, CameraOptions } from '@ionic-native/camera';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreFileProvider, CoreFileProgressEvent } from '@providers/file';
+import { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@@ -36,11 +37,19 @@ export class CoreFileUploaderHelperProvider {
protected filePickerDeferred: PromiseDefer;
protected actionSheet: ActionSheet;
- constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private translate: TranslateService,
- private fileUploaderProvider: CoreFileUploaderProvider, private domUtils: CoreDomUtilsProvider,
- private textUtils: CoreTextUtilsProvider, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider,
- private actionSheetCtrl: ActionSheetController, private uploaderDelegate: CoreFileUploaderDelegate,
- private camera: Camera, private platform: Platform) {
+ constructor(logger: CoreLoggerProvider,
+ protected appProvider: CoreAppProvider,
+ protected translate: TranslateService,
+ protected fileUploaderProvider: CoreFileUploaderProvider,
+ 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');
}
@@ -99,14 +108,14 @@ export class CoreFileUploaderHelperProvider {
const filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
// 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);
});
}).catch((error) => {
this.logger.error('Error reading file to upload.', error);
modal.dismiss();
- return Promise.reject(this.translate.instant('core.fileuploader.errorreadingfile'));
+ return Promise.reject(error);
}).then((fileEntry) => {
modal.dismiss();
@@ -319,9 +328,7 @@ export class CoreFileUploaderHelperProvider {
// Success uploading or picking, return the result.
this.fileUploaded(result);
}).catch((error) => {
- if (error) {
- this.domUtils.showErrorModal(error);
- }
+ this.domUtils.showErrorModalDefault(error, this.translate.instant('core.fileuploader.errorreadingfile'));
});
// Do not close the action sheet, it will be closed if success.
diff --git a/src/lang/en.json b/src/lang/en.json
index 3b9ccff0f..60d16355c 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -43,6 +43,7 @@
"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.",
"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?
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.",
"content": "Content",
"contenteditingsynced": "The content you are editing has been synced.",
diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts
index 00b4b4150..31a2e6e70 100644
--- a/src/providers/file-helper.ts
+++ b/src/providers/file-helper.ts
@@ -15,12 +15,15 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from './app';
-import { CoreFileProvider } from './file';
+import { CoreConfigProvider } from './config';
+import { CoreFileProvider, CoreFileProgressFunction } from './file';
import { CoreFilepoolProvider } from './filepool';
import { CoreSitesProvider } from './sites';
import { CoreWSProvider } from './ws';
+import { CoreDomUtilsProvider } from './utils/dom';
import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '@core/constants';
+import { FileEntry } from '@ionic-native/file';
import { makeSingleton } from '@singletons/core.singletons';
/**
@@ -29,9 +32,23 @@ import { makeSingleton } from '@singletons/core.singletons';
@Injectable()
export class CoreFileHelperProvider {
- constructor(private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider,
- private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private translate: TranslateService,
- private utils: CoreUtilsProvider, private wsProvider: CoreWSProvider) { }
+ // Variables for reading files in chunks.
+ protected MAX_CHUNK_SIZE_NAME = 'file_max_chunk_size';
+ 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.
@@ -240,6 +257,34 @@ export class CoreFileHelperProvider {
return file.timemodified || 0;
}
+ /**
+ * Initialize the max chunk size.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async initMaxChunkSize(): Promise {
+ 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.
*
@@ -334,6 +379,99 @@ export class CoreFileHelperProvider {
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 {
+ 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 {
+
+ 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 {
+ 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) {}
diff --git a/src/providers/file.ts b/src/providers/file.ts
index 658560c42..5bc9140e8 100644
--- a/src/providers/file.ts
+++ b/src/providers/file.ts
@@ -44,6 +44,11 @@ export interface CoreFileProgressEvent {
total?: number;
}
+/**
+ * Progress function.
+ */
+export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void;
+
/**
* Factory to interact with the file system.
*/
@@ -60,14 +65,21 @@ export class CoreFileProvider {
static SITESFOLDER = 'sites';
static TMPFOLDER = 'tmp';
+ static CHUNK_SIZE = 10485760; // 10 MB.
+
protected logger;
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) {
+ constructor(logger: CoreLoggerProvider,
+ protected platform: Platform,
+ protected file: File,
+ protected appProvider: CoreAppProvider,
+ protected textUtils: CoreTextUtilsProvider,
+ protected zip: Zip,
+ protected mimeUtils: CoreMimetypeUtilsProvider) {
+
this.logger = logger.getInstance('CoreFileProvider');
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 append Whether to append the data to the end of the file.
* @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 {
offset = offset || 0;
- // Get the chunk to read.
- const readWholeFile = offset === 0 && this.CHUNK_SIZE >= file.size;
- const chunk = readWholeFile ? file : file.slice(offset, Math.min(offset + this.CHUNK_SIZE, file.size));
+ // 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.writeFileDataInFileChunk(chunk, path, append);
- offset += this.CHUNK_SIZE;
+ offset += CoreFileProvider.CHUNK_SIZE;
onProgress && onProgress({
lengthComputable: true,
@@ -681,7 +694,7 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file.
* @return Promise resolved when done.
*/
- protected writeFileDataInFileChunk(chunkData: any, path: string, append?: boolean): Promise {
+ writeFileDataInFileChunk(chunkData: Blob, path: string, append?: boolean): Promise {
// Read the chunk data.
return this.readFileData(chunkData, CoreFileProvider.FORMATARRAYBUFFER).then((fileData) => {
// Write the data in the file.
diff --git a/upgrade.txt b/upgrade.txt
index 75333c0c4..f66263f6a 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -1,6 +1,10 @@
This files describes API changes in the Moodle Mobile app,
information provided here is intended especially for developers.
+=== 3.8.3 ===
+
+- CoreFileProvider.writeFileDataInFile has been deprecated. Please use CoreFileHelperProvider.writeFileDataInFile instead.
+
=== 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.