Created
February 11, 2018 00:44
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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