Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: resession/bittorrent-fetch
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: ducksandgoats/list-fetch
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.

Commits on May 22, 2022

  1. start

    ducksandgoats committed May 22, 2022
    Copy the full SHA
    45f5a6f View commit details
  2. start

    ducksandgoats committed May 22, 2022
    Copy the full SHA
    f227d17 View commit details
  3. start

    ducksandgoats committed May 22, 2022
    Copy the full SHA
    46328bc View commit details
  4. new

    ducksandgoats committed May 22, 2022
    Copy the full SHA
    28cc751 View commit details
  5. new

    ducksandgoats committed May 22, 2022
    Copy the full SHA
    c40c7c3 View commit details

Commits on May 23, 2022

  1. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    4286a99 View commit details
  2. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    a9fa416 View commit details
  3. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    b149241 View commit details
  4. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    acfd4eb View commit details
  5. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    5732c20 View commit details
  6. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    aa74521 View commit details
  7. middle

    ducksandgoats committed May 23, 2022
    Copy the full SHA
    8b8c8e5 View commit details
  8. Copy the full SHA
    3e4239c View commit details
  9. Copy the full SHA
    de3674a View commit details

Commits on May 30, 2022

  1. ready the package

    ducksandgoats committed May 30, 2022
    Copy the full SHA
    246b3ac View commit details
  2. update torrentz

    ducksandgoats committed May 30, 2022
    Copy the full SHA
    e3d7e54 View commit details

Commits on Jun 1, 2022

  1. update torrentz

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    9e25bec View commit details
  2. update torrentz

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    ec53bbe View commit details
  3. handle infohash

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    864b29e View commit details
  4. handle infohash

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    77002d1 View commit details
  5. update

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    9f095de View commit details
  6. update

    ducksandgoats committed Jun 1, 2022
    Copy the full SHA
    7b698fb View commit details

Commits on Jun 4, 2022

  1. update url

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    add0015 View commit details
  2. update package

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    775e5f5 View commit details
  3. update package

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    2b2e289 View commit details
  4. update package

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    2d71c17 View commit details
  5. update package

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    d21470a View commit details
  6. update list-fetch

    ducksandgoats committed Jun 4, 2022
    Copy the full SHA
    b0e213b View commit details

Commits on Jun 5, 2022

  1. update package

    ducksandgoats committed Jun 5, 2022
    Copy the full SHA
    622d6bf View commit details
  2. update package

    ducksandgoats committed Jun 5, 2022
    Copy the full SHA
    40f6392 View commit details
  3. update package

    ducksandgoats committed Jun 5, 2022
    Copy the full SHA
    7512d48 View commit details
  4. update package

    ducksandgoats committed Jun 5, 2022
    Copy the full SHA
    10958a1 View commit details

Commits on Jun 6, 2022

  1. update package

    ducksandgoats committed Jun 6, 2022
    Copy the full SHA
    3166507 View commit details

Commits on Jun 7, 2022

  1. update package

    ducksandgoats committed Jun 7, 2022
    Copy the full SHA
    104093e View commit details

Commits on Jun 9, 2022

  1. add change

    ducksandgoats committed Jun 9, 2022
    Copy the full SHA
    fe77e50 View commit details
  2. add change

    ducksandgoats committed Jun 9, 2022
    Copy the full SHA
    5d626cb View commit details

Commits on Jun 10, 2022

  1. update package

    ducksandgoats committed Jun 10, 2022
    Copy the full SHA
    647b8a9 View commit details
  2. update package

    ducksandgoats committed Jun 10, 2022
    Copy the full SHA
    c6282d3 View commit details
  3. update packages

    ducksandgoats committed Jun 10, 2022
    Copy the full SHA
    b9994e3 View commit details
  4. update packages

    ducksandgoats committed Jun 10, 2022
    Copy the full SHA
    b4c783a View commit details

Commits on Jun 11, 2022

  1. update package

    ducksandgoats committed Jun 11, 2022
    Copy the full SHA
    515ced7 View commit details

Commits on Jun 12, 2022

  1. update torrentz

    ducksandgoats committed Jun 12, 2022
    Copy the full SHA
    6b55f21 View commit details
  2. update torrentz

    ducksandgoats committed Jun 12, 2022
    Copy the full SHA
    647dd74 View commit details
  3. update torrentz

    ducksandgoats committed Jun 12, 2022
    Copy the full SHA
    ea39f80 View commit details

Commits on Jun 13, 2022

  1. update package

    ducksandgoats committed Jun 13, 2022
    Copy the full SHA
    c9629b6 View commit details

