[Solved] How do I pass data to meteor method?

#1

I’m working on a way to upload to an AWS S3 bucket from a meteor server and react frontend.

I have defined the following files

server/methods.js

import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';

const AWS = require('aws-sdk')
const s3_bucket = "bucket-name"

import { mediaFiles } from '../imports/api/files.collection';

const s3  = new AWS.S3({
    accessKeyId: '<key>',
    secretAccessKey: '<secret>',
    endpoint: 's3.eu-west-2.amazonaws.com',
    region: 'eu-west-2',
    signatureVersion: 'v4'
});

Meteor.methods({
    'aws.getUploadId' (filename, filetype) {
        let params = {
            Bucket: s3_bucket,
            Key: filename,
            ContentType: filetype
        }
        return new Promise((resolve, reject) => {
            s3.createMultipartUpload(params, (err, data) => {
                if (err) reject(err)
                if (data) resolve(data.UploadId)
            })
        })
    },

    'aws.uploadPart' (filename, blob, upload_id, index) {
        let params = {
            Bucket: s3_bucket,
            Key: filename,
            PartNumber: index,
            UploadId: upload_id,
        }

        return new Promise((resolve, reject) => {
            s3.uploadPart(params, (err, data) => {
                if (err) reject(err)
                if (data) resolve(data)
            })
        })
    },

    'aws.completeUpload' (filename, upload_id, upload_parts) {
        console.log("aws.completeUpload called")
        console.log(`filename: ${filename}\nID: ${upload_id}\nUpload_parts****${upload_parts}****`)
        let params = {
            Bucket: s3_bucket,
            Key: filename,
            UploadId: upload_id,
            MultipartUpload: {Parts: upload_parts}
        }

        return new Promise((resolve, reject) => {
            s3.completeMultipartUpload(params, (err, data) => {
                if (err) reject(err)
                if (data) resolve(data)
            })
        })
    },
});

upload.js # client side

import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { Page, Icon, ProgressBar, Input, Select } from 'react-onsenui';
import _ from 'underscore';

import Navbar from './Navbar';

class Upload extends Component {

    state = { 
        uploadId : '',
        media_file : null,
        filename : '' 
    }

    setUploadFileParameters = (e) => {
        e.preventDefault();
        e.stopPropagation();
        console.log('setUploadFileParameters called')
        const media_file = e.target.files[0]
        const filename = media_file.name
        const filetype = media_file.type
        
        Meteor.call('aws.getUploadId', filename, filetype, (err, res) => {
            if (err) console.log("Error getting id: ", err)
            if (res) {
                this.setState({ media_file: media_file, filename: filename, uploadId: res })
            }
        })
    }

    uploadIt = (e) => {
        e.preventDefault();
        const t = e.target
        const upload_id = this.state.uploadId
        const media_file = t.media_file.files[0]
        console.log(`mediafile: ${media_file}`)

        try {
            const FILE_CHUNK_SIZE = 10000000 // 10MB
            const fileSize = media_file.size
            const filename = media_file.name
            const NUM_CHUNKS = Math.round(fileSize / FILE_CHUNK_SIZE) + 1
            let start, end, blob
            let upload_parts = []
    
            for (let index = 1; index < NUM_CHUNKS + 1; index++) {
                start = (index - 1)*FILE_CHUNK_SIZE
                end = (index)*FILE_CHUNK_SIZE
                blob = (index < NUM_CHUNKS) ? media_file.slice(start, end) : media_file.slice(start)

                // Puts each file part into the storage server
                Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
                    if (err) console.log("uploading part error ", err)
                    if (res) {
                        // console.log("RES: ", typeof res, res)
                        upload_parts.push({Etag: res.ETag, PartNumber: index})    
                    }
                })
            }
            
            // Generate the parts list to complete the upload
            // Calls the CompleteMultipartUpload endpoint in the backend server
            console.log("upload_parts: ", upload_parts)

            Meteor.call('aws.completeUpload', filename, upload_id, upload_parts, (err, res) => {
                console.log("Complete upload called *****")
                if (err) console.log("Complete upload err: ", err)
                if (res) console.log("Complete upload res: ", res)
            })
        }
        catch(err) {
            console.log(err)
        }
    }

    render() {
        const { showMenu } = this.props
        console.log("State: ", JSON.stringify(this.state))

            return (
                <Page renderToolbar={Navbar('Upload', showMenu)}>
                    <div className="form-container">

                    {Meteor.user() &&
                        <form onSubmit={(e) => this.uploadIt(e)}>

                            <p>File</p>

                            <Input
                                type="file"
                                id="fileinput"
                                ref="fileinput"
                                name="media_file"
                                onChange={e => this.setUploadFileParameters(e)}
                            />
                            <br/>

                            <button
                                type="submit"
                                value="Upload"
                                className="btn upload-work-button" 
                            >
                                Upload
                            </button>
                        </form>
                    }
                    </div>
                </Page>
            )
        }
    }
