I have a form that accepts a file to pass on to Dropbox using the session upload. I've wrapped the code in a Fastify server but always end up with an error in append:
{"error_summary": "closed/..", "error": {".tag": "closed"}}.
Here's the Fastify server:
require("dotenv").config()
const https = require("https")
const JSONbig = require("json-bigint")
const TOKEN = process.env.DROPBOX_ACCESS_TOKEN
const fastify = require("fastify")({
logger: {
level: "info"
}
})
fastify.addContentTypeParser("*", function (req, done) {
done()
})
// Promisify https request
const httpRequest = (url, options, data) => {
return new Promise((resolve, reject) => {
const req = https.request(url, options, (res) => {
let body = ""
res.on("data", (chunk) => {
console.log("SDSDSDSDASDASDSAD")
body += chunk.toString()
})
res.on("error", reject)
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode <= 299) {
resolve({ statusCode: res.statusCode, headers: res.headers, body: JSON.parse(body) })
} else {
reject("Request failed. status: " + res.statusCode + ", body: " + body)
}
})
})
req.on("error", reject)
if (data) req.write(data)
req.end()
})
}
fastify.register(require("fastify-cors"))
fastify.post("/", async (req, reply) => {
const stream = req.raw
const initResponse = await httpRequest(
"https://content.dropboxapi.com/2/files/upload_session/start",
{
method: "post",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Dropbox-API-Arg": JSON.stringify({
close: false
}),
"Content-Type": "application/octet-stream"
}
}
)
const sessionId = initResponse.body.session_id
req.log.info(`Initiating session ${sessionId}`)
let offset = 0
stream.on("data", async (chunk) => {
req.log.info("Sending chunk")
stream.pause()
const appendResponse = await httpRequest(
`https://content.dropboxapi.com/2/files/upload_session/append_v2`,
{
method: "post",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Dropbox-API-Arg": JSONbig.stringify({
cursor: {
session_id: sessionId,
offset
},
close: false
}),
"Content-Type": "application/octet-stream"
}
},
chunk
)
stream.resume()
offset += chunk.length
})
stream.on("end", async () => {
const terminateResponse = await httpRequest(
`https://content.dropboxapi.com/2/files/upload_session/finish`,
{
method: "post",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Dropbox-API-Arg": JSONbig.stringify({
cursor: {
session_id: sessionId,
offset: offset
},
commit: {
path: "/Upload/test-large.txt",
mode: "add",
autorename: true,
mute: false,
strict_conflict: false
}
}),
"Content-Type": "application/octet-stream"
}
}
)
req.log.info("upload session finished")
reply.code(204).send()
})
})
fastify.listen(3000, (err) => {
if (err) {
fastify.log.fatal(err)
process.exit(1)
}
console.log(`server listening on ${fastify.server.address().port}`)
})
And here's the form:
<!DOCTYPE html>
<!-- Coding By CodingNepal - youtube.com/codingnepal -->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Upload with Progress Bar | CodingNepal</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
/>
<style>
/* Import Google font - Poppins */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #6990f2;
}
::selection {
color: #fff;
background: #6990f2;
}
.wrapper {
width: 430px;
background: #fff;
border-radius: 5px;
padding: 30px;
box-shadow: 7px 7px 12px rgba(0, 0, 0, 0.05);
}
.wrapper header {
color: #6990f2;
font-size: 27px;
font-weight: 600;
text-align: center;
}
.wrapper form {
height: 167px;
display: flex;
cursor: pointer;
margin: 30px 0;
align-items: center;
justify-content: center;
flex-direction: column;
border-radius: 5px;
border: 2px dashed #6990f2;
}
form :where(i, p) {
color: #6990f2;
}
form i {
font-size: 50px;
}
form p {
margin-top: 15px;
font-size: 16px;
}
section .row {
margin-bottom: 10px;
background: #e9f0ff;
list-style: none;
padding: 15px 20px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
}
section .row i {
color: #6990f2;
font-size: 30px;
}
section .details span {
font-size: 14px;
}
.progress-area .row .content {
width: 100%;
margin-left: 15px;
}
.progress-area .details {
display: flex;
align-items: center;
margin-bottom: 7px;
justify-content: space-between;
}
.progress-area .content .progress-bar {
height: 6px;
width: 100%;
margin-bottom: 4px;
background: #fff;
border-radius: 30px;
}
.content .progress-bar .progress {
height: 100%;
width: 0%;
background: #6990f2;
border-radius: inherit;
}
.uploaded-area {
max-height: 232px;
overflow-y: scroll;
}
.uploaded-area.onprogress {
max-height: 150px;
}
.uploaded-area::-webkit-scrollbar {
width: 0px;
}
.uploaded-area .row .content {
display: flex;
align-items: center;
}
.uploaded-area .row .details {
display: flex;
margin-left: 15px;
flex-direction: column;
}
.uploaded-area .row .details .size {
color: #404040;
font-size: 11px;
}
.uploaded-area i.fa-check {
font-size: 16px;
}
</style>
</head>
<body>
<div class="wrapper">
<header>File Uploader JavaScript</header>
<form action="#">
<input class="file-input" type="file" name="file" hidden />
<i class="fas fa-cloud-upload-alt"></i>
<p>Browse File to Upload</p>
</form>
<section class="progress-area"></section>
<section class="uploaded-area"></section>
</div>
<script>
const form = document.querySelector("form"),
fileInput = document.querySelector(".file-input"),
progressArea = document.querySelector(".progress-area"),
uploadedArea = document.querySelector(".uploaded-area")
// form click event
form.addEventListener("click", () => {
fileInput.click()
})
fileInput.onchange = ({ target }) => {
let file = target.files[0] //getting file [0] this means if user has selected multiple files then get first one only
if (file) {
let fileName = file.name //getting file name
if (fileName.length >= 12) {
//if file name length is greater than 12 then split it and add ...
let splitName = fileName.split(".")
fileName = splitName[0].substring(0, 13) + "... ." + splitName[1]
}
uploadFile(fileName) //calling uploadFile with passing file name as an argument
}
}
// file upload function
function uploadFile(name) {
let xhr = new XMLHttpRequest() //creating new xhr object (AJAX)
xhr.open("POST", "http://localhost:3000") //sending post request to the specified URL
xhr.upload.addEventListener("progress", ({ loaded, total }) => {
//file uploading progress event
let fileLoaded = Math.floor((loaded / total) * 100) //getting percentage of loaded file size
let fileTotal = Math.floor(total / 1000) // getting total file size in KB from bytes
let fileSize
// if file size is less than 1024 then add only KB else convert this KB into MB
fileTotal < 1024
? (fileSize = fileTotal + " KB")
: (fileSize = (loaded / (1024 * 1024)).toFixed(2) + " MB")
let progressHTML = `<li class="row">
<i class="fas fa-file-alt"></i>
<div class="content">
<div class="details">
<span class="name">${name} • Uploading</span>
<span class="percent">${fileLoaded}%</span>
</div>
<div class="progress-bar">
<div class="progress" style="width: ${fileLoaded}%"></div>
</div>
</div>
</li>`
// uploadedArea.innerHTML = ""; //uncomment this line if you don't want to show upload history
uploadedArea.classList.add("onprogress")
progressArea.innerHTML = progressHTML
if (loaded == total) {
progressArea.innerHTML = ""
let uploadedHTML = `<li class="row">
<div class="content upload">
<i class="fas fa-file-alt"></i>
<div class="details">
<span class="name">${name} • Uploaded</span>
<span class="size">${fileSize}</span>
</div>
</div>
<i class="fas fa-check"></i>
</li>`
uploadedArea.classList.remove("onprogress")
// uploadedArea.innerHTML = uploadedHTML; //uncomment this line if you don't want to show upload history
uploadedArea.insertAdjacentHTML("afterbegin", uploadedHTML) //remove this line if you don't want to show upload history
}
})
let data = new FormData(form) //FormData is an object to easily send form data
xhr.send(data) //sending form data
}
</script>
</body>
</html>
Does need an app created on DB to get oauth token to run example.
Anyone see the issue here? I've worked from a number of examples online but all of them assume I am using some dummy static file rather than incoming uploaded file.
Ok figured it out. Basically in my example I am terminating the session on end
but it turns out the end
event is fired at the same time as the last data
event so I am effectively closing the session in the midst of the last chunk append. I refactored things to have a flag isEnd
that gets set in the end
event handler and moved the terminate session and resolve code inside of the data
handler to be fired from there once isEnd
is set.