Commits on Jun 15, 2022

  1. update package

    ducksandgoats committed Jun 15, 2022
    Copy the full SHA
    47eb69a View commit details
  2. update package

    ducksandgoats committed Jun 15, 2022
    Copy the full SHA
    9e6b3c1 View commit details
  3. update package

    ducksandgoats committed Jun 15, 2022
    Copy the full SHA
    f86667a View commit details

Commits on Jun 17, 2022

  1. update package

    ducksandgoats committed Jun 17, 2022
    Copy the full SHA
    ef476e4 View commit details

Commits on Jun 25, 2022

  1. update torrentz

    ducksandgoats committed Jun 25, 2022
    Copy the full SHA
    2a298dc View commit details
Showing with 404 additions and 151 deletions.
  1. +72 −2 README.md
  2. +324 −140 index.js
  3. +8 −9 package.json
74 changes: 72 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,73 @@
# Bittorrent-Fetch
# list-fetch

coming soon
example of how a url looks like using list-fetch
`bt://someAddressOrInfohashAsHostname/some/path`

`_` - does different things depending on the http method
`address` - a public key that is 64 characters that is used as an address
`infohash` - a identifier for torrents that is 40 characters

method: `HEAD` - does not return a body, only returns headers<br>
hostname:

- `_` - user's own data<br>
path:
- `/` - if path is `/` then it returns data about the current torrents, if no headers are used, then it returns the byte size and count of all of the authored torrents<br>
headers:
- `X-Data` - `true` | `false` - if true, it returns the byte size and count of all the torrents, if false, it returns only the count of all the torrents<br>
- `/path/to/dir/or/file` - if path is not `/` then it returns data in the headers about the user directory that is local and not publically shared<br>
- `address` | `infohash` - a torrent you want to load<br>
path:
- `/any/path/to/dir/or/file` - it can be any path including `/`, if no headers, it returns the byte size, link, and other data of the torrent in the headers<br>
headers:
- `X-Copy` - `true` | `false` - if true, copies a file and saves it to the user directory(with the address or infohash as the directory name, it is publically shared) on the local disk, if false, copies a file and saves it to the user directory(it is publically shared) on the local disk<br>
- `X-Timer` - `String` - a number for a timeout<br>

method: `GET` - return a body<br>
hostname:

- `_` - user's own data<br>
path:
- `/` - if path is `/` then it is same as `HEAD`, in addition, it also sends a body. if there are no headers, then only author data is returned<br>
headers:
- `X-Data` - `true` | `false` - if true, it returns the byte size and count of all the torrents, if false, it returns only the count of all the torrents<br>
- `/path/to/dir/or/file` - if path is not `/` then it is the same as `HEAD`, in addition, it also sends a body<br>
- `address` | `infohash` - a torrent you want to load<br>
path:
- `/any/path/to/dir/or/file` - it can be any path including `/`, if no headers, it returns the byte size, link, and other data in the headers<br>
headers:
- `Range` - if path is a file, it returns the data from a file that fits this range<br>
- `X-Timer` - `String` - a number for a timeout<br>

method: `POST` - return a body<br>
hostname:

- `_` - make a new torrent<br>
path:
- `/path/to/dir/or/file` - any path, this is where the files will go for the torrent<br>
body:
- `FormData` | `String` - either FormData which will hold the files or some string for a single file<br>
headers:
- `X-Update` - `true` | `false` - if true, a mutable BEP46 torrent, if false, an immutable regular torrent<br>
- `X-Version` - `String` - what sequence to use for the torrent<br>
- `X-Opt` - `String` - options to use for the content, stringified object<br>
- `address` | `infohash` - an already existing torrent that you want to modify<br>
path:
- `/path/to/dir/or/file` - any path, this is where the files will go for the torrent<br>
body:
- `FormData` | `String` - either FormData which will hold the files or some string for a single file<br>
headers:
- `X-Version` - `String` - what sequence to use for the torrent<br>
- `X-Opt` - `String` - options to use for the content, stringified object<br>

method: `DELETE` - returns a body<br>
hostname:

- `_` - delete user directory data<br>
path:
- `/path/to/dir/or/file` - any path, this is where the files will go for the torrent<br>
- `address` | `infohash` - an already existing torrent to delete entirely or modify<br>
path:
- `/path/to/dir/or/file` - any path, if `/` then entire torrent is delete, if not `/`, then only the path is deleted and a new torrent is made
headers:
- `X-Opt` - `String` - options to use for the content, stringified object<br>
464 changes: 324 additions & 140 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,190 +1,374 @@
const makeFetch = require('make-fetch')
const streamToIterator = require('stream-async-iterator')
const mime = require('mime/lite')
const parseRange = require('range-parser')
const Torrentz = require('torrentz')

