Nuxt Upload multiple files toS3 - amazon-s3

I am trying to figure out how to upload multiple files to my S3 bucket but can't seem to figure it out. I am running a Nuxt.js app with express api. I am just testing out 1 or 2 files to upload, but get a 500 response with the error:
Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1.
My Vue Template: /pages/add.vue
<template>
<v-col cols="12">
<h2 class="display-1 font-weight-light mb-5">Add Photos</h2>
<v-form
ref="uploadForm"
v-model="valid"
enctype="multipart/form-data"
lazy-validation
#submit.prevent="submitFiles"
>
<v-row>
<v-col cols="12" sm="6">
<v-file-input
v-model="photos"
:counter="30"
:rules="uploadRules"
accept="image/png, image/jpeg"
show-size
multiple
label="Click to upload photos"
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn :loading="loading" color="primary" #click="submitFiles()">
Upload
</v-btn>
</v-col>
</v-row>
</v-form>
</v-col>
</template>
<script>
export default {
layout: 'admin',
data: () => ({
photos: [],
uploadRules: [(v) => (v && v.length <= 30) || 'No more than 30 files'],
valid: true,
loading: false
}),
methods: {
submitFiles() {
if (this.$refs.uploadForm.validate()) {
this.loading = true
const formData = new FormData()
const Form = this
for (let i = 0; i < this.photos.length; i++) {
const file = this.photos[i]
formData.append(`photos['${i}']`, file)
}
this.$axios
.post('/photos/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then((res) => {
console.log('success')
console.log(res)
})
.catch((error) => {
console.log('fail')
console.log(error)
})
.finally(() => {
Form.loading = false
Form.photos = []
})
}
}
}
}
</script>
my upload file: /utils/upload.js
const aws = require('aws-sdk')
const multer = require('multer')
const multerS3 = require('multer-s3')
const s3 = new aws.S3()
aws.config.update({
apiVersion: '2006-03-01',
accessKeyId: process.env.AWS_KEY,
secretAccessKey: process.env.AWS_SECRET,
region: process.env.AWS_REGION_APP
})
const upload = multer({
storage: multerS3({
s3,
bucket: process.env.BUCKET,
acl: 'public-read',
metadata: (req, file, cb) => {
cb(null, { fieldName: file.fieldname })
},
key: (req, file, cb) => {
cb(null, Date.now().toString())
}
})
})
module.exports = upload
My Express api: /api/photos.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
app.use(bodyParser.json())
const upload = require('../utils/upload')
// Upload photos
app.post('/upload', upload.array('photos', 3), (req, res) => {
res.send('Successfully uploaded ' + req.files.length + ' files!')
})
module.exports = app

I'm still not exactly sure why, but after I installed aws-cli to test my credentials, it worked.
I also updated part of the code in the add.vue file
from:
for (let i = 0; i < this.photos.length; i++) {
const file = this.photos[i]
formData.append(`photos['${i}']`, file)
}
to:
for (let i = 0; i < this.photos.length; i++) {
formData.append(`photos`, this.photos[i])
}

Related

Trouble uploading file in Vue frontend

