log-fetch
Version:
Implementation of Fetch that uses the Dat SDK for loading p2p content
332 lines (292 loc) • 18.3 kB
JavaScript
module.exports = async function makeHyperFetch (opts = {}) {
const { makeRoutedFetch } = await import('make-fetch')
const {fetch, router} = makeRoutedFetch({onNotFound: handleEmpty, onError: handleError})
const mime = require('mime/lite')
const parseRange = require('range-parser')
const { Readable, pipelinePromise } = require('streamx')
const path = require('path')
const DEFAULT_OPTS = {}
const finalOpts = { ...DEFAULT_OPTS, ...opts }
const app = await (async (finalOpts) => {if(finalOpts.sdk){return finalOpts.sdk}else{const SDK = await import('hyper-sdk');const sdk = await SDK.create(finalOpts);return sdk;}})(finalOpts)
const hyperTimeout = 30000
const hostType = '_'
// const SUPPORTED_METHODS = ['GET', 'HEAD', 'POST', 'DELETE']
const drives = new Map()
const id = await (async () => {
const drive = await app.getDrive('id')
const check = drive.key.toString('hex')
drives.set(check, drive)
return check
})()
async function checkForDrive(prop){
if(drives.has(prop)){
return drives.get(prop)
}
const drive = await app.getDrive(prop)
drives.set(drive.key.toString('hex'), drive)
return drive
}
async function waitForStuff(useTo, mainData) {
if (useTo.num) {
return await Promise.race([
new Promise((resolve, reject) => setTimeout(() => { const err = new Error(`${useTo.msg} timed out`); err.name = 'TimeoutError'; reject(err); })),
mainData
])
} else {
return await mainData
}
}
function handleEmpty(request) {
const { url, headers: reqHeaders, method, body, signal } = request
if(signal){
signal.removeEventListener('abort', takeCareOfIt)
}
const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
return {status: 400, headers: { 'Content-Type': mainRes }, body: mainReq ? `<html><head><title>${url}</title></head><body><div><p>did not find any data</p></div></body></html>` : JSON.stringify('did not find any data')}
}
function handleError(e, request) {
const { url, headers: reqHeaders, method, body, signal } = request
if(signal){
signal.removeEventListener('abort', takeCareOfIt)
}
const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
return {status: 500, headers: { 'X-Error': e.name, 'Content-Type': mainRes }, body: mainReq ? `<html><head><title>${e.name}</title></head><body><div><p>${e.stack}</p></div></body></html>` : JSON.stringify(e.stack)}
}
function takeCareOfIt(data){
console.log(data)
throw new Error('aborted')
}
function sendTheData(theSignal, theData){
if(theSignal){
theSignal.removeEventListener('abort', takeCareOfIt)
}
return theData
}
function handleFormData(formdata){
const arr = []
for (const [name, info] of formdata) {
if (name === 'file') {
arr.push(info)
}
}
return arr
}
function formatReq(hostname, pathname){
const useData = {}
if(hostname === hostType){
useData.useHost = id
} else {
useData.useHost = hostname
}
useData.usePath = decodeURIComponent(pathname)
return useData
}
async function saveFileData(drive, main, body, useOpt) {
await pipelinePromise(Readable.from(body), drive.createWriteStream(main.usePath, useOpt))
return main.usePath
}
async function saveFormData(drive, mid, data, useOpts) {
for (const info of data) {
await pipelinePromise(Readable.from(info.stream()), drive.createWriteStream(path.join(mid.usePath, info.webkitRelativePath || info.name).replace(/\\/g, "/"), useOpts))
}
return mid.usePath
}
function getMimeType (path) {
let mimeType = mime.getType(path) || 'text/plain'
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
return mimeType
}
async function handleHead(request) {
const { url, headers: reqHeaders, method, signal, body } = request
if(signal){
signal.addEventListener('abort', takeCareOfIt)
}
const { hostname, pathname, protocol, search, searchParams } = new URL(url)
const main = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname))
const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
const useOpts = { ...useOpt, timeout: reqHeaders.has('x-timer') || searchParams.has('x-timer') ? reqHeaders.get('x-timer') !== '0' || searchParams.get('x-timer') !== '0' ? Number(reqHeaders.get('x-timer') || searchParams.get('x-timer')) * 1000 : undefined : hyperTimeout }
if (reqHeaders.has('x-copy') || searchParams.has('x-copy')) {
const useDrive = await waitForStuff({ num: useOpts.timeout, msg: 'drive' }, checkForDrive(main.useHost))
if (path.extname(main.usePath)) {
const useData = await useDrive.entry(main.usePath)
if (useData) {
const pathToFile = JSON.parse(reqHeaders.get('x-copy') || searchParams.get('x-copy')) ? path.join(`/${useDrive.key.toString('hex')}`, useData.key).replace(/\\/g, "/") : useData.key
const mainDrive = await checkForDrive(id)
await mainDrive.put(pathToFile, await useDrive.get(useData.key))
const useHeaders = {}
useHeaders['X-Link'] = 'hyper://_' + pathToFile.replace(/\\/g, "/")
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
return sendTheData(signal, { status: 200, headers: { 'Content-Length': `${useData.value.blob.byteLength}`, ...useHeaders }, body: '' })
} else {
return sendTheData(signal, { status: 400, headers: { 'X-Error': 'did not find any file' }, body: '' })
}
} else {
const useIdenPath = JSON.parse(reqHeaders.get('x-copy') || searchParams.get('x-copy')) ? `/${useDrive.key.toString('hex')}` : '/'
const mainDrive = await checkForDrive(id)
let useNum = 0
for await (const test of useDrive.list(main.usePath)) {
useNum = useNum + test.value.blob.byteLength
const pathToFile = path.join(useIdenPath, test.key).replace(/\\/g, "/")
await mainDrive.put(pathToFile, await useDrive.get(test.key))
}
const pathToFolder = path.join(useIdenPath, main.usePath).replace(/\\/g, "/")
const useHeaders = {}
useHeaders['X-Link'] = 'hyper://_' + pathToFolder.replace(/\\/g, "/")
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
return sendTheData(signal, { status: 200, headers: { 'Content-Length': `${useNum}`, ...useHeaders }, body: '' })
}
} else if (reqHeaders.has('x-load') || searchParams.has('x-load')) {
const useDrive = await waitForStuff({ num: useOpts.timeout, msg: 'drive' }, checkForDrive(main.useHost))
if (JSON.parse(reqHeaders.get('x-load') || searchParams.get('x-load'))) {
if (path.extname(main.usePath)) {
const useData = await useDrive.entry(main.usePath)
await useDrive.get(main.usePath)
const useHeaders = {}
useHeaders['X-Link'] = `hyper://${useDrive.key.toString('hex')}${main.usePath}`
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
return sendTheData(signal, { status: 200, headers: { 'Content-Length': `${useData.value.blob.byteLength}`, ...useHeaders }, body: '' })
} else {
await useDrive.download(main.usePath, useOpts)
const useHeaders = {}
useHeaders['X-Link'] = `hyper://${useDrive.key.toString('hex')}${main.usePath}`
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
return sendTheData(signal, { status: 200, headers: { 'Content-Length': '0', ...useHeaders }, body: '' })
}
} else {
if (path.extname(main.usePath)) {
await useDrive.del(main.usePath)
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), main.usePath).replace(/\\/g, '/')
return sendTheData(signal, {status: 200, headers: {'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`}, body: ''})
} else {
for await (const test of useDrive.list(main.usePath)){
await useDrive.del(test.key)
}
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), main.usePath).replace(/\\/g, '/')
return sendTheData(signal, { status: 200, headers: { 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"` }, body: ''})
}
}
} else {
const useDrive = await waitForStuff({num: useOpts.timeout, msg: 'drive'}, checkForDrive(main.useHost))
if (path.extname(main.usePath)) {
const useData = await useDrive.entry(main.usePath)
if (useData) {
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), useData.key).replace(/\\/g, "/")
return sendTheData(signal, { status: 200, headers: { 'Content-Length': String(useData.value.blob.byteLength), 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"` }, body: '' })
} else {
return sendTheData(signal, {status: 400, headers: {'X-Error': 'did not find any file'}, body: ''})
}
} else {
let useNum = 0
for await (const test of useDrive.list(main.usePath)) {
useNum = useNum + test.value.blob.byteLength
}
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), main.usePath).replace(/\\/g, "/")
return sendTheData(signal, { status: 200, headers: { 'Content-Length': String(useNum), 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"` }, body: '' })
}
}
}
async function handleGet(request) {
const { url, headers: reqHeaders, method, signal, body } = request
if(signal){
signal.addEventListener('abort', takeCareOfIt)
}
const { hostname, pathname, protocol, search, searchParams } = new URL(url)
const main = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname))
const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
const useOpts = { ...useOpt, timeout: reqHeaders.has('x-timer') || searchParams.has('x-timer') ? reqHeaders.get('x-timer') !== '0' || searchParams.get('x-timer') !== '0' ? Number(reqHeaders.get('x-timer') || searchParams.get('x-timer')) * 1000 : undefined : hyperTimeout }
const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
const useDrive = await waitForStuff({num: useOpts.timeout, msg: 'drive'}, checkForDrive(main.useHost))
if (path.extname(main.usePath)) {
const useData = await useDrive.entry(main.usePath)
if (useData) {
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), useData.key).replace(/\\/g, "/")
const isRanged = reqHeaders.has('Range') || reqHeaders.has('range')
if(isRanged){
const ranges = parseRange(useData.value.blob.byteLength, reqHeaders.get('Range') || reqHeaders.get('range'))
// if (ranges && ranges.length && ranges.type === 'bytes') {
if ((ranges !== -1 && ranges !== -2) && ranges.type === 'bytes') {
const [{ start, end }] = ranges
const length = (end - start + 1)
return sendTheData(signal, {status: 206, headers: {'Content-Type': getMimeType(useData.key), 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Length': `${length}`, 'Content-Range': `bytes ${start}-${end}/${useData.value.blob.byteLength}`}, body: useDrive.createReadStream(useData.key, {start, end})})
} else {
return sendTheData(signal, {status: 416, headers: {'Content-Type': mainRes, 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Length': `${useData.value.blob.byteLength}`}, body: mainReq ? '<html><head><title>range</title></head><body><div><p>malformed or unsatisfiable range</p></div></body></html>' : JSON.stringify('malformed or unsatisfiable range')})
}
} else {
return sendTheData(signal, {status: 200, headers: {'Content-Type': getMimeType(useData.key), 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Length': `${useData.value.blob.byteLength}`}, body: useDrive.createReadStream(useData.key)})
}
} else {
return sendTheData(signal, { status: 400, headers: { 'Content-Type': mainRes }, body: mainReq ? `<html><head><title>hyper://${main.useHost}${main.usePath}</title></head><body><div><p>did not find any file</p></div></body></html>` : JSON.stringify('did not find any file') })
}
} else {
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), main.usePath).replace(/\\/g, "/")
const arr = []
for await (const test of useDrive.readdir(main.usePath)) {
arr.push(path.join('/', test).replace(/\\/g, '/'))
}
return sendTheData(signal, {status: 200, headers: {'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Type': mainRes}, body: mainReq ? `<html><head><title>${main.usePath}</title></head><body><div><p><a href='../'>..</a></p>${arr.map((data) => {return `<p><a href="${data}">${data}</a></p>`})}</div></body></html>` : JSON.stringify(arr)})
}
}
async function handlePost(request) {
const { url, headers: reqHeaders, method, signal, body } = request
if(signal){
signal.addEventListener('abort', takeCareOfIt)
}
const { hostname, pathname, protocol, search, searchParams } = new URL(url)
const main = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname))
const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
const useDrive = await checkForDrive(main.useHost)
const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
const getSaved = reqHeaders.has('content-type') && reqHeaders.get('content-type').includes('multipart/form-data') ? await saveFormData(useDrive, main, handleFormData(await request.formData()), useOpt) : await saveFileData(useDrive, main, body, useOpt)
const useName = useDrive.key.toString('hex')
const saved = 'hyper://' + path.join(useName, getSaved).replace(/\\/g, '/')
const useLink = 'hyper://' + path.join(useName, main.usePath).replace(/\\/g, '/')
return sendTheData(signal, {status: 200, headers: {'Content-Type': mainRes, 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`}, body: mainReq ? `<html><head><title>Fetch</title></head><body><div>${JSON.stringify(saved)}</div></body></html>` : JSON.stringify(saved)})
}
async function handleDelete(request) {
const { url, headers: reqHeaders, method, signal, body } = request
if(signal){
signal.addEventListener('abort', takeCareOfIt)
}
const { hostname, pathname, protocol, search, searchParams } = new URL(url)
const main = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname))
const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
const useDrive = await checkForDrive(main.useHost)
const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
if (path.extname(main.usePath)) {
const useData = await useDrive.entry(main.usePath)
if (useData) {
await useDrive.del(useData.key)
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), useData.key).replace(/\\/g, '/')
useOpt.deleted = 'success'
return sendTheData(signal, {status: 200, headers: {'Status': useOpt.deleted, 'Content-Type': mainRes, 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Length': `${useData.value.blob.byteLength}`}, body: mainReq ? `<html><head><title>Fetch</title></head><body><div>${useLink}</div></body></html>` : JSON.stringify(useLink)})
} else {
return sendTheData(signal, { status: 400, headers: { 'Content-Type': mainRes }, body: mainReq ? '<html><head><title>range</title></head><body><div><p>did not find any file</p></div></body></html>' : JSON.stringify('did not find any file') })
}
} else {
let useNum = 0
for await (const test of useDrive.list(main.usePath)){
useNum = useNum + test.value.blob.byteLength
await useDrive.del(test.key)
}
const useLink = 'hyper://' + path.join(useDrive.key.toString('hex'), main.usePath).replace(/\\/g, '/')
return sendTheData(signal, { status: 200, headers: { 'Content-Type': mainRes, 'X-Link': useLink, 'Link': `<${useLink}>; rel="canonical"`, 'Content-Length': `${useNum}` }, body: mainReq ? `<html><head><title>Fetch</title></head><body><div>${useLink}</div></body></html>` : JSON.stringify(useLink) })
}
}
router.head('hyper://*/**', handleHead)
router.get('hyper://*/**', handleGet)
router.post('hyper://*/**', handlePost)
router.delete('hyper://*/**', handleDelete)
fetch.close = async () => {
for (const drive of drives.values()) {
await drive.close()
}
drives.clear()
return await app.close()
}
return fetch
}