Adam Smolenski
Posted on February 27, 2021
So a challenge I recently had to tackle was getting a file upload to go to a specific dropbox.
Some caveats are that this was behind a paywall so the security issues were not as dire as an open website and the media was personally reviewed by a human before reposting so there was no concern of inappropriate images being put back into the world. There are some APIs that can handle that but for now, let's just link an upload from react to a Dropbox folder.
So let's start with some tools we will need for the backend.There will be a few different libraries here outside of the regular old express. I used dropbox-v2-api, fs and multer to serve up the file once it reached my backend before processing and sending it to dropbox. Another thing to note, this was hosted on heroku and due to their ephemeral system I was able to utilize the tmp folder and not worry about impact on my system.
The Dropbox API is pretty self-explanatory but what is multer and fs? Multer is middleware to handle multi-part forms. Since sending a file is considered a multipart form (there are probably other part's to the form... get it?). I haven't had coffee yet so my jokes are probably awful. FS is just filesystem asynchronous promises for handling of files.
Let's go through the base imports and some of the settings on multer that we will have for express first.
const express = require("express");
const keys = require('./keys');
const fs = require("fs");
const cors = require("cors")
const dropboxV2Api = require("dropbox-v2-api");
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, '/tmp/');
},
filename: (req, file, cb) => {
const fileName = file.originalname.toLowerCase().split(' ').join('-');
cb(null, fileName)
}
});
var upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype == "image/png" || file.mimetype == "image/jpg" || file.mimetype == "image/jpeg") {
cb(null, true);
} else {
cb(null, false);
req.error = 'Only .png, .jpg and .jpeg allowed';
return cb(null, false, new Error('Only .png, .jpg and .jpeg format allowed!'));
}
}
});
On top we have the storage working out. To reiterate since we have heroku as an ephemeral system we store it in the tmp folder and it will be cleared out. One reason I did this is incase I wanted to implement that external API for NSFW photos I could send it through the filter before sending it to dropbox.
The filename parsing was just to protect against whitespace and standardize convention... probably could have made it a regex expression as well, but that's what came to mind first. So that first part is setting up all of our storage options locally on the server now to the upload part.
We were only looking for certain filetypes, the original intention of the project was to just take screenshots from peoples journey through the app so those typically fall into the jpg or png category depending on what tool you are using so we set up the filter there. On the else side of the clause we are creating an error message that would be sent back to the frontend to warn our users they're not sending an acceptable filetype.
Another aside... I'm importing keys so you don't see my dropbox token. I chose to link this to a specific folder.. to do this we can start by creating an app at Dropbox Developer. Create an app, change permissions so you can write files. That's extremely important since this permission is tied to the key being generated. After that is done you can generate a token that doesn't expire. Copy that over to a keys.js and do a lovely export. module.exports = { DROPBOX: your key here}
Now let's get to the upload route portion of the backend.
const app = express()
app.use(
cors({
origin: "http://localhost:3000"
})
);
app.get("/", (req, res) => {
res.send("potato")
})
app.post('/upload', upload.any(), (req, res) => {
if (req.error) {
return res.json({ errors: req.error })
}
let { path } = req.files[0]
let filetype = req.files[0].mimetype.split("/")[1]
let { folder, user } = req.body
const dropbox = dropboxV2Api.authenticate({
token: keys.DROPBOX
});
const params = Object.freeze({
resource: 'files/upload',
parameters: {
path: `/${folder}/${user}.${filetype}`
},
readStream: fs.createReadStream(path)
});
let dropboxPromise = new Promise(function (resolve, reject) {
dropbox(params, function (err, result) {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
dropboxPromise.then(function (resultObj) {
console.log("fileUpload_OK")
return res.json({ success: "Your file has been successfully added." })
}).catch(function (err) {
console.log(err)
return res.json({ errors: err })
});
})
app.listen(5000, () => {
console.log(`Server successfully created on Port: 5000`);
});
You can ignore the send of potato... I just like having that to makesure if I'm using an API that I am able to check I didn't break anything... I should start leaving a signature on things right? As a stagehand we used to leave our marks in random ceilings in 100 year old theaters. This is easily edited but who knows.
We have upload.any(), this means we can send a number of files back. We will focus on just one, but you may adjust your code as necessary. The parameters for the upload were set up earlier. So we are just going to get the first object because on the frontend I will limit to one file upload. I am getting the filetype from the mimetype, mainly because I want to rename these files in dropbox, and want it to transfer into the right format. The path is the filepath on the users computer. Folder and user are two variables taken from the frontend. Since these files were uploaded on a per performance situation (it was the theater project I've mentioned before). The folder was actually the show schedule and user well... was the user. For something that is more open you may want to use a UUID to makesure there are no collisions, for this usecase Username was unique, so that was not a concern.
Next part we are logging into our dropbox API with our token. We establish an object with a readstream of our file to transfer while then starting a Promise to ensure we can wait for a success or failure. Once it's resolved we can return a json object to tell our users if it was a success or a failure. So this is a quick overview of what is happening. Much of the magic is handled by the dropbox API.
Let's create our frontend now to hook it up and see if it works. We will be using filepond because it has some neat features and makes drag and drop easy. We will also use axios in our react component so we can send the form to the backend. Also filepond-react and filepond, here I'm just going to use UUID instead of username, so do what you want with that.
So here is what our frontend will look like.
import React, { useState } from 'react';
import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css';
import axios from 'axios'
import {v4} from 'uuid'
const Upload = (props) => {
const [photo, setPhoto] = useState()
const [errors, setErrors] = useState("")
const [success, setSuccess] = useState(false)
const onSubmit = (e) => {
e.preventDefault()
setErrors("")
if (photo && photo.length > 0){
let formData = new FormData()
formData.append('photo', photo[0].file)
formData.append('folder', new Date())
formData.append('user', v4())
setErrors("Sending File")
axios.post(`http://localhost:5000/upload`, formData, {
}).then(res => {
console.log(res.data)
if (res.data.errors){
setErrors(res.data.errors)
}else if(res.data.success){
setSuccess(true)
}
}).catch(e=> setErrors(e))}else{
setErrors("Please select an image file first.")
}
}
const renderErrors = () => {
if (errors !== ""){
return (
<div>{errors}</div>
)
}else{
return null
}
}
return (
<>
{success ?
<div>Upload Success!</div>
:
<div style={{height: "300px", width:"400px", margin: "auto"}}>
<form onSubmit={onSubmit} >
<FilePond
labelIdle={"Drag your file here"}
credits={true}
file={photo}
name="photo"
allowMultiple={false}
instantUpload={false}
onupdatefiles={(fileItems) => setPhoto(fileItems)}>
</FilePond>
{renderErrors()}
<div >
<button type="submit">Upload</button>
</div>
</form>
</div>
}
</>
)
};
export default Upload;
So walking through this again... the first three variables are just setting our status, photo will be the file after it is dragged and dropped onto filepond, errors so we can show any errors that may arise like wrong filetype, no file or maybe our server went down... it happens. Success is just there to get rid of the filepond once we are uploaded.
Formdata here is the multi-part that we were talking about with multer. So each append is adding a part of the form. Those are also what we are deciphering in our backend. The rest seems pretty self explanatory. The filepond options used are labelIdle for the message on first render. Credits are for the filepond team to make some money for their good work. File is the controlled part of our previous useState photo. Allowmultiple can give you the option of uploading several files at once. Instant upload will enable filepond to upload as soon as you drop, I disabled that and added the button just because sometimes you have users who make mistakes, lets not clutter our filesystem :). On update, in case they make that mistake you want to swap that file out. Once you hit upload, it goes off to the dropbox folder for your app...
Have Fun!!!
Posted on February 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 12, 2024