When I try to upload a user photo with Vue, the formData just seems to be an empty object. The Multer middleware then fails because there is no req.file for it read.
User.vue
<form enctype="multipart/form-data">
<div class="pl-3 label">User Photo</div>
<v-file-input
accept='image/*'
label="Upload Photo"
v-model="photo"
></v-file-input>
<v-btn class="mr-5" color="blue lighten-3" #click="submit">
save changes
</v-btn>
export default {
data: () => ({
photo: null
}),
methods: {
submit() {
let formData = new FormData();
if (this.photo) {
formData.append('photo', this.photo)
}
this.$store.dispatch('updateMe', {
photo: formData
});
}
}
};
Vuex Store
export default new Vuex.Store({
state: {
photo: null
},
mutations: {
setUpdatedUser(state, payload) {
state.name = payload.name;
state.email = payload.email;
state.photo = payload.photo;
}
},
actions: {
async updateMe({ commit, getters }, payload) {
let id = localStorage.getItem('userId');
let user = {
photo: payload.photo
};
try {
let token = getters.token;
const response = await axios.patch(
`http://localhost:3000/api/v1/users/updateMe`,
user,
{
headers: { Authorization: 'Bearer ' + token }
}
);
commit('setUpdatedUser', response.data.updatedUser);
} catch (err) {
console.log(err);
}
}
}
User Controller
const multerStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/users');
},
filename: (req, file, cb) => {
console.log(file)
const ext = file.mimetype.split('/')[1];
cb(null, `user-${req.user.id}-${Date.now()}.${ext}`);
}
});
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image. Please upload only images.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadUserPhoto = upload.single('photo');
Most other examples I have compared to do not use v-model for the input binding. Does that make a difference?
In an application where I am handling file uploads the files get posted to the node backend.
I also v-model in my component so that shouldn't be the problem.
Frontend method:
upload() {
const data = new FormData()
data.append('file', this.file, this.file.name) // Filename is optional
data.append('additionalInformation', this.additionalInformation)
axios.
post('/api/v1/upload/', data)
.then(response => /* response handling */)
.catch(error => /* error handling */)
}
The node endpoint looks like this:
app.post('/api/v1/upload/', multer().single('file'), (request, response) => {
upload(request.body, request.file, response, config)
})
Reference: FormData.append()

Vue JS Jest : mount() doesn't render properly

MyView.vue :
<template>
<v-container fluid>
<v-row>
<v-col v-for="obj in myObjs" :key="obj.ref">
<MyChild :obj="obj" />
</v-col>
</v-row>
</v-container>
</template>
export default class MyView extends Vue {
myObjs: MyObj[] = []
mounted () {
this.myObjs = await api.getGreatObjs()
// this.myObjs is like expected
}
}
MyView.spec.ts :
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
jest.mock('axios')
jest.spyOn(api, 'getGreatObjs')
.mockImplementation( () => new Promise((resolve, reject) => {
resolve([new MyObj({name: 'toto', value: 45}), ...])
}))
})
it('should render', () => {
wrapper = mount(MyView, {
stubs: ['router-link']
})
expect(wrapper.findAll(MyChild).length).toBe(2)
})
Content of wrapper.html():
<v-container fluid=\"\"><v-container fluid=\"\"><v-row></v-row></v-container></v-container>
I also tried this (MyView.spec.ts):
describe('P1RealTime.vue', () => {
let wrapper: any
let localVue: any
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
})
it('should render', () => {
wrapper = mount(MyView, {
localVue: localVue,
stubs: ['router-link']
})
expect(wrapper.html()).toEqual('')
})
})
Console result :
Expected: ""
Received: "<div class=\"container container--fluid\"><div class=\"container container--fluid\"><div></div></div>"
There is nothing inside "row" (<v-row></v-row>) or "div" (<div></div>) but there should be multiple MyChild ; so, I don't understand.
Could you help me ?

Quasar upload file from axios

