@hyperspace/cli
Version:
A CLI for the hyper:// space network.
163 lines (145 loc) • 4.84 kB
JavaScript
import http from 'http'
import parseRange from 'range-parser'
import pump from 'pump'
import * as HyperStruct from '../../hyper/struct.js'
import * as mime from '../../mime.js'
const FULL_USAGE = `
Options:
-p/--port {number} - Choose the port to serve on (default 8080).
Examples:
hyp drive http hyper://1234..af/
`
export default {
name: 'drive http',
options: [
{
name: 'port',
default: 8080,
abbr: 'p'
}
],
description: 'Host a hyperdrive as using HTTP on the localhost.',
usage: {
simple: '{url}',
full: FULL_USAGE
},
command: async function (args) {
if (!args._[0]) throw new Error('URL is required')
var port = args.port || 8080
var drive = await HyperStruct.get(args._[0], {expect: 'hyperdrive'})
const server = http.createServer(createRequestHandler(drive))
server.listen(port)
console.log(`Serving at localhost:${port}`)
process.on('SIGINT', () => {
server.close()
// force a close after 1s if its being finicky
let to = setTimeout(() => process.exit(0), 1e3)
to.unref()
})
}
}
function createRequestHandler (drive) {
return async function (req, res) {
const respondRedirect = (url) => {
res.writeHead(200, {'Content-Type': 'text/html'})
res.end(`<!doctype html><meta http-equiv="refresh" content="0; url=${url}">`)
}
const respondError = (code, status, body = undefined) => {
res.writeHead(code, status)
res.end(body ? body : code + ' ' + status)
}
try {
// validate request
if (req.method !== 'GET' && req.method !== 'HEAD') {
return respondError(405, 'Method Not Supported')
}
var filepath = req.url
if (!filepath) filepath = '/'
if (filepath.indexOf('?') !== -1) filepath = filepath.slice(0, filepath.indexOf('?')) // strip off any query params
var hasTrailingSlash = filepath.endsWith('/')
// lookup entry
var statusCode = 200
var headers = {}
try {
var entry = await drive.api.promises.stat(filepath)
} catch (e) {
return respondError(404, 'Not Found')
}
// handle folder
if (entry && entry.isDirectory()) {
if (!hasTrailingSlash) {
// make sure there's a trailing slash
return respondRedirect(`${req.url || ''}/`)
}
var files = await drive.api.promises.readdir(filepath)
files.sort()
if (files.includes('index.html')) {
res.writeHead(200, {'Content-Type': 'text/html'})
drive.api.createReadStream(filepath + 'index.html').pipe(res)
} else {
res.writeHead(200, {'Content-Type': 'text/html'})
var updog = filepath === '/' ? '' : `<li><a href="..">..</a></li>`
res.end(`<ul>${updog}${files.map(file => `<li><a href="./${file}">${file}</a></li>`).join('')}</ul>`)
}
return
}
// 404
if (!entry) {
return respondError(404, 'File Not Found')
}
// handle range
headers['Accept-Ranges'] = 'bytes'
var length
var range = req.headers.Range || req.headers.range
if (range) range = parseRange(entry.size, range)
if (range && range.type === 'bytes') {
range = range[0] // only handle first range given
statusCode = 206
length = (range.end - range.start + 1)
headers['Content-Length'] = '' + length
headers['Content-Range'] = 'bytes ' + range.start + '-' + range.end + '/' + entry.size
} else {
if (entry.size) {
length = entry.size
headers['Content-Length'] = '' + length
}
}
Object.assign(headers, {
'Cache-Control': 'no-cache'
})
var mimeType = entry.metadata.mimeType;
if (!mimeType) {
// FIXME- for some reason createReadStream() is hanging when opts.length > the actual file length
// -prf
// let chunk
// for await (const part of drive.api.createReadStream(filepath, { start: 0, length: 512 })) {
// chunk = chunk ? Buffer.concat([chunk, part]) : part;
// }
mimeType = mime.identify(filepath/*, chunk*/)
}
if (mimeType.startsWith('text/markdown')) {
mimeType = 'text/plain'
}
headers['Content-Type'] = mimeType
if (req.method === 'HEAD') {
res.writeHead(204, headers)
res.end()
} else {
res.writeHead(statusCode, headers)
pump(
drive.api.createReadStream(filepath, range),
res,
err => {
if (err) {
console.error('Error while serving file', filepath)
console.error(err)
}
res.end()
}
)
}
} catch (e) {
respondError(500, 'Uncaught error', e.toString())
}
}
}