Reputation: 2958
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
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
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