I have a form with multiple inputs that includes file input too. Now, I want to pass these data on the onSubmit function. but, there is an issue, in the quasar documentation, I didn't see instruction about file upload by Axios in the script part.
I read Uploader in the quasar doc and also I read this one from Stackoverlow, But I didn't work for me.
Also, this is my templates code:
<template>
<div class="q-pa-md q-mt-md">
<q-card class="my-card">
<q-form
#submit="onSubmit"
class="q-gutter-md"
>
<div class="row justify-center">
<q-uploader
label="Upload your music"
color="purple"
accept=".mp3"
:max-file-size="20000000"
square
flat
#add="file_selected"
bordered
/>
</div>
<div class="row justify-center">
<q-btn label="Edit" type="submit" color="primary" v-if="song_id" class="q-ma-md" />
<q-btn label="Add" type="submit" color="primary" v-else class="q-ma-md" />
<q-btn label="Cancel" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</div>
</template>
And the methods part:
file_selected: function (file) {
console.log(file)
this.selected_file = file[0]
this.check_if_document_upload = true
},
onSubmit: function () {
const url = '/core/v1/api/songs/upload'
const fileData = new FormData()
fileData.append('file_data', this.selected_file)
fileData.append('song_id', this.song_id)
this.$axios.post(url, fileData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function () {
console.log('SUCCESS!!')
})
.catch(function () {
console.log('FAILURE!!')
})
And data part:
data: () => ({
selected_file: '',
check_if_document_upload: false,
song_id: '',
song_data: {
status: true
},
dashData: []
}),
If quasar uploads isn't working for you and you are using state management vuex, you could attempt writing custom code to accomplish what you want. Try this for sending the post request using axios
createEvents({ commit }, payload) {
const stuff = {
title: payload.title,
location: payload.location,
description: payload.description,
image = payload.image;
};
let formData = new FormData();
bodyFormData.set('title', stuff.title); //done for the text data
formData.append("imageUrl", stuff.image); //done for file data
axios
.post({
method: 'post',
url: 'myurl',
data: formData,
headers: {'Content-Type': 'multipart/form-data' }
})
.then(response => {
commit("createEvents", response.data);
})
.catch(err => err.data);
}
}
And for the submit function(method), it should look something like this
createEvent(){
const newEvent = {
title: '',
location: '',
description: '',
image: this.image,
};
this.$store.dispatch("createEvents", newEvent);
};
finally, the form itself in your code. The image should be referenced with a simple
<input type='file' ref='image'> and the rest of your form can be normal
<form>
<input type='text' v-model='text'>
<-- more of the same -->
<input type='file' ref='image'>
// prevent is to keep the page from reloading when the form gets submitted,
// just a precaution measure
<button type=submit #click.prevent=createEvent()>submit</button>
</form>
Hope this helped
I found my issue. I should change #add to #added in the template.
<template>
<div class="q-pa-md q-mt-md">
<q-card class="my-card">
<q-form
#submit="onSubmit"
class="q-gutter-md"
>
<div class="row justify-center">
<q-uploader
label="Upload your music"
color="purple"
accept=".mp3"
:max-file-size="20000000"
square
flat
#added="file_selected"
bordered
/>
</div>
<div class="row justify-center">
<q-btn label="Edit" type="submit" color="primary" v-if="song_id" class="q-ma-md" />
<q-btn label="Add" type="submit" color="primary" v-else class="q-ma-md" />
<q-btn label="Cancel" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</div>
</template>
If you want to keep the QUploader functionalities, status changes, upload progress, in my case I made the component extension and it works fine, maybe it is not efficient because I had to add my own methods: upload, __uploadFiles.
Don't add method to override __runFactory, since I omitted batch load option, and I will always use factory as function.
QUploader source:
Part Code from Quasar Components Uploader -> xhr mixin.js
upload () {
if (this.canUpload === false) {
return
}
const queue = this.queuedFiles.slice(0)
this.queuedFiles = []
if (this.xhrProps.batch(queue)) {
this.__runFactory(queue)
}
else {
queue.forEach(file => {
this.__runFactory([ file ])
})
}
},
__runFactory (files) {
this.workingThreads++
if (typeof this.factory !== 'function') {
this.__uploadFiles(files, {})
return
}
const res = this.factory(files)
if (!res) {
this.$emit(
'factory-failed',
new Error('QUploader: factory() does not return properly'),
files
)
this.workingThreads--
}
else if (typeof res.catch === 'function' && typeof res.then === 'function') {
this.promises.push(res)
const failed = err => {
if (this._isBeingDestroyed !== true && this._isDestroyed !== true) {
this.promises = this.promises.filter(p => p !== res)
if (this.promises.length === 0) {
this.abortPromises = false
}
this.queuedFiles = this.queuedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'failed') })
this.$emit('factory-failed', err, files)
this.workingThreads--
}
}
res.then(factory => {
if (this.abortPromises === true) {
failed(new Error('Aborted'))
}
else if (this._isBeingDestroyed !== true && this._isDestroyed !== true) {
this.promises = this.promises.filter(p => p !== res)
this.__uploadFiles(files, factory)
}
}).catch(failed)
}
else {
this.__uploadFiles(files, res || {})
}
},
__uploadFiles (files, factory) {
const
form = new FormData(),
xhr = new XMLHttpRequest()
const getProp = (name, arg) => {
return factory[name] !== void 0
? getFn(factory[name])(arg)
: this.xhrProps[name](arg)
}
const url = getProp('url', files)
if (!url) {
console.error('q-uploader: invalid or no URL specified')
this.workingThreads--
return
}
const fields = getProp('formFields', files)
fields !== void 0 && fields.forEach(field => {
form.append(field.name, field.value)
})
let
uploadIndex = 0,
uploadIndexSize = 0,
uploadedSize = 0,
maxUploadSize = 0,
aborted
xhr.upload.addEventListener('progress', e => {
if (aborted === true) { return }
const loaded = Math.min(maxUploadSize, e.loaded)
this.uploadedSize += loaded - uploadedSize
uploadedSize = loaded
let size = uploadedSize - uploadIndexSize
for (let i = uploadIndex; size > 0 && i < files.length; i++) {
const
file = files[i],
uploaded = size > file.size
if (uploaded) {
size -= file.size
uploadIndex++
uploadIndexSize += file.size
this.__updateFile(file, 'uploading', file.size)
}
else {
this.__updateFile(file, 'uploading', size)
return
}
}
}, false)
xhr.onreadystatechange = () => {
if (xhr.readyState < 4) {
return
}
if (xhr.status && xhr.status < 400) {
this.uploadedFiles = this.uploadedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'uploaded') })
this.$emit('uploaded', { files, xhr })
}
else {
aborted = true
this.uploadedSize -= uploadedSize
this.queuedFiles = this.queuedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'failed') })
this.$emit('failed', { files, xhr })
}
this.workingThreads--
this.xhrs = this.xhrs.filter(x => x !== xhr)
}
xhr.open(
getProp('method', files),
url
)
if (getProp('withCredentials', files) === true) {
xhr.withCredentials = true
}
const headers = getProp('headers', files)
headers !== void 0 && headers.forEach(head => {
xhr.setRequestHeader(head.name, head.value)
})
const sendRaw = getProp('sendRaw', files)
files.forEach(file => {
this.__updateFile(file, 'uploading', 0)
if (sendRaw !== true) {
form.append(getProp('fieldName', file), file, file.name)
}
file.xhr = xhr
file.__abort = () => { xhr.abort() }
maxUploadSize += file.size
})
this.$emit('uploading', { files, xhr })
this.xhrs.push(xhr)
if (sendRaw === true) {
xhr.send(new Blob(files))
}
else {
xhr.send(form)
}
}
Result: With AXIOS - Component Vue that extend from QUploader
<script lang="ts">
import { QUploader } from 'quasar';
export default class AxiosUploader extends QUploader {
constructor(props) {
super(props);
}
AxiosUpload() {
if (this.canUpload === false) {
return;
}
const queue = this.queuedFiles.slice(0);
this.queuedFiles = [];
const factory = this.factory(queue);
queue.forEach(file => {
this.workingThreads++;
this.uploadFiles([file], factory);
});
}
uploadFiles(files, factory) {
const form = new FormData(),
headers = {};
factory.headers.forEach(head => {
headers[head.name] = head.value;
});
factory.formFields.forEach(field => {
form.append(field.name, field.value);
});
form.append(factory.fieldName, files[0], files[0].name);
let uploadIndex = 0,
uploadIndexSize = 0,
uploadedSize = 0,
maxUploadSize = 0,
aborted;
const xhr = this.$axios.post(factory.url, form, {
headers,
onUploadProgress: (e: ProgressEvent) => {
if (aborted === true) {
return;
}
const loaded = Math.min(maxUploadSize, e.loaded);
this.uploadedSize += loaded - uploadedSize;
uploadedSize = loaded;
let size = uploadedSize - uploadIndexSize;
for (let i = uploadIndex; size > 0 && i < files.length; i++) {
const file = files[i],
uploaded = size > file.size;
if (uploaded) {
size -= file.size;
uploadIndex++;
uploadIndexSize += file.size;
this.__updateFile(file, 'uploading', file.size);
} else {
this.__updateFile(file, 'uploading', size);
return;
}
}
}
});
this.xhrs.push(xhr);
this.$emit('uploading', { files, xhr });
xhr
.then(res => {
this.uploadedFiles = this.uploadedFiles.concat(files);
files.forEach(f => {
this.__updateFile(f, 'uploaded');
});
this.$emit('uploaded', { files, xhr });
})
.catch(err => {
aborted = true;
this.uploadedSize -= uploadedSize;
this.queuedFiles = this.queuedFiles.concat(files);
files.forEach(f => {
this.__updateFile(f, 'failed');
});
this.$emit('failed', { files, xhr });
})
.finally(() => {
this.workingThreads--;
this.xhrs = this.xhrs.filter(x => x !== xhr);
});
files.forEach(file => {
this.__updateFile(file, 'uploading', 0);
file.xhr = xhr;
file.__abort = () => {
xhr.abort();
};
maxUploadSize += file.size;
});
this.$emit('uploading', { files, xhr });
this.xhrs.push(xhr);
}
}
</script>
The component to use is AxiosUploader instead of q-uploader,
and instead of calling the upload () method, I call the AxiosUpload method. You could adapt to your needs