const checkHash = /^[a-fA-F0-9]{40}$/
const checkAddress = /^[a-fA-F0-9]{64}$/
const checkTitle = /^[a-zA-Z0-9]/
// const DEFAULT_OPTS = {}

module.exports = function makeBTFetch (opts = {}) {
// const finalOpts = { ...DEFAULT_OPTS, ...opts }
const SUPPORTED_METHODS = ['GET', 'PUT', 'DELETE', 'HEAD']
module.exports = async function makeBTFetch (opts = {}) {
const {makeRoutedFetch} = await import('make-fetch')
const fs = require('fs')
const {fetch, router} = makeRoutedFetch({onNotFound: handleEmpty, onError: handleError})
// const streamToIterator = require('stream-async-iterator')
const mime = require('mime/lite')
const parseRange = require('range-parser')
const Torrentz = require('torrentz')
const path = require('path')

const DEFAULT_OPTS = {}
const finalOpts = { ...DEFAULT_OPTS, ...opts }
const checkHash = /^[a-fA-F0-9]{40}$/
const checkAddress = /^[a-fA-F0-9]{64}$/
// const SUPPORTED_METHODS = ['GET', 'POST', 'DELETE', 'HEAD']
const hostType = '_'
const btTimeout = 30000

const app = new Torrentz(opts)
const app = await new Promise((resolve) => {if(finalOpts.torrentz){resolve(finalOpts.torrentz)}else{resolve(new Torrentz(finalOpts))}})

// const prog = new Map()

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 handleFormData(formdata) {
const arr = []
for (const [name, info] of formdata) {
if (name === 'file') {
arr.push(info)
}
}
return arr
}

function takeCareOfIt(data){
console.log(data)
throw new Error('aborted')
}

function sendTheData(theSignal, theData){
if(theSignal){
theSignal.removeEventListener('abort', takeCareOfIt)
}
return theData
}

function htmlIden(data){
if(data.address){
data.link = `<a href='bt://${data.address}/'>${data.address}</a>`
} else if(data.infohash){
data.link = `<a href='bt://${data.infohash}/'>${data.infohash}</a>`
}
return `<p>${JSON.stringify(data)}</p>`
}

function jsonIden(data){
if(data.address){
data.link = `bt://${data.address}/`
} else if(data.infohash){
data.link = `bt://${data.infohash}/`
}
return data
}

function getMimeType (path) {
let mimeType = mime.getType(path) || 'text/plain'
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
return mimeType
}

function formatReq (hostname, pathname) {
function formatReq (hostname, pathname, extra) {

// let mainType = hostname[0] === hostType || hostname[0] === sideType ? hostname[0] : ''
const mainQuery = hostname[0] === hostType ? hostname[0] : ''
const mainHost = hostname.replace(mainQuery, '')
// if(pathname){
// console.log(decodeURIComponent(pathname))
// }
const mainQuery = hostname === hostType ? true : false
const mainHost = hostname
const mainId = {}
if(!mainQuery){
if(checkAddress.test(mainHost)){
mainId.address = mainHost
mainId.secret = extra
} else if(checkHash.test(mainHost)){
mainId.infohash = mainHost
} else {
throw new Error('identifier is invalid')
}
}

const mainPath = decodeURIComponent(pathname)
return { mainQuery, mainHost, mainPath }
const mainLink = `bt://${mainHost}${mainPath.includes('.') ? mainPath : mainPath + '/'}`
return { mainQuery, mainHost, mainPath, mainId, mainLink }
}

const fetch = makeFetch(async request => {
// if (request.body !== null) {
// request.body = await getBody(request.body)
// try {
// request.body = JSON.parse(request.body)
// } catch (error) {
// console.log(error)
// }
// }

const { url, method, headers: reqHeaders, body } = request

try {
const { hostname, pathname, protocol, searchParams } = new URL(url)

if (protocol !== 'bt:') {
return { statusCode: 409, headers: {}, data: ['wrong protocol'] }
} else if (!method || !SUPPORTED_METHODS.includes(method)) {
return { statusCode: 409, headers: {}, data: ['something wrong with method'] }
} else if ((!hostname) || (hostname.length === 1 && hostname !== hostType) || (hostname.length !== 1 && !checkTitle.test(hostname) && !checkHash.test(hostname) && !checkAddress.test(hostname))) {
return { statusCode: 409, headers: {}, data: ['something wrong with hostname'] }
}
async function handleHead(request) {
const { url, method, headers: reqHeaders, body, signal } = request

if(signal){
signal.addEventListener('abort', takeCareOfIt)
}

const { hostname, pathname, protocol, search, searchParams } = new URL(url)

const mid = formatReq(hostname, pathname)
const mid = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname), reqHeaders.get('x-authentication'))

if(method === 'HEAD'){
if (mid.mainQuery) {
return { statusCode: 400, headers: {'Content-Length': '0'}, data: [] }
// const mainReq = !reqHeaders.accept || !reqHeaders.accept.includes('application/json')
// const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
if (mid.mainQuery) {
if(mid.mainPath === '/'){
if(reqHeaders.has('x-data') || searchParams.has('x-data')){
const torrentData = await app.torrentData(JSON.parse(reqHeaders.get('x-data') || searchParams.get('x-data')))
const useHeaders = {}
const useCount = torrentData.length
let useLength = 0
for(const test of torrentData){
if(test.length){
useLength = useLength + test.length
}
}
useHeaders['X-Count'] = useCount
if(useLength){
useHeaders['X-Length'] = useLength
}
return sendTheData(signal, {status: 200, headers: useHeaders, body: ''})
} else {
const torrentData = await app.loadTorrent(mid.mainHost)
if (mid.mainPath === '/') {
const torrentData = await app.authorData()
const useHeaders = {}
const useCount = torrentData.length
let useLength = 0
for(const test of torrentData){
useLength = useLength + test.length
}
useHeaders['X-Count'] = useCount
useHeaders['X-Length'] = useLength
return sendTheData(signal, {status: 200, headers: useHeaders, body: ''})
}
} else {
return sendTheData(signal, {status: 400, headers: {'X-Status': 'can not have a path'}, body: ''})
}
} else {
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 : btTimeout }
if (reqHeaders.has('x-copy') || searchParams.has('x-copy')) {
const torrentData = await app.userTorrent(mid.mainId, mid.mainPath, { ...useOpts, id: JSON.parse(reqHeaders.get('x-copy') || searchParams.get('x-copy')) })
return sendTheData(signal, { status: 200, headers: { 'X-Path': torrentData }, body: '' })
} else {
const torrentData = await app.loadTorrent(mid.mainId, mid.mainPath, useOpts)
if (torrentData) {
if (Array.isArray(torrentData)) {
const useHeaders = { 'Content-Length': 0, 'X-Downloaded': 0, 'X-Link': `bt://${mid.mainHost}${mid.mainPath}` }
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
torrentData.forEach((data) => {
useHeaders['Content-Length'] = useHeaders['Content-Length'] + data.length
useHeaders['X-Downloaded'] = useHeaders['X-Downloaded'] + data.downloaded
})

return sendTheData(signal, { status: 200, headers: useHeaders, body: '' })
} else if(torrentData.createReadStream){
const useHeaders = {}
useHeaders['Content-Type'] = mid.mainRes
useHeaders['Content-Type'] = getMimeType(torrentData.path)
useHeaders['Content-Length'] = `${torrentData.length}`
useHeaders['Accept-Ranges'] = 'bytes'
useHeaders['X-Downloaded'] = `${torrentData.downloaded}`
return {statusCode: 200, headers: useHeaders, data: []}
useHeaders['X-Link'] = `bt://${mid.mainHost}${mid.mainPath}`
useHeaders['Link'] = `<bt://${useHeaders['X-Link']}>; rel="canonical"`

return sendTheData(signal, {status: 200, headers: useHeaders, body: ''})
} else {
const foundFile = torrentData.files.find(file => { return mid.mainPath === file.urlPath })
if (foundFile) {
const useHeaders = {}
useHeaders['Content-Type'] = getMimeType(mid.mainPath)
useHeaders['Content-Length'] = `${foundFile.length}`
useHeaders['Accept-Ranges'] = 'bytes'
useHeaders['X-Downloaded'] = `${foundFile.downloaded}`
return {statusCode: 200, headers: useHeaders, data: []}
} else {
return {statusCode: 400, headers: {'Content-Length': '0'}, data: []}
}
return sendTheData(signal, { status: 400, headers: { 'X-Error': 'did not find any data' }, body: '' })
}
} else {
return sendTheData(signal, {status: 400, headers: {'X-Error': 'did not find any data'}, body: ''})
}
} else if(method === 'GET'){
const mainRange = reqHeaders.Range || reqHeaders.range
const mainReq = reqHeaders.accept && reqHeaders.accept.includes('text/html')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
if (mid.mainQuery) {
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? ['<html><head><title>Bittorrent-Fetch</title></head><body><div><p>Thank you for using Bittorrent-Fetch-Fetch</p></div></body></html>'] : [JSON.stringify('Thank you for using BT-Fetch')]}
}
}
}

async function handleGet(request) {
const { url, method, headers: reqHeaders, body, signal } = request

if(signal){
signal.addEventListener('abort', takeCareOfIt)
}

const { hostname, pathname, protocol, search, searchParams } = new URL(url)

const mid = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname), reqHeaders.get('x-authentication'))

const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'

if (mid.mainQuery) {
if(mid.mainPath === '/'){
if(reqHeaders.has('x-data') || searchParams.has('x-data')){
const torrentData = await app.torrentData(JSON.parse(reqHeaders.get('x-data') || searchParams.get('x-data')))
return sendTheData(signal, {status: 200, headers: {'Content-Type': mainRes}, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div>${torrentData.map(htmlIden)}</div></body></html>` : JSON.stringify(torrentData.map(jsonIden))})
} else {
const torrentData = await app.loadTorrent(mid.mainHost, reqHeaders['x-timer'] && reqHeaders['x-timer'] !== '0' ? Number(reqHeaders['x-timer']) * 1000 : 0)
let foundFile = null
if (mid.mainPath === '/') {
return {statusCode: 200, headers: {'Content-Type': mainRes, 'Content-Length': String(torrentData.length)}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div>${torrentData.files.map(file => { return `<p><a href="${file.urlPath}">${file.name}</a></p>` })}</div></body></html>`] : [JSON.stringify(torrentData.files.map(file => { return `${file.urlPath}` }))]}
} else {
foundFile = torrentData.files.find(file => { return mid.mainPath === file.urlPath })
if (foundFile) {
if (mainRange) {
const ranges = parseRange(foundFile.length, mainRange)
if (ranges && ranges.length && ranges.type === 'bytes') {
const [{ start, end }] = ranges
const length = (end - start + 1)

return {statusCode: 206, headers: {'Content-Length': `${length}`, 'Content-Range': `bytes ${start}-${end}/${foundFile.length}`, 'Content-Type': getMimeType(mid.mainPath)}, data: streamToIterator(foundFile.createReadStream({ start, end }))}
} else {
return {statusCode: 400, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>could not find partial contect for ${foundFile.name}</p></div></body></html>`] : [JSON.stringify(`could not find partial contect for ${foundFile.name}`)]}
}
} else {
return {statusCode: 200, headers: {'Content-Type': getMimeType(mid.mainPath), 'Content-Length': String(foundFile.length)}, data: streamToIterator(foundFile.createReadStream())}
}
} else {
return {statusCode: 400, headers: mainRes, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>could not find the file</p></div></body></html>`] : [JSON.stringify('could not find the file')]}
}
}
const torrentData = await app.authorData()
return sendTheData(signal, {status: 200, headers: {'Content-Type': mainRes}, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div>${torrentData.map(htmlIden)}</div></body></html>` : JSON.stringify(torrentData.map(jsonIden))})
}
} else if(method === 'PUT'){
const mainReq = reqHeaders.accept && reqHeaders.accept.includes('text/html')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
const count = reqHeaders['x-version'] && !isNaN(reqHeaders['x-version']) ? Number(reqHeaders['x-version']) : null
if (mid.mainQuery) {
if ((!reqHeaders['x-update']) || (reqHeaders['x-update'] !== 'true' && reqHeaders['x-update'] !== 'false') || (reqHeaders['x-update'] === 'false' && !reqHeaders['x-title']) || (!reqHeaders['content-type'] || !reqHeaders['content-type'].includes('multipart/form-data')) || ((reqHeaders['x-empty']) && (reqHeaders['x-empty'] !== 'false' && reqHeaders['x-empty'] !== 'true')) || !body) {
return {statusCode: 400, headers: {'Content-Type': mainRes}, data: mainReq ? ['<html><head><title>Bittorrent-Fetch</title></head><body><div><p>must have X-Update header which must be set to true or false, must have Content-Type header set to multipart/form-data, must have body, also must have X-Title header for non-BEP46 torrents</p></div></body></html>'] : [JSON.stringify('must have X-Update header which must be set to true or false, must have Content-Type header set to multipart/form-data, must have body, also must have X-Title header for non-BEP46 torrents')]}
} else {
const update = JSON.parse(reqHeaders['x-update'])
// const torrentData = await app.publishTorrent(update, null, count, reqHeaders, body)
if(update){
const torrentData = await app.publishTorrent(update, null, count, reqHeaders, body, reqHeaders['x-timer'] && reqHeaders['x-timer'] !== '0' ? Number(reqHeaders['x-timer']) * 1000 : 0, reqHeaders['x-empty'] ? JSON.parse(reqHeaders['x-empty']) : null)
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>address: ${torrentData.address}</p><p>secret: ${torrentData.secret}</p></div></body></html>`] : [JSON.stringify({ address: torrentData.address, secret: torrentData.secret })]}
} else {
return sendTheData(signal, { status: 400, headers: mainRes, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><p>can not have a path</p></div></body></html>` : JSON.stringify('can not have a path') })
}
} else {
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 : btTimeout }
const torrentData = await app.loadTorrent(mid.mainId, mid.mainPath, useOpts)
if(torrentData){
if (Array.isArray(torrentData)) {
const useHeaders = { 'Content-Length': 0, 'Accept-Ranges': 'bytes', 'X-Downloaded': 0, 'X-Link': `bt://${mid.mainHost}${mid.mainPath}` }
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
torrentData.forEach((data) => {
useHeaders['Content-Length'] = useHeaders['Content-Length'] + data.length
useHeaders['X-Downloaded'] = useHeaders['X-Downloaded'] + data.downloaded
})
useHeaders['Content-Type'] = mainRes
useHeaders['Content-Length'] = String(useHeaders['Content-Length'])
useHeaders['X-Downloaded'] = String(useHeaders['X-Downloaded'])
return sendTheData(signal, {status: 200, headers: useHeaders, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><h1>Directory</h1><p><a href='../'>..</a></p>${torrentData.map(file => { return `<p><a href='${file.urlPath}'>${file.name}</a></p>` })}</div></body></html>` : JSON.stringify(torrentData.map(file => { return file.urlPath }))})
} else if(torrentData.createReadStream){
const mainRange = reqHeaders.has('Range') || reqHeaders.has('range')
if (mainRange) {
const ranges = parseRange(torrentData.length, reqHeaders.get('Range') || reqHeaders.get('range'))
if (ranges && ranges.length && ranges.type === 'bytes') {
const [{ start, end }] = ranges
const length = (end - start + 1)

return sendTheData(signal, {status: 206, headers: {'X-Link': `bt://${mid.mainHost}${mid.mainPath}`, 'Link': `<bt://${mid.mainHost}${mid.mainPath}>; rel="canonical"`, 'Content-Length': `${length}`, 'Content-Range': `bytes ${start}-${end}/${torrentData.length}`, 'Content-Type': getMimeType(torrentData.path)}, body: torrentData.createReadStream({ start, end })})
} else {
const torrentData = await app.publishTorrent(update, {title: reqHeaders['x-title']}, count, reqHeaders, body, reqHeaders['x-timer'] && reqHeaders['x-timer'] !== '0' ? Number(reqHeaders['x-timer']) * 1000 : 0, reqHeaders['x-empty'] ? JSON.parse(reqHeaders['x-empty']) : null)
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>infohash: ${torrentData.infohash}</p><p>title: ${torrentData.title}</p></div></body></html>`] : [JSON.stringify({ hash: torrentData.hash, title: torrentData.title })]}
return sendTheData(signal, {status: 416, headers: {'Content-Type': mainRes, 'Content-Length': String(torrentData.length)}, 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 {
if((!reqHeaders['x-update']) || (reqHeaders['x-update'] !== 'true' && reqHeaders['x-update'] !== 'false') || (reqHeaders['x-update'] === 'true' && !reqHeaders['x-authentication']) || (!reqHeaders['content-type'] || !reqHeaders['content-type'].includes('multipart/form-data')) || ((reqHeaders['x-empty']) && (reqHeaders['x-empty'] !== 'false' && reqHeaders['x-empty'] !== 'true')) || !body){
return {statusCode: 400, headers: {'Content-Type': mainRes}, data: mainReq ? ['<html><head><title>Bittorrent-Fetch</title></head><body><div><p>must have X-Update header which must be set to true or false, must have Content-Type header set to multipart/form-data, must have body, also must have X-Authentication header for BEP46 torrents</p></div></body></html>'] : [JSON.stringify('must have X-Update header which must be set to true or false, must have Content-Type header set to multipart/form-data, must have body')]}
} else {
const update = JSON.parse(reqHeaders['x-update'])
if(update){
const torrentData = await app.publishTorrent(update, {address: mid.mainHost, secret: reqHeaders['x-authentication']}, count, reqHeaders, body, reqHeaders['x-timer'] && reqHeaders['x-timer'] !== '0' ? Number(reqHeaders['x-timer']) * 1000 : 0, reqHeaders['x-empty'] ? JSON.parse(reqHeaders['x-empty']) : null)
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>address: ${torrentData.address}</p><p>secret: ${torrentData.secret}</p></div></body></html>`] : [JSON.stringify({ address: torrentData.address, secret: torrentData.secret })]}
} else {
const torrentData = await app.publishTorrent(update, {sub: reqHeaders['x-substitution'], title: mid.mainHost}, count, reqHeaders, body, reqHeaders['x-timer'] && reqHeaders['x-timer'] !== '0' ? Number(reqHeaders['x-timer']) * 1000 : 0, reqHeaders['x-empty'] ? JSON.parse(reqHeaders['x-empty']) : null)
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>${torrentData.name}</title></head><body><div><p>infohash: ${torrentData.infohash}</p><p>title: ${torrentData.title}</p></div></body></html>`] : [JSON.stringify({ hash: torrentData.hash, title: torrentData.title })]}
}
return sendTheData(signal, {status: 200, headers: {'Content-Type': getMimeType(torrentData.path), 'X-Link': `bt://${mid.mainHost}${mid.mainPath}`, 'Link': `<bt://${mid.mainHost}${mid.mainPath}>; rel="canonical"`, 'Content-Length': String(torrentData.length)}, body: torrentData.createReadStream()})
}
}
} else if(method === 'DELETE'){
const mainReq = reqHeaders.accept && reqHeaders.accept.includes('text/html')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
if (mid.mainQuery) {
return {statusCode: 400, headers: {'Content-Type': mainRes}, data: mainReq ? ['<html><head><title>Bittorrent-Fetch</title></head><body><div><p>must not use underscore</p></div></body></html>'] : [JSON.stringify('must not use udnerscore')]}
} else {
const torrentData = await app.shredTorrent(mid.mainHost)
return {statusCode: 200, headers: {'Content-Type': mainRes}, data: mainReq ? [`<html><head><title>Bittorrent-Fetch</title></head><body><div><p>${torrentData} was shredded</p></div></body></html>`] : [JSON.stringify(`${torrentData} was shredded`)]}
return sendTheData(signal, { status: 400, headers: { 'Content-Type': mainRes }, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><p>could not find the data</p></div></body></html>` : JSON.stringify('could not find the data') })
}
} else {
const mainReq = reqHeaders.accept && reqHeaders.accept.includes('text/html')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'
return { statusCode: 400, headers: { 'Content-Type': mainRes }, data: mainReq ? ['<html><head><title>Bittorrent-Fetch</title></head><body><div><p>method is not supported</p></div></body></html>'] : [JSON.stringify('method is not supported')] }
return sendTheData(signal, {status: 400, headers: {'Content-Type': mainRes}, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><p>could not find the data</p></div></body></html>` : JSON.stringify('could not find the data')})
}
} catch (e) {
if(e.name === 'ErrorTimeout'){
return { statusCode: 408, headers: {}, data: [e.stack] }
}
}

async function handlePost(request) {
const { url, method, headers: reqHeaders, body, signal } = request

if(signal){
signal.addEventListener('abort', takeCareOfIt)
}

const { hostname, pathname, protocol, search, searchParams } = new URL(url)

const mid = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname), reqHeaders.get('x-authentication'))

const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'

if (mid.mainQuery) {
if (reqHeaders.has('x-update') || searchParams.has('x-update')) {
if (JSON.parse(reqHeaders.get('x-update') || searchParams.get('x-update'))) {
mid.mainId = { address: null, secret: null }
} else {
mid.mainId = { infohash: null }
}
} else {
return { statusCode: 500, headers: {}, data: [e.stack] }
return sendTheData(signal, { status: 400, headers: mainRes, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><p>invalid data</p></div></body></html>` : JSON.stringify('invalid data') })
}
}

const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
const useOpts = {
...useOpt,
seq: reqHeaders.has('x-version') || searchParams.has('x-version') ? Number(reqHeaders.get('x-version') || searchParams.get('x-version')) : null,
}
const useBody = reqHeaders.has('content-type') && reqHeaders.get('content-type').includes('multipart/form-data') ? handleFormData(await request.formData()) : body
const torrentData = await app.publishTorrent(mid.mainId, mid.mainPath, useBody, useOpts)
const useHeaders = {}
for (const test of ['sequence', 'name', 'infohash', 'dir', 'pair', 'secret', 'address']) {
if (torrentData[test] || typeof(torrentData[test]) === 'number') {
useHeaders['X-' + test.charAt(0).toUpperCase() + test.slice(1)] = torrentData[test]
}
}
const useIden = torrentData.address || torrentData.infohash
torrentData.saved = 'bt://' + path.join(useIden, torrentData.saved).replace(/\\/g, '/')
useHeaders['X-Link'] = `bt://${useIden}${torrentData.path}`
useHeaders['Link'] = `<${useHeaders['X-Link']}>; rel="canonical"`
return sendTheData(signal, { status: 200, headers: { 'Content-Length': String(torrentData.length), 'Content-Type': mainRes, ...useHeaders }, body: mainReq ? `<html><head><title>${useIden}</title></head><body><div>${JSON.stringify(torrentData.saved)}</div></body></html>` : JSON.stringify(torrentData.saved) })
}

async function handleDelete(request) {
const { url, method, headers: reqHeaders, body, signal } = request

if(signal){
signal.addEventListener('abort', takeCareOfIt)
}
})

fetch.destroy = () => {
return new Promise((resolve, reject) => {
const { hostname, pathname, protocol, search, searchParams } = new URL(url)

const mid = formatReq(decodeURIComponent(hostname), decodeURIComponent(pathname), reqHeaders.get('x-authentication'))

const mainReq = !reqHeaders.has('accept') || !reqHeaders.get('accept').includes('application/json')
const mainRes = mainReq ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8'

if (mid.mainQuery) {
return sendTheData(signal, { status: 400, headers: mainRes, body: mainReq ? `<html><head><title>${mid.mainLink}</title></head><body><div><p>invalid query</p></div></body></html>` : JSON.stringify('invalid query') })
}
const useOpt = reqHeaders.has('x-opt') || searchParams.has('x-opt') ? JSON.parse(reqHeaders.get('x-opt') || decodeURIComponent(searchParams.get('x-opt'))) : {}
const useOpts = {
...useOpt,
count: reqHeaders.has('x-version') || searchParams.has('x-version') ? Number(reqHeaders.get('x-version') || searchParams.get('x-version')) : null
}
const torrentData = await app.shredTorrent(mid.mainId, mid.mainPath, useOpts)
const useHead = {}
for (const test of ['id', 'path', 'infohash', 'dir', 'name', 'sequence', 'pair', 'address', 'secret']) {
if (torrentData[test]) {
useHead['X-' + test.charAt(0).toUpperCase() + test.slice(1)] = torrentData[test]
}
}
const useIden = torrentData.address || torrentData.infohash || torrentData.id
const useLink = `bt://${torrentData.id}${torrentData.path}`
useHead['X-Link'] = `bt://${useIden}${torrentData.path}`
useHead['Link'] = `<${useHead['X-Link']}>; rel="canonical"`

return sendTheData(signal, {status: 200, headers: {'Content-Type': mainRes, ...useHead}, body: mainReq ? `<html><head><title>${useIden}</title></head><body><div>${useLink}</div></body></html>` : JSON.stringify(useLink)})
}

router.head('bt://*/**', handleHead)
router.get('bt://*/**', handleGet)
router.post('bt://*/**', handlePost)
router.delete('bt://*/**', handleDelete)

fetch.close = async () => {
for (const data of app.webtorrent.torrents) {
await new Promise((resolve, reject) => {
data.destroy({ destroyStore: false }, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
app.checkId.clear()
clearInterval(app.session)
return await new Promise((resolve, reject) => {
app.webtorrent.destroy(error => {
if (error) {
reject(error)
} else {
clearInterval(app.updateRoutine)
resolve()
}
})
})
}

return fetch
}
}
17 changes: 8 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
{
"name": "bittorrent-fetch",
"version": "1.2.0",
"name": "list-fetch",
"version": "31.0.9",
"description": "coming soon",
"main": "index.js",
"scripts": {
"test": "node run.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/resession/bittorrent-fetch.git"
"url": "git+https://github.com/ducksandgoats/list-fetch.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "ducksandgoats",
"bugs": {
"url": "https://github.com/resession/bittorrent-fetch/issues"
"url": "https://github.com/ducksandgoats/list-fetch/issues"
},
"homepage": "https://github.com/resession/bittorrent-fetch#readme",
"homepage": "https://github.com/ducksandgoats/list-fetch#readme",
"dependencies": {
"make-fetch": "^2.3.1",
"make-fetch": "github:ducksandgoats/make-fetch",
"mime": "^3.0.0",
"range-parser": "^1.2.1",
"stream-async-iterator": "^2.0.0",
"torrentz": "^1.1.0"
"torrentz": "^12.0.6"
},
"devDependencies": {
"form-data": "^4.0.0",