export default Upload;

The problem I have is that the upload_parts content is not being passed to the meteor backend server. A console log on the back end server doesn’t return anything. It doesn’t even return undefined.
I need help with this.

#2

The Meteor.call from the client run asynchronous, so I believe your method aws.completeUpload fires before the loop is finished …

#3

Yeah. I see that now.

I would use Promise.all() but the complication is that the return value of Meteor.call() is only accessible from a callback function. So this doesn’t work because it puts the method calls in the array.

let promises = []
promises.push(
    Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
        console.log("res ", res)
        return res
    })
)

This doesn’t work either

let promises = []
Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
    console.log("res ", res)
    promises.push(res)
    })
)

What I am looking for is a way to have all the res in an array so that I can apply Promise.all().

Is it something possible or I just have to find another way of completing my upload?

#4

This may help:

#5

This link has led me down a rabbit hole on Pony Foo.

Thanks @robfallows

As for my question, I eventually did this

const uploadParts = (filename, blob, upload_id, index) => {
    return new Promise(
        (resolve, reject) => 
        Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
            resolve(res)
        })
    )
}

for (let index = 1; index < NUM_CHUNKS; index++) {
    start = (index - 1)*FILE_CHUNK_SIZE
    end = (index)*FILE_CHUNK_SIZE
    blob = (index < NUM_CHUNKS) ? media_file.slice(start, end) : media_file.slice(start)

    const b = new Blob([blob], {type:filetype})
    const c = {size: blob.size, type:filetype}
    console.log("Media ", media_file.size, media_file)
    console.log("Blob: ", blob.size, blob)
    console.log("B: ", b.size, b)

    promises.push(uploadParts(filename, c, upload_id, index))
}

Promise.all(promises).then(res => {
    res.forEach((r, index) => 
        upload_parts.push({ETag: r.ETag, PartNumber: index+1})
    )
    console.log("upload_parts: ", upload_parts)
    Meteor.call('aws.completeUpload', filename, upload_id, upload_parts, (err, res) => {
        if (err) console.log("Complete upload err: ", err)
        if (res) console.log("Complete upload res: ", res)
    })
})

But I didn’t use it in the end. I learn’t it’d be too much work on the server, so now I just have the client handle the upload using a signed url generated by the server.

1 Like
#6

Glad you got it working!

Just a note on your code, this would still run the method NUM_CHUNKS times simultaneously.
The for loop creates each promise and starts working immediately.

If you wanted it to upload the next part only when the last part has finished, you would either use for await of as the loop (caution, not as easy as it sounds) or some other async series / waterfall handler.

Because you need to slice up the blob, I would store each slice of work and use a utility function to control the execution flow:

const uploadParts = (filename, blob, upload_id, index) => {
    return new Promise(
        (resolve, reject) => 
        Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
            resolve(res)
        })
    )
}
// Very generic utility function so you can save it for later.
const doInSeries = async (tasks, func) => {
    const results = [];
    for (const task of tasks) {
        const result = await func(task);
        results.push(result);
    }
    return results;
}
// Store the slices instead of promises
const slicesToUpload = [];
for (let index = 1; index < NUM_CHUNKS; index++) {
    start = (index - 1)*FILE_CHUNK_SIZE
    end = (index)*FILE_CHUNK_SIZE
    blob = (index < NUM_CHUNKS) ? media_file.slice(start, end) : media_file.slice(start)

    const b = new Blob([blob], {type:filetype})
    const c = {size: blob.size, type:filetype}
    console.log("Media ", media_file.size, media_file)
    console.log("Blob: ", blob.size, blob)
    console.log("B: ", b.size, b)
    // Store each slice as an array of arguments for uploadParts
    slicesToUpload.push([filename, c, upload_id, index])
}

// use an anonymous function to spread the task array into the arguments of uploadParts
// alternatively, uploadParts could be written to take an object of arguments and the task could be stored
// as a matching object so no intermediate function would be needed.
doInSeries(slicesToUpload, task => uploadParts(...task))
    .then(results => {
        // like Promise.all, we return the array of results;
        const upload_parts = results.map((result, index) => {
            return { ETag: result.ETag, PartNumber: index+1 };
        });
        console.log("upload_parts: ", upload_parts)
        Meteor.call('aws.completeUpload', filename, upload_id, upload_parts, (err, res) => {
            if (err) console.log("Complete upload err: ", err)
            if (res) console.log("Complete upload res: ", res)
        });
    });
1 Like