Vue Axios - with bootstrapVue and vuex, still problem to upload file and text?

I have vue SPA and I trying to upload some images and text. I try using postman to test my server code and it's work, but my client-side still error.
I think I'm still mistaken for handle req.file.
addImage.vue
<template>
<div>
<b-form-group label-cols-sm="3" description="image title" label="Title">
<b-form-input v-model="newImage.title"></b-form-input>
</b-form-group>
<b-form-group label="my Image" label-for="file" label-cols-sm="2">
<b-form-file id="file" v-model="newImage.image"></b-form-file>
</b-form-group>
<b-button variant="primary" class="pl-5 pr-5" #click.prevent="addImage">Save</b-button>
</div>
</template>
<script>
export default {
data() {
return {
newImage: {
title: '',
image: null,
},
};
},
methods: {
addImage() {
this.$store.dispatch('addProduct', this.newProduct);
}
},
};
store.js
actions: {
addImage(context, newImage) {
const config = {
headers: {
token: localStorage.getItem('token'),
},
};
Axios
.post(`${baseUrl}/product`, newImage, config)
.then(({ newImage }) => {
context.commit('ADD_IMAGE', newImage);
})
.catch((err) => {
console.log(err.message);
});
}
}
When you want to upload an image you have to set the content type and create FormData, like this:
let data = new FormData();
for (var i = 0; i < files.length; i++) {
let file = files.item(i);
data.append('images[' + i + ']', file, file.name);
}
const config = {
headers: { 'content-type': 'multipart/form-data' }
}
return axios.post('/api/images', data, config)

