chidimo
chidimo

Reputation: 2958

How do I pass data to meteor method?

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.

Upvotes: 0

Views: 210

Answers (2)

chidimo
chidimo

Reputation: 2958

Thanks @Nathan Schwarz for your response. You really helped me on this one. Here's how I finally solved my puzzle.

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)
    })
})

With this code upload_parts is completed before Meteor.call('aws.completeUpload',...)

Take note of how I resolved the promises in uploadParts. It follows from how the backend server works. I can only get the return value from the backend if I pass a callback to Meteor.call(). The backend itself returns a promise as seen below.

'aws.uploadPart' (filename, blob, upload_id, index) {
    console.log("aws.uploadPart method")

    console.log("file blob: ", blob)
    console.log("filename: ", filename)

    let params = {
        Bucket: s3_bucket,
        Key: filename,
        PartNumber: index,
        UploadId: upload_id,
        Body: blob,
    }

    return new Promise((resolve, reject) => {
        s3.uploadPart(params, (err, data) => {
            if (err) reject(err)
            if (data) {
                console.log("Upload part return ", data)
                resolve(data)
            }
        })
    })

Upvotes: 0

Nathan Schwarz
Nathan Schwarz

Reputation: 641

Your array is empty because you call asyncs functions to fill the array, so meteor delete it when you send it to the server.

you need to fill the arrays synchronously, or wrap them into promises.

    ....
    const uploadParts = (filename, blob, upload_id, index) => {
       return new Promise((resolve, reject) => resolve(
       Meteor.call('aws.uploadPart', filename, blob, upload_id, index, (err, res) => {
           upload_parts.push({Etag: res.ETag, partNumber: res.index})
       }))
    }
    let promises = []
    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)
        promises.push(uploadParts(filename, blob, upload_id, index))
    }
    Promise.all(promises).then(() => {
       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)
       })
    })

check the documentation here ('asyncCallback'): https://docs.meteor.com/api/methods.html#Meteor-call

Upvotes: 2

Related Questions