Wednesday, February 17, 2016

FlowJs Upload Reciever for Grails

I chose flow.js (https://github.com/flowjs/flow.js) for my client side file uploading due to its fault tollerance. I use using the ng-flow (https://github.com/flowjs/ng-flow/) angular library that builds an excellent set of directives around the framework. The one downside is the lack of documentation on what it takes to build a server side compliment to this. The particular beckend I am using is Grails3 based (although this would work for earlier versions of grails with little modification).

I built a service that can be plugged into multiple controllers to achieve the effect. While I wont publish the whole service, here is a skelleton that should be enough to get you started.



A Grails Flow Upload Service (replace config values with your own)
/** * A Simple service that can handle multipart uploads and recombine them once complete */
@Transactional
class FlowUploadService {

    //Needed to get config values    def grailsApplication
    /**     * Parse the incoming Flow Upload Chunk Requests     * @param params HTTP Request Parameter Map     * @param request HTTP Request     * @return     */    def handleFlowPartUpload(params, request) {

        if (!params.flowChunkNumber || !params.flowTotalChunks || !params.flowIdentifier || !params.flowFilename)
            return  [status: 400, upload: null, err: new Exception("invalidParams")]

        if (request.get) { /** Flow is asking if chunk exists, it might be retrying an upload **/            println "get ${params} ${request}"            def status = checkChunk(params)
            return [status: status, upload: null, err: null]
        }
        else if (request.post) /** Flow is handling us a chunk to upload **/        {
            println "post ${params} ${request}"            def file = request.getFile('file')
            if(file.empty) {
                return [status: 400, upload: null, err : new Exception('noFile')]
            } else {

                def finalFile = null                try {
                    saveChunk(params, file)
                    if (params.flowChunkNumber == params.flowTotalChunks) {
                        finalFile = combineChunks(params, file) /** Handle Final Chunk **/                    }
                }
                catch(Exception e)
                {
                    e.printStackTrace()
                    return [status: 200, upload: finalFile, err: e]
                }

                return [status: 200, upload: finalFile, err : null]
            }
        }
    }

    private int checkChunk(params) {
        def destinationPath = grailsApplication.config.yolobe.upload.temp
        new File(destinationPath).mkdirs() //TODO remove for performance        String output = getHashOfChunk(params)
        def chunkFile = new File(destinationPath + output)
        return chunkFile.exists()? 200 : 204    }

    private void saveChunk(params, file) {
        def destinationPath = grailsApplication.config.yolobe.upload.temp +  getHashOfChunk(params)
        def finalFile = new File(destinationPath)
        println "uploading chunk $params.flowChunkNumber to $destinationPath"        file.transferTo(finalFile)
    }

    private def combineChunks(params, file) {
        //Determine a final random "Date/(HASH)(UUID)"        String uuid = UUID.randomUUID().toString()
        String date = new Date().format('M-d-yyyy')

        /*        def extension = params.flowFilename.lastIndexOf('.').with {            it != -1 ? params.flowFilename[0..<it] : params.flowFilename        } //optionally preserve extension of final file - could be exploited*/
        def fileName = date + File.separator + getHashOfChunk(params) + uuid
        def destinationPath = grailsApplication.config.yolobe.upload.path +  File.separator + fileName
        def chunks = Integer.parseInt(params.flowTotalChunks)
        def finalFile = new File(destinationPath)
        def fileStream = new FileOutputStream(finalFile)
        println "combining $params.flowTotalChunks chunks to $destinationPath"            1.upto(chunks) { c ->
                def input = new FileInputStream(grailsApplication.config.yolobe.upload.temp + getHashOfChunk(params, c))
                fileStream << input
            }
        fileStream.close()

        [path: destinationPath, name: fileName]
    }

    private String getHashOfChunk(params, chunkNo = null) {
        MessageDigest md = MessageDigest.getInstance("MD5")
        byte[] md5sum = md.digest(params.flowIdentifier.getBytes());
        String output = String.format("%032X", new BigInteger(1, md5sum)) + (chunkNo? chunkNo : params.flowChunkNumber);
        output
    }
}

No comments:

Post a Comment