Vue JS and Multer

<template>
<v-app id="inspire" dark >
<v-layout v-for = "post in posts" :key = "post.id">
<v-flex xs3>
<v-card >
<v-card-media>
<img
:src = "post.postImage">
</v-card-media>
<v-card-title>
{{post.description}}
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-app>
</template>
<script>
import PostService from '#/service/PostService'
export default {
data(){
return {
posts: []
}
},
mounted (){
this.getPosts()
},
methods: {
async getPosts() {
const response = await PostService.fetchPosts()
this.posts = response.data.posts
}
}
}
</script>
const multer = require('multer');
const Post = require('./models/post')
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/');
},
filename: (req, file, cb ) => {
cb(null, Date.now() + file.originalname);
}
})
const fileFilter = (req, file, cb) => {
if(file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
}else {
cb(null, false);
}
}
const upload = multer({
storage: storage,
fileFilter,
limits: {
fileSize: 1024 * 1024 * 5
}
});
app.get('/posts', (req, res) => {
Post.find({},'postImage description', (error, posts) => {
if(error) {
console.log(error)
}
res.send({
posts
})
}).sort({_id: -1})
})
Hi all, i was about to display images using multer on my front end. but it does only show imagelink or image destination, but i already added some images in my backend. So how can i show images to my front-end using vue dynamically. I did use v-for for each data in my database. what should i input to my v-card-media to get images dynamically.