From 4492b43061c2621dc539f396681524e3e8f8668a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Jan 2018 08:52:27 +0100 Subject: [PATCH] MOBILE-2312 uploader: Implement file uploader helper --- src/app/app.module.ts | 2 + src/core/fileuploader/fileuploader.module.ts | 51 ++ src/core/fileuploader/providers/helper.ts | 687 +++++++++++++++++++ 3 files changed, 740 insertions(+) create mode 100644 src/core/fileuploader/fileuploader.module.ts create mode 100644 src/core/fileuploader/providers/helper.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 979d5f135..bf5da58d2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,6 +53,7 @@ import { CoreEmulatorModule } from '../core/emulator/emulator.module'; import { CoreLoginModule } from '../core/login/login.module'; import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module'; import { CoreCoursesModule } from '../core/courses/courses.module'; +import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module'; // For translate loader. AoT requires an exported function for factories. @@ -82,6 +83,7 @@ export function createTranslateLoader(http: HttpClient) { CoreLoginModule, CoreMainMenuModule, CoreCoursesModule, + CoreFileUploaderModule, CoreComponentsModule ], bootstrap: [IonicApp], diff --git a/src/core/fileuploader/fileuploader.module.ts b/src/core/fileuploader/fileuploader.module.ts new file mode 100644 index 000000000..5d52372bb --- /dev/null +++ b/src/core/fileuploader/fileuploader.module.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CoreFileUploaderProvider } from './providers/fileuploader'; +import { CoreFileUploaderHelperProvider } from './providers/helper'; +import { CoreFileUploaderDelegate } from './providers/delegate'; +import { CoreFileUploaderAlbumHandler } from './providers/album-handler'; +import { CoreFileUploaderAudioHandler } from './providers/audio-handler'; +import { CoreFileUploaderCameraHandler } from './providers/camera-handler'; +import { CoreFileUploaderFileHandler } from './providers/file-handler'; +import { CoreFileUploaderVideoHandler } from './providers/video-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreFileUploaderProvider, + CoreFileUploaderHelperProvider, + CoreFileUploaderDelegate, + CoreFileUploaderAlbumHandler, + CoreFileUploaderAudioHandler, + CoreFileUploaderCameraHandler, + CoreFileUploaderFileHandler, + CoreFileUploaderVideoHandler + ] +}) +export class CoreFileUploaderModule { + constructor(delegate: CoreFileUploaderDelegate, albumHandler: CoreFileUploaderAlbumHandler, + audioHandler: CoreFileUploaderAudioHandler, cameraHandler: CoreFileUploaderCameraHandler, + videoHandler: CoreFileUploaderVideoHandler, fileHandler: CoreFileUploaderFileHandler) { + delegate.registerHandler(albumHandler); + delegate.registerHandler(audioHandler); + delegate.registerHandler(cameraHandler); + delegate.registerHandler(fileHandler); + delegate.registerHandler(videoHandler); + } +} diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts new file mode 100644 index 000000000..cfd38e40d --- /dev/null +++ b/src/core/fileuploader/providers/helper.ts @@ -0,0 +1,687 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { ActionSheetController, ActionSheet, Platform } from 'ionic-angular'; +import { MediaCapture, 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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../providers/utils/text'; +import { CoreUtilsProvider, PromiseDefer } from '../../../providers/utils/utils'; +import { CoreFileUploaderProvider, CoreFileUploaderOptions } from './fileuploader'; +import { CoreFileUploaderDelegate } from './delegate'; + +/** + * Helper service to upload files. + */ +@Injectable() +export class CoreFileUploaderHelperProvider { + + protected logger; + 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 mediaCapture: MediaCapture, private camera: Camera, private platform: Platform) { + this.logger = logger.getInstance('CoreFileUploaderProvider'); + } + + /** + * Show a confirmation modal to the user if the size of the file is bigger than the allowed threshold. + * + * @param {number} size File size. + * @param {boolean} [alwaysConfirm] True to show a confirm even if the size isn't high. + * @param {boolean} [allowOffline] True to allow uploading in offline. + * @param {number} [wifiThreshold] Threshold for WiFi connection. Default: CoreFileUploaderProvider.WIFI_SIZE_WARNING. + * @param {number} [limitedThreshold] Threshold for limited connection. Default: CoreFileUploaderProvider.LIMITED_SIZE_WARNING. + * @return {Promise} Promise resolved when the user confirms or if there's no need to show a modal. + */ + confirmUploadFile(size: number, alwaysConfirm?: boolean, allowOffline?: boolean, wifiThreshold?: number, + limitedThreshold?: number) : Promise { + if (size == 0) { + return Promise.resolve(); + } + + if (!allowOffline && !this.appProvider.isOnline()) { + return Promise.reject(this.translate.instant('core.fileuploader.errormustbeonlinetoupload')); + } + + wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreFileUploaderProvider.WIFI_SIZE_WARNING : wifiThreshold; + limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreFileUploaderProvider.LIMITED_SIZE_WARNING : limitedThreshold; + + if (size < 0) { + return this.domUtils.showConfirm(this.translate.instant('core.fileuploader.confirmuploadunknownsize')); + } else if (size >= wifiThreshold || (this.appProvider.isNetworkAccessLimited() && size >= limitedThreshold)) { + let readableSize = this.textUtils.bytesToSize(size, 2); + return this.domUtils.showConfirm(this.translate.instant('core.fileuploader.confirmuploadfile', {size: readableSize})); + } else if (alwaysConfirm) { + return this.domUtils.showConfirm(this.translate.instant('core.areyousure')); + } else { + return Promise.resolve(); + } + } + + /** + * Create a temporary copy of a file and upload it. + * + * @param {any} file File to copy and upload. + * @param {boolean} [upload] True if the file should be uploaded, false to return the copy of the file. + * @param {string} [name] Name to use when uploading the file. If not defined, use the file's name. + * @return {Promise} Promise resolved when the file is uploaded. + */ + copyAndUploadFile(file: any, upload?: boolean, name?: string) : Promise { + name = name || file.name; + + let modal = this.domUtils.showModalLoading('core.fileuploader.readingfile', true), + 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, this.fileProvider.FORMATARRAYBUFFER).then((data) => { + fileData = data; + + // Get unique name for the copy. + return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, name); + }).then((newName) => { + let filePath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName); + + return this.fileProvider.writeFile(filePath, fileData); + }).catch((error) => { + this.logger.error('Error reading file to upload.', error); + modal.dismiss(); + return Promise.reject(this.translate.instant('core.fileuploader.errorreadingfile')); + }).then((fileEntry) => { + modal.dismiss(); + + if (upload) { + // Pass true to delete the copy after the upload. + return this.uploadGenericFile(fileEntry.toURL(), name, file.type, true); + } else { + return fileEntry; + } + }); + } + + /** + * Copy or move a file to the app temporary folder. + * + * @param {string} path Path of the file. + * @param {boolean} shouldDelete True if original file should be deleted (move), false otherwise (copy). + * @param {number} [maxSize] Max size of the file. If not defined or -1, no max size. + * @param {string} [defaultExt] Defaut extension to use if the file doesn't have any. + * @return {Promise} Promise resolved with the copied file. + */ + protected copyToTmpFolder(path: string, shouldDelete: boolean, maxSize?: number, defaultExt?: string) : Promise { + let fileName = this.fileProvider.getFileAndDirectoryFromPath(path).name, + promise, + fileTooLarge; + + // Check that size isn't too large. + if (typeof maxSize != 'undefined' && maxSize != -1) { + promise = this.fileProvider.getExternalFile(path).then((fileEntry) => { + return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => { + if (file.size > maxSize) { + fileTooLarge = file; + } + }); + }).catch(() => { + // Ignore failures. + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + if (fileTooLarge) { + return this.errorMaxBytes(maxSize, fileTooLarge.name); + } + + // File isn't too large. + // Picking an image from album in Android adds a timestamp at the end of the file. Delete it. + fileName = fileName.replace(/(\.[^\.]*)\?[^\.]*$/, '$1'); + + // Get a unique name in the folder to prevent overriding another file. + return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, fileName, defaultExt); + }).then((newName) => { + // Now move or copy the file. + const destPath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName); + if (shouldDelete) { + return this.fileProvider.moveExternalFile(path, destPath); + } else { + return this.fileProvider.copyExternalFile(path, destPath); + } + }); + } + + /** + * Function called when trying to upload a file bigger than max size. Shows an error. + * + * @param {number} maxSize Max size (bytes). + * @param {string} fileName Name of the file. + * @return {Promise} Rejected promise. + */ + protected errorMaxBytes(maxSize: number, fileName: string) : Promise { + let errorMessage = this.translate.instant('core.fileuploader.maxbytesfile', {$a: { + file: fileName, + size: this.textUtils.bytesToSize(maxSize, 2) + }}); + + this.domUtils.showErrorModal(errorMessage); + return Promise.reject(null); + } + + /** + * Function called when the file picker is closed. + */ + filePickerClosed() : void { + if (this.filePickerDeferred) { + this.filePickerDeferred.reject(); + this.filePickerDeferred = undefined; + } + // Close the action sheet if it's opened. + if (this.actionSheet) { + this.actionSheet.dismiss(); + } + } + + /** + * Function to call once a file is uploaded using the file picker. + * + * @param {any} result Result of the upload process. + */ + fileUploaded(result: any) : void { + if (this.filePickerDeferred) { + this.filePickerDeferred.resolve(result); + this.filePickerDeferred = undefined; + } + // Close the action sheet if it's opened. + if (this.actionSheet) { + this.actionSheet.dismiss(); + } + } + + /** + * Open the "file picker" to select and upload a file. + * + * @param {number} [maxSize] Max size of the file to upload. If not defined or -1, no max size. + * @param {string} [title] File picker title. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @return {Promise} Promise resolved when a file is uploaded, rejected if file picker is closed without a file uploaded. + * The resolve value is the response of the upload request. + */ + selectAndUploadFile(maxSize?: number, title?: string, mimetypes?: string[]) : Promise { + return this.selectFileWithPicker(maxSize, false, title, mimetypes, true); + } + + /** + * Open the "file picker" to select a file without uploading it. + * + * @param {number} [maxSize] Max size of the file. If not defined or -1, no max size. + * @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection. + * @param {string} [title] File picker title. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @return {Promise} Promise resolved when a file is selected, rejected if file picker is closed without selecting a file. + * The resolve value is the FileEntry of a copy of the picked file, so it can be deleted afterwards. + */ + selectFile(maxSize?: number, allowOffline?: boolean, title?: string, mimetypes?: string[]) + : Promise { + return this.selectFileWithPicker(maxSize, allowOffline, title, mimetypes, false); + } + + /** + * Open the "file picker" to select a file and maybe uploading it. + * + * @param {number} [maxSize] Max size of the file. If not defined or -1, no max size. + * @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection. + * @param {string} [title] File picker title. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @param {boolean} [upload] Whether the file should be uploaded. + * @return {Promise} Promise resolved when a file is selected/uploaded, rejected if file picker is closed. + */ + protected selectFileWithPicker(maxSize?: number, allowOffline?: boolean, title?: string, mimetypes?: string[], + upload?: boolean) : Promise { + // Create the cancel button and get the handlers to upload the file. + let buttons: any[] = [{ + text: this.translate.instant('core.cancel'), + role: 'cancel', + handler: () => { + // User cancelled the action sheet. + this.filePickerClosed(); + } + }], + handlers = this.uploaderDelegate.getHandlers(mimetypes); + + this.filePickerDeferred = this.utils.promiseDefer(); + + // Sort the handlers by priority. + handlers.sort((a, b) => { + return a.priority <= b.priority ? 1 : -1; + }); + + // Create a button for each handler. + handlers.forEach((handler) => { + buttons.push({ + text: this.translate.instant(handler.title), + icon: handler.icon, + cssClass: handler.class, + handler: () => { + if (!handler.action) { + // Nothing to do. + return false; + } + + if (!allowOffline && !this.appProvider.isOnline()) { + // Not allowed, show error. + this.domUtils.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true); + return false; + } + + handler.action(maxSize, upload, allowOffline, handler.mimetypes).then((data) => { + if (data.treated) { + // The handler already treated the file. Return the result. + return data.result; + } else { + // The handler didn't treat the file, we need to do it. + if (data.fileEntry) { + // The handler provided us a fileEntry, use it. + return this.uploadFileEntry(data.fileEntry, data.delete, maxSize, upload, allowOffline); + } else if (data.path) { + // The handler provided a path. First treat it like it's a relative path. + return this.fileProvider.getFile(data.path).catch(() => { + // File not found, it's probably an absolute path. + return this.fileProvider.getExternalFile(data.path); + }).then((fileEntry) => { + // File found, treat it. + return this.uploadFileEntry(fileEntry, data.delete, maxSize, upload, allowOffline); + }); + } + + // Nothing received, fail. + return Promise.reject('No file received'); + } + }).then((result) => { + // Success uploading or picking, return the result. + this.fileUploaded(result); + }).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } + }); + + // Do not close the action sheet, it will be closed if success. + return false; + } + }); + }); + + this.actionSheet = this.actionSheetCtrl.create({ + title: title ? title : this.translate.instant('core.fileuploader.' + (upload ? 'uploadafile' : 'selectafile')), + buttons: buttons + }); + this.actionSheet.present(); + + // Call afterRender for each button. + setTimeout(() => { + handlers.forEach((handler) => { + if (handler.afterRender) { + handler.afterRender(maxSize, upload, allowOffline, handler.mimetypes); + } + }); + }, 500); + + return this.filePickerDeferred.promise; + } + + /** + * Convenience function to upload a file on a certain site, showing a confirm if needed. + * + * @param {any} fileEntry FileEntry of the file to upload. + * @param {boolean} [deleteAfterUpload] Whether the file should be deleted after upload. + * @param {string} [siteId] Id of the site to upload the file to. If not defined, use current site. + * @return {Promise} Promise resolved when the file is uploaded. + */ + showConfirmAndUploadInSite(fileEntry: any, deleteAfterUpload?: boolean, siteId?: string) : Promise { + return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => { + return this.confirmUploadFile(file.size).then(() => { + return this.uploadGenericFile(fileEntry.toURL(), file.name, file.type, deleteAfterUpload, siteId).then(() => { + this.domUtils.showAlert('core.success', 'core.fileuploader.fileuploaded'); + }); + }).catch((err) => { + if (err) { + this.domUtils.showErrorModal(err); + } + return Promise.reject(null); + }); + }, () => { + this.domUtils.showErrorModal('core.fileuploader.errorreadingfile', true); + return Promise.reject(null); + }); + } + + /** + * Treat a capture audio/video error. + * + * @param {any} error Error returned by the Cordova plugin. Can be a string or an object. + * @param {string} defaultMessage Key of the default message to show. + * @return {Promise} Rejected promise. If it doesn't have an error message it means it was cancelled. + */ + protected treatCaptureError(error: any, defaultMessage: string) : Promise { + // Cancelled or error. If cancelled, error is an object with code = 3. + if (error) { + if (typeof error === 'string') { + this.logger.error('Error while recording audio/video: ' + error); + if (error.indexOf('No Activity found') > -1) { + // User doesn't have an app to do this. + return Promise.reject(this.translate.instant('core.fileuploader.errornoapp')); + } else { + return Promise.reject(this.translate.instant(defaultMessage)); + } + } else { + if (error.code != 3) { + // Error, not cancelled. + this.logger.error('Error while recording audio/video', error); + return Promise.reject(this.translate.instant(defaultMessage)); + } else { + this.logger.debug('Cancelled'); + } + } + } + return Promise.reject(null); + } + + /** + * Treat a capture image or browse album error. + * + * @param {string} error Error returned by the Cordova plugin. + * @param {string} defaultMessage Key of the default message to show. + * @return {Promise} Rejected promise. If it doesn't have an error message it means it was cancelled. + */ + protected treatImageError(error: string, defaultMessage: string) : Promise { + // Cancelled or error. + if (error) { + if (typeof error == 'string') { + if (error.toLowerCase().indexOf('error') > -1 || error.toLowerCase().indexOf('unable') > -1) { + this.logger.error('Error getting image: ' + error); + return Promise.reject(error); + } else { + // User cancelled. + this.logger.debug('Cancelled'); + } + } else { + return Promise.reject(this.translate.instant(defaultMessage)); + } + } + return Promise.reject(null); + } + + /** + * Convenient helper for the user to record and upload a video. + * + * @param {boolean} isAudio True if uploading an audio, false if it's a video. + * @param {number} maxSize Max size of the upload. -1 for no max size. + * @param {boolean} [upload] True if the file should be uploaded, false to return the picked file. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @return {Promise} Promise resolved when done. + */ + uploadAudioOrVideo(isAudio: boolean, maxSize: number, upload?: boolean, mimetypes?: string[]) : Promise { + this.logger.debug('Trying to record a video file'); + + const options = {limit: 1, mimetypes: mimetypes}, + promise = isAudio ? this.mediaCapture.captureAudio(options) : this.mediaCapture.captureVideo(options); + + // The mimetypes param is only for desktop apps, the Cordova plugin doesn't support it. + return promise.then((medias) => { + // We used limit 1, we only want 1 media. + let media: MediaFile = medias[0], + path = media.fullPath, + error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported. + + if (error) { + return Promise.reject(error); + } + + if (upload) { + return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getMediaUploadOptions(media)); + } else { + // Copy or move the file to our temporary folder. + return this.copyToTmpFolder(path, true, maxSize); + } + }, (error) => { + const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo'; + return this.treatCaptureError(error, defaultError); + }); + } + + /** + * Uploads a file of any type. + * This function will not check the size of the file, please check it before calling this function. + * + * @param {string} uri File URI. + * @param {string} name File name. + * @param {string} type File type. + * @param {boolean} [deleteAfterUpload] Whether the file should be deleted after upload. + * @param {string} [siteId] Id of the site to upload the file to. If not defined, use current site. + * @return {Promise} Promise resolved when the file is uploaded. + */ + uploadGenericFile(uri: string, name: string, type: string, deleteAfterUpload?: boolean, siteId?: string) : Promise { + let options = this.fileUploaderProvider.getFileUploadOptions(uri, name, type, deleteAfterUpload); + return this.uploadFile(uri, -1, false, options, siteId); + } + + /** + * Convenient helper for the user to upload an image, either from the album or taking it with the camera. + * + * @param {Boolean} fromAlbum True if the image should be selected from album, false if it should be taken with camera. + * @param {Number} maxSize Max size of the upload. -1 for no max size. + * @param {Boolean} upload True if the image should be uploaded, false to return the picked file. + * @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported. + * @return {Promise} The reject contains the error message, if there is no error message + * then we can consider that this is a silent fail. + */ + uploadImage(fromAlbum, maxSize, upload, mimetypes) { + this.logger.debug('Trying to capture an image with camera'); + + let options: CameraOptions = { + quality: 50, + destinationType: this.camera.DestinationType.FILE_URI, + correctOrientation: true + }; + + if (fromAlbum) { + const imageSupported = !mimetypes || this.utils.indexOfRegexp(mimetypes, /^image\//) > -1, + videoSupported = !mimetypes || this.utils.indexOfRegexp(mimetypes, /^video\//) > -1; + + options.sourceType = this.camera.PictureSourceType.PHOTOLIBRARY; + options.popoverOptions = { + x: 10, + y: 10, + width: this.platform.width() - 200, + height: this.platform.height() - 200, + arrowDir: this.camera.PopoverArrowDirection.ARROW_ANY + }; + + // Determine the mediaType based on the mimetypes. + if (imageSupported && !videoSupported) { + options.mediaType = this.camera.MediaType.PICTURE; + } else if (!imageSupported && videoSupported) { + options.mediaType = this.camera.MediaType.VIDEO; + } else if (this.platform.is('ios')) { + // Only get all media in iOS because in Android using this option allows uploading any kind of file. + options.mediaType = this.camera.MediaType.ALLMEDIA; + } + } else if (mimetypes) { + if (mimetypes.indexOf('image/jpeg') > -1) { + options.encodingType = this.camera.EncodingType.JPEG; + } else if (mimetypes.indexOf('image/png') > -1) { + options.encodingType = this.camera.EncodingType.PNG; + } + } + + return this.camera.getPicture(options).then((path) => { + let error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported. + if (error) { + return Promise.reject(error); + } + + if (upload) { + return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getCameraUploadOptions(path, fromAlbum)); + } else { + // Copy or move the file to our temporary folder. + return this.copyToTmpFolder(path, !fromAlbum, maxSize, 'jpg'); + } + }, (error) => { + let defaultError = fromAlbum ? 'core.fileuploader.errorgettingimagealbum' : 'core.fileuploader.errorcapturingimage'; + return this.treatImageError(error, defaultError); + }); + } + + /** + * Upload a file given the file entry. + * + * @param {any} fileEntry The file entry. + * @param {boolean} deleteAfter True if the file should be deleted once treated. + * @param {number} [maxSize] Max size of the file. If not defined or -1, no max size. + * @param {boolean} [upload] True if the file should be uploaded, false to return the picked file. + * @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection. + * @param {string} [name] Name to use when uploading the file. If not defined, use the file's name. + * @return {Promise} Promise resolved when done. + */ + uploadFileEntry(fileEntry: any, deleteAfter: boolean, maxSize?: number, upload?: boolean, allowOffline?: boolean, + name?: string) : Promise { + return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => { + return this.uploadFileObject(file, maxSize, upload, allowOffline, name).then((result) => { + if (deleteAfter) { + // We have uploaded and deleted a copy of the file. Now delete the original one. + this.fileProvider.removeFileByFileEntry(fileEntry); + } + return result; + }); + }); + } + + /** + * Upload a file given the file object. + * + * @param {any} file The file object. + * @param {number} [maxSize] Max size of the file. If not defined or -1, no max size. + * @param {boolean} [upload] True if the file should be uploaded, false to return the picked file. + * @param {boolean} [allowOffline] True to allow selecting in offline, false to require connection. + * @param {string} [name] Name to use when uploading the file. If not defined, use the file's name. + * @return {Promise} Promise resolved when done. + */ + uploadFileObject(file: any, maxSize?: number, upload?: boolean, allowOffline?: boolean, name?: string) : Promise { + if (maxSize != -1 && file.size > maxSize) { + return this.errorMaxBytes(maxSize, file.name); + } + + return this.confirmUploadFile(file.size, false, allowOffline).then(() => { + // 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.copyAndUploadFile(file, upload, name); + }); + } + + /** + * Convenience function to upload a file, allowing to retry if it fails. + * + * @param {string} path Absolute path of the file to upload. + * @param {number} maxSize Max size of the upload. -1 for no max size. + * @param {boolean} checkSize True to check size. + * @param {CoreFileUploaderOptions} Options. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if the file is uploaded, rejected otherwise. + */ + protected uploadFile(path: string, maxSize: number, checkSize: boolean, options: CoreFileUploaderOptions, siteId?: string) + : Promise { + + let errorStr = this.translate.instant('core.error'), + retryStr = this.translate.instant('core.retry'), + uploadingStr = this.translate.instant('core.fileuploader.uploading'), + promise, + file, + errorUploading = (error) => { + // Allow the user to retry. + return this.domUtils.showConfirm(error, errorStr, retryStr).then(() => { + // Try again. + return this.uploadFile(path, maxSize, checkSize, options, siteId); + }, () => { + // User cancelled. Delete the file if needed. + if (options.deleteAfterUpload) { + this.fileProvider.removeExternalFile(path); + } + return Promise.reject(null); + }); + }; + + if (!this.appProvider.isOnline()) { + return errorUploading(this.translate.instant('core.fileuploader.errormustbeonlinetoupload')); + } + + if (checkSize) { + // Check that file size is the right one. + promise = this.fileProvider.getExternalFile(path).then((fileEntry) => { + return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((f) => { + file = f; + return file.size; + }); + }).catch(() => { + // Ignore failures. + }); + } else { + promise = Promise.resolve(0); + } + + return promise.then((size) => { + if (maxSize != -1 && size > maxSize) { + return this.errorMaxBytes(maxSize, file.name); + } + + if (size > 0) { + return this.confirmUploadFile(size); + } + }).then(() => { + // File isn't too large and user confirmed, let's upload. + let modal = this.domUtils.showModalLoading(uploadingStr); + + return this.fileUploaderProvider.uploadFile(path, options, (progress: ProgressEvent) => { + // Progress uploading. + if (progress && progress.lengthComputable) { + let 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) => { + this.logger.error('Error uploading file.', error); + + modal.dismiss(); + if (typeof error != 'string') { + error = this.translate.instant('core.fileuploader.errorwhileuploading'); + } + return errorUploading(error); + }).finally(() => { + modal.dismiss(); + }); + }); + } +}