js/file-manager.js

/**
 * @module fileManager
 *
 * @description Simple file manager with cross-browser support. That uses the FileReader
 * to create previews. Can be replaced with a more advanced version that
 * obtains files from storage.
 *
 * The replacement should support the same public methods and return the same
 * types.
 */

import $ from 'jquery';

import { getFilename, dataUriToBlobSync } from './utils';
const fileManager = {};
import { t } from 'enketo/translator';
const URL_RE = /[a-zA-Z0-9+-.]+?:\/\//;

/**
 * @static
 * @function init
 *
 * @description Initialize the file manager.
 *
 * @return {Promise|boolean|Error} promise boolean or rejection with Error
 */
fileManager.init = () => { return Promise.resolve( true ); };

/**
 * @static
 * @function isWaitingForPermissions
 *
 * @description Whether the filemanager is waiting for user permissions
 *
 * @return {boolean} [description]
 */
fileManager.isWaitingForPermissions = () => { return false; };

/**
 * @static
 * @function getFileUrl
 *
 * @description Obtains a URL that can be used to show a preview of the file when used
 * as a src attribute.
 *
 * It is meant for media previews and media downloads.
 *
 * @param  {?string|object} subject - File or filename in local storage
 * @return {Promise|string|Error} promise url string or rejection with Error
 */
fileManager.getFileUrl = subject => {
    return new Promise( ( resolve, reject ) => {
        let error;

        if ( !subject ) {
            resolve( null );
        } else if ( typeof subject === 'string' ) {
            // TODO obtain from storage as http URL or objectURL
            // or from model for default binary files

            // Very crude URL checker which is fine for now,
            // because at this point we don't expect anything other than jr://
            if ( URL_RE.test( subject ) ) {
                resolve( subject );
            } else {
                reject( 'no!' );
            }
        } else if ( typeof subject === 'object' ) {
            if ( fileManager.isTooLarge( subject ) ) {
                error = new Error( t( 'filepicker.toolargeerror', { maxSize: fileManager.getMaxSizeReadable() } ) );
                reject( error );
            } else {
                resolve( URL.createObjectURL( subject ) );
            }
        } else {
            reject( new Error( 'Unknown error occurred' ) );
        }
    } );
};

/**
 * @static
 * @function getObjectUrl
 *
 * @description Similar to getFileURL, except that this one is guaranteed to return an objectURL
 *
 * It is meant for loading images into a canvas.
 *
 * @param  {?string|object} subject - File or filename in local storage
 * @return {Promise|string|Error} promise url string or rejection with Error
 */
fileManager.getObjectUrl = subject => fileManager.getFileUrl( subject )
    .then( url => {
        if ( /https?:\/\//.test( url ) ) {
            return fileManager.urlToBlob( url ).then( URL.createObjectURL );
        }

        return url;
    } );

/**
 * @static
 * @function urlToBlob
 *
 * @param {string} url - url to get
 * @return {Promise} promise of XMLHttpRequesting given url
 */
fileManager.urlToBlob = url => {
    const xhr = new XMLHttpRequest();

    return new Promise( resolve => {
        xhr.open( 'GET', url );
        xhr.responseType = 'blob';
        xhr.onload = () => {
            resolve( xhr.response );
        };
        xhr.send();
    } );
};

/**
 * @static
 * @function getCurrentFiles
 *
 * @description Obtain files currently stored in file input elements of open record
 *
 * @return {Array.File} array of files
 */
fileManager.getCurrentFiles = () => {
    const files = [];

    // Get any files inside file input elements or text input elements for drawings.
    $( 'form.or' ).find( 'input[type="file"]:not(.ignore), input[type="text"][data-drawing="true"]' ).each( function() {
        let newFilename;
        let file = null;
        let canvas = null;
        if ( this.type === 'file' ) {
            file = this.files[ 0 ]; // Why doesn't this fail for empty file inputs?
        } else if ( this.value ) {
            canvas = $( this ).closest( '.question' )[ 0 ].querySelector( '.draw-widget canvas' );
            if ( canvas && !URL_RE.test( this.value ) ) {
                // TODO: In the future, we could simply do canvas.toBlob() instead
                file = dataUriToBlobSync( canvas.toDataURL() );
                file.name = this.value;
            }
        }
        if ( file && file.name ) {
            // Correct file names by adding a unique-ish postfix
            // First create a clone, because the name property is immutable
            // TODO: in the future, when browser support increase we can invoke
            // the File constructor to do this.
            newFilename = getFilename( file, this.dataset.filenamePostfix );

            // If file is resized, get Blob representation of data URI
            if ( this.dataset.resized && this.dataset.resizedDataURI ) {
                file = dataUriToBlobSync( this.dataset.resizedDataURI );
            }
            file = new Blob( [ file ], {
                type: file.type
            } );
            file.name = newFilename;
            files.push( file );
        }
    } );

    return files;
};

/**
 * @static
 * @function isTooLarge
 *
 * @description Placeholder function to check if file size is acceptable.
 *
 * @return {boolean} whether file is too large
 */
fileManager.isTooLarge = () => { return false; };

/**
 * @static
 * @function getMaxSizeReadable
 *
 * @description Replace with function that determines max size published in OpenRosa server response header.
 *
 * @return {string} human radable maximiym size
 */
fileManager.getMaxSizeReadable = () => { return `${5}MB`; };

export default fileManager;