cozy/cozy-mobile

View on GitHub
src/app/lib/media/picture_handler.coffee

Summary

Maintainability
Test Coverage
async = require 'async'
ConnectionHandler = require '../connection_handler'
fs = require '../../replicator/filesystem'
path = require 'path'
DesignDocuments = require '../../replicator/design_documents'
MediaUploader = require './media_uploader'
RemoteRequest = require '../remote_request'
Permission = require '../permission'
log = require('../persistent_log')
    prefix: 'PictureHandler'
    date: true


instance = null


module.exports = class PictureHandler


    constructor: (media) ->
        if instance
            instance.media = media
        else
            @media = media
        return instance if instance
        instance = @

        _.extend @, Backbone.Events

        @localDb ?= app.init.database.localDb
        @replicateDb ?= app.init.database.replicateDb
        @remoteDb ?= app.init.database.remoteDb
        @connectionHandler ?= new ConnectionHandler()
        @requestCozy ?= app.init.requestCozy
        @remoteRequest = new RemoteRequest @requestCozy
        @permission = new Permission()


    upload: (callback) ->
        @_findLocalPicturesPath (err, picturesPath) =>
            return callback err if err

            async.series [
                (cb) => @_ensureDeviceFolder cb
                (cb) => @_removeOldPicturesOnCache picturesPath, cb
                (cb) => @_saveNewPicturesOnCache picturesPath, cb
                (cb) => @_findPicturesOnCache cb
                (cb) => @_findPicturesOnCozy cb
            ], (err, result) =>
                return callback err if err

                picturesCache = result[3]
                picturesCache = picturesCache.filter (pictureCache) ->
                    !pictureCache.value.binaryExist
                return callback() if picturesCache.length is 0

                @trigger "change:total", @, picturesCache.length
                @progression = 0

                cozyFiles = {}
                for cozyFile in result[4].rows
                    fileName = cozyFile.key[1].substr(2).toLowerCase()
                    cozyFiles[fileName] = cozyFile

                @_updateProgression()
                async.eachSeries picturesCache, (pictureCache, cb) =>
                    @media.isUploadable (ok) =>
                        return cb new Error "Is not uploadable." unless ok
                        async.series [
                            (cb) => @_uploadCozyFile pictureCache, cozyFiles, cb
                            (cb) => @_uploadCozyBinary pictureCache, cb
                            (cb) => @_checkCozyBinary pictureCache, cb
                        ], (err) =>
                            log.warn err if err
                            @_updateProgression()
                            cb()
                , (err) ->
                    log.warn err if err
                    callback()


    _checkCozyBinary: (pictureCache, callback) ->
        pictureValue = pictureCache.value
        return callback() if pictureValue.binaryExist or !pictureValue.binaryId

        @media.checkBinary pictureValue.binaryId, (err, binaryExist) =>
            if binaryExist
                data = binaryExist: binaryExist
            else
                data = binaryId: false

            @_updateCache pictureCache.id, data, callback


    _uploadCozyBinary: (pictureCache, callback) ->
        pictureValue = pictureCache.value
        return callback() if pictureValue.binaryId or not pictureValue.fileId

        @remoteDb ?= app.init.database.remoteDb
        @remoteDb.get pictureValue.fileId, (err, cozyFile) =>
            return callback err if err

            setBinaryId = (binaryId) =>
                @_updateCache pictureCache.id, binaryId: binaryId, (err) ->
                    return callback err if err
                    pictureCache.value.binaryId = binaryId
                    callback null, pictureCache

            binaryId = cozyFile.binary?.file?.id
            return setBinaryId binaryId if binaryId

            fs.getFileFromPath pictureCache.key, (err, file) =>
                return callback err if err

                @media.uploadBinary file, cozyFile._id, (err) =>
                    return callback err if err

                    @remoteDb.get cozyFile._id, (err, cozyFile) ->
                        return callback err if err

                        setBinaryId cozyFile.binary?.file?.id


    _uploadCozyFile: (pictureCache, cozyFiles, callback) ->
        return callback() if pictureCache.value.fileId

        setFileId = (fileId) =>
            @_updateCache pictureCache.id, fileId: fileId, (err) ->
                return callback err if err
                pictureCache.value.fileId = fileId
                callback null, pictureCache

        fileName = path.parse(pictureCache.key).base.toLowerCase()
        if cozyFiles[fileName] isnt undefined
            setFileId cozyFiles[fileName].id
        else
            @_createFile pictureCache.key, (err, fileId) ->
                return callback err if err
                setFileId fileId



    _findPicturesOnCozy: (callback) ->
        options =
            startkey: ['/' + t 'photos']
            endkey: ['/' + t('photos'), {}]
        @replicateDb.query DesignDocuments.FILES_AND_FOLDER, options, callback



    ############
    ### File
    ###


    _findLocalPicturesPath: (callback) ->

        success = ->
            ImagesBrowser.getImagesList (err, pictures) ->
                if pictures
                    if device.platform is 'Android'

                        # Filter images : keep only the ones from Camera
                        pictures = pictures.filter (picturePath) ->
                            picturePath? and \
                                    picturePath.indexOf('/DCIM/') isnt -1

                    # Filter pathes with ':' (colon), as cordova plugin won't
                    # pick them especially ':nopm:' ending files,
                    # which may be google+ 's NO Photo Manager
                    pictures = pictures.filter (picturePath) ->
                        picturePath.indexOf(':') is -1

                callback err, pictures

        @permission.checkPermission 'files', success, (err) ->
            callback err, []


    _createFile: (picturePath, callback) ->
        cozyPath = "/#{t 'photos'}"

        fs.getFileFromPath picturePath, (err, file) =>
            return callback err if err
            @media.createFile file, picturePath, cozyPath, callback



    ###############
    ### Local cache
    ###

    _removeOldPicturesOnCache: (picturesPath, callback) ->
        @_findPicturesOnCache (err, cachePictures) =>
            return callback err if err

            oldCache = cachePictures.filter (cachePicture) ->
                cachePicture.key not in picturesPath

            async.eachSeries oldCache, (cachePicture, cb) =>
                @_deleteCache cachePicture.id, cb
            , callback


    _saveNewPicturesOnCache: (picturesPath, callback) ->
        @_findPicturesOnCache (err, cachePictures) =>
            return callback err if err

            picturesCacheOrdered = {}
            for pictureCache in cachePictures
                picturesCacheOrdered[pictureCache.key] = pictureCache

            picturesPathFiltered = picturesPath.filter (picturePath) ->
                picturesCacheOrdered[picturePath] is undefined

            async.eachSeries picturesPathFiltered, (picturePath, cb) =>
                return @_createCache picturePath, cb
            , callback


    _deleteCache: (pictureId, callback) ->
        @localDb.get pictureId, (err, cachePicture) =>
            return callback err if err

            @localDb.remove cachePicture, callback


    _createCache: (picturePath, callback) ->
        cachePicture =
            docType : 'Photo'
            localId: picturePath

        @localDb.post cachePicture, callback


    _updateCache: (pictureId, data, callback) ->
        @localDb.get pictureId, (err, cachePicture) =>
            return callback err if err

            if data
                for key, value of data
                    cachePicture[key] = value

            @localDb.put cachePicture, callback


    _findPicturesOnCache: (callback) ->
        @localDb.query DesignDocuments.PHOTOS_BY_LOCAL_ID, {}, (err, result) ->
            return callback err if err
            cachePictures = result.rows
            callback null, cachePictures


    _ensureDeviceFolder: (callback) ->
        findFolder = (id, cb) =>
            @replicateDb.get id, (err, result) ->
                if not err? and result.rows isnt undefined
                    cb null, result.rows[0]
                else
                    # Busy waiting for device folder creation
                    setTimeout (-> findFolder id, cb ), 1000

        designId = DesignDocuments.FILES_AND_FOLDER
        options = key: ['', "1_#{t('photos').toLowerCase()}"]
        @replicateDb.query designId, options, (err, results) =>
            return callback err if err

            if results.rows.length > 0
                folder = results.rows[0]
                callback null, folder
            else
                docType = 'folder'
                filterName = 'byFullPath'

                @remoteRequest.putRequest docType, filterName, (err) =>
                    return callback err if err

                    options =
                        method: 'post'
                        type: 'data-system'
                        path: "/request/#{docType}/#{filterName}/"
                        retry: 3
                        body:
                            key: t 'photos'

                    @requestCozy.request options, (err, res, docs) =>
                        return callback err if err

                        if docs?.length is 0
                            @media.createFolder t('photos'), '', (err, id) ->
                                return callback err if err

                                # Wait to receive folder in local database
                                findFolder id, callback
                        else
                            # should not reach here: already exist remote,
                            # but not present in replicated @replicateDb ...
                            callback new Error 'photo folder not replicated yet'


    _updateProgression: ->
        @progression++
        @trigger "change:progress", @, @progression