Skip to content

Instantly share code, notes, and snippets.

@danneu
Created February 11, 2018 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danneu/81395fcb6378c24a95a76b820bb3ada0 to your computer and use it in GitHub Desktop.
Save danneu/81395fcb6378c24a95a76b820bb3ada0 to your computer and use it in GitHub Desktop.
This is a naive impl of a node server that accepts image uploads at POST /uploads/:uuid but will stream inflight-uploads from GET /uploads/:uuid.
// curl -X POST --data-binary "@../massive.jpg" http://localhost:3001/uploads/xxx
// curl -X POST --data-binary "@../massive.jpg" http://localhost:3001/uploads/xxx --limit-rate 32k
'use strict'
const fs = require('fs')
const { EventEmitter } = require('events')
const express = require('express')
const PORT = 3001
const app = express()
app.disable('x-powered-by')
// lets our upload route communicate progress to our download route
//
// | event | payload |
// +------------+--------------+
// | "progress" | [uuid, buffer] |
// | "end" | uuid |
const emitter = new EventEmitter()
// set of inflight uuids being uploaded
const uploads = new Set()
// generates the full file path of a given uuid
function makeFilePath(uuid) {
return `${__dirname}/uploads/${uuid}.jpg`
}
// UPLOAD A FILE
app.post('/uploads/:uuid', (req, res) => {
const { uuid } = req.params
// TODO: Handle uuid already exists or inflight
uploads.add(uuid)
const writable = fs.createWriteStream(makeFilePath(uuid))
req.on('data', chunk => {
console.log(`chunk: ${Buffer.byteLength(chunk)} bytes`)
writable.write(chunk)
emitter.emit('progress', [uuid, chunk])
})
req.on('error', err => {
console.error('upload error:', err)
uploads.delete(uuid)
// TODO: Delete incomplete file from fs
})
req.on('close', () => {
// Client closed connection before finish
uploads.delete(uuid)
// TODO: Delete incomplete file from fs
})
req.on('end', () => {
// Client finished upload
writable.end()
uploads.delete(uuid)
// Respond to request with URL of file upload
res.end(`http://localhost:${PORT}/uploads/${uuid}`)
})
})
// DOWNLOAD A FILE
app.get('/uploads/:uuid', (req, res) => {
const { uuid } = req.params
// TODO: 404 if path doesn't exist and is not an inflight upload
res.set('content-type', 'image/jpeg')
// if file is not inflight then we just pipe from fs and end
if (!uploads.has(uuid)) {
const stream = fs.createReadStream(makeFilePath(uuid))
stream.on('error', err => {
res.set('content-type', 'text/plain')
if (err.code === 'ENOENT') {
res.status(404).end('Not found')
} else {
console.error(err)
res.status(500).end('Internal server error')
}
})
stream.pipe(res)
return
}
// file is still being uploaded, so pipe what we have on disk and listen for upload progress
// pipe what's on disk but don't auto-close
fs.createReadStream(makeFilePath(uuid)).pipe(res, { end: false })
const onProgress = ([chunkId, buffer]) => {
if (uuid === chunkId) {
res.write(buffer)
}
}
const onEnd = chunkId => {
if (uuid === chunkId) {
res.end()
emitter.off('progress', onProgress)
emitter.off('end', onEnd)
}
}
emitter.on('progress', onProgress)
emitter.on('end', onEnd)
})
app.listen(PORT, () => console.log(`listening on port ${PORT}`))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment