diff --git a/next-env.d.ts b/next-env.d.ts
index 7b7aa2c..db98b80 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,2 +1,4 @@
///
///
+
+declare module 'xkcd-password'
diff --git a/src/bootstrap.ts b/src/bootstrap.ts
deleted file mode 100644
index 9ad59f1..0000000
--- a/src/bootstrap.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-const express = require('express')
-const db = require('../db')
-
-const routes = module.exports = new express.Router()
-
-function bootstrap(uploader, req, res, next) {
- if (uploader) {
- res.locals.data = {
- DownloadStore: {
- status: 'ready',
- token: uploader.token,
- fileSize: uploader.fileSize,
- fileName: uploader.fileName,
- fileType: uploader.fileType,
- infoHash: uploader.infoHash,
- },
- }
-
- next()
- } else {
- const err = new Error('Not Found')
- err.status = 404
- next(err)
- }
-}
-
-routes.get(/^\/([a-z]+\/[a-z]+\/[a-z]+\/[a-z]+)$/, (req, res, next) => {
- const uploader = db.find(req.params[0])
- return bootstrap(uploader, req, res, next)
-})
-routes.get(/^\/download\/(\w+)$/, (req, res, next) => {
- const uploader = db.findShort(req.params[0])
- return bootstrap(uploader, req, res, next)
-})
diff --git a/src/channel.ts b/src/channel.ts
index 104f6a9..6970c2e 100644
--- a/src/channel.ts
+++ b/src/channel.ts
@@ -1,5 +1,6 @@
import config from './config'
import Redis from 'ioredis'
+import { generateShortSlug, generateLongSlug } from './slugs'
export type Channel = {
uploaderPeerID: string
@@ -76,11 +77,27 @@ export class RedisChannelRepo implements ChannelRepo {
}
private async generateShortSlug(): Promise {
- return 'foo' // TODO
+ for (let i = 0; i < config.shortSlug.maxAttempts; i++) {
+ const slug = generateShortSlug()
+ const currVal = await this.client.get(this.getShortSlugKey(slug))
+ if (!currVal) {
+ return slug
+ }
+ }
+
+ throw new Error('max attempts reached generating short slug')
}
private async generateLongSlug(): Promise {
- return 'foo/bar/baz' // TODO
+ for (let i = 0; i < config.longSlug.maxAttempts; i++) {
+ const slug = await generateLongSlug()
+ const currVal = await this.client.get(this.getLongSlugKey(slug))
+ if (!currVal) {
+ return slug
+ }
+ }
+
+ throw new Error('max attempts reached generating long slug')
}
private getShortSlugKey(shortSlug: string): string {
diff --git a/src/components/Downloader.tsx b/src/components/Downloader.tsx
index 011c498..540fdfc 100644
--- a/src/components/Downloader.tsx
+++ b/src/components/Downloader.tsx
@@ -1,56 +1,54 @@
-import React, { useEffect, useRef } from 'react'
-import { usePeerData } from 'react-peer-data'
-import { Room } from 'peer-data'
+import React from 'react'
interface Props {
- roomName: string
+ uploaderPeerID: string
}
-const Downloader: React.FC = ({ roomName }: Props) => {
- const room = useRef(null)
- const peerData = usePeerData()
-
- useEffect(() => {
- if (room.current) return
-
- room.current = peerData.connect(roomName)
- console.log(room.current)
- setInterval(() => console.log(room.current), 1000)
- room.current
- .on('participant', (participant) => {
- console.log(participant.getId() + ' joined')
- participant.newDataChannel()
-
- participant
- .on('connected', () => {
- console.log('connected', participant.id)
- })
- .on('disconnected', () => {
- console.log('disconnected', participant.id)
- })
- .on('track', (event) => {
- console.log('stream', participant.id, event.streams[0])
- })
- .on('message', (payload) => {
- console.log(participant.id, payload)
- })
- .on('error', (event) => {
- console.error('peer', participant.id, event)
- participant.renegotiate()
- })
-
- console.log(participant)
- participant.send(`hello there, I'm the downloader`)
- })
- .on('error', (event) => {
- console.error('room', roomName, event)
- })
-
- return () => {
- room.current.disconnect()
- room.current = null
- }
- }, [peerData])
+const Downloader: React.FC = ({ uploaderPeerID }: Props) => {
+ // const room = useRef(null)
+ // const peerData = usePeerData()
+
+ // useEffect(() => {
+ // if (room.current) return
+
+ // room.current = peerData.connect(roomName)
+ // console.log(room.current)
+ // setInterval(() => console.log(room.current), 1000)
+ // room.current
+ // .on('participant', (participant) => {
+ // console.log(participant.getId() + ' joined')
+ // participant.newDataChannel()
+
+ // participant
+ // .on('connected', () => {
+ // console.log('connected', participant.id)
+ // })
+ // .on('disconnected', () => {
+ // console.log('disconnected', participant.id)
+ // })
+ // .on('track', (event) => {
+ // console.log('stream', participant.id, event.streams[0])
+ // })
+ // .on('message', (payload) => {
+ // console.log(participant.id, payload)
+ // })
+ // .on('error', (event) => {
+ // console.error('peer', participant.id, event)
+ // participant.renegotiate()
+ // })
+
+ // console.log(participant)
+ // participant.send(`hello there, I'm the downloader`)
+ // })
+ // .on('error', (event) => {
+ // console.error('room', roomName, event)
+ // })
+
+ // return () => {
+ // room.current.disconnect()
+ // room.current = null
+ // }
+ // }, [peerData])
return null
}
diff --git a/src/config.ts b/src/config.ts
index 35a6daf..da715e5 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,6 +1,28 @@
+import toppings from './toppings'
+
export default {
redisURL: 'redis://localhost:6379/0',
channel: {
ttl: 60 * 60, // 1 hour
},
+ bodyKeys: {
+ uploaderPeerID: {
+ min: 3,
+ max: 256,
+ },
+ slug: {
+ min: 3,
+ max: 256,
+ },
+ },
+ shortSlug: {
+ numChars: 8,
+ chars: '0123456789abcdefghijklmnopqrstuvwxyz',
+ maxAttempts: 8,
+ },
+ longSlug: {
+ numWords: 4,
+ words: toppings,
+ maxAttempts: 8,
+ },
}
diff --git a/src/db.ts b/src/db.ts
index c781e3d..46d8a82 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -48,9 +48,3 @@ export function find(token) {
export function findShort(shortToken) {
return shortTokens[shortToken.toLowerCase()]
}
-
-export function remove(client) {
- if (client == null) { return }
- delete tokens[client.token]
- delete shortTokens[client.shortToken]
-}
diff --git a/src/pages/api/create.ts b/src/pages/api/create.ts
index f3fa9db..be03de1 100644
--- a/src/pages/api/create.ts
+++ b/src/pages/api/create.ts
@@ -1,19 +1,10 @@
-import type { Request, Response } from 'express'
-import { channelRepo } from '../../channel'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { Channel, channelRepo } from '../../channel'
+import { routeHandler, getBodyKey } from '../../routes'
-export default (req: Request, res: Response): void => {
- // TODO: validate method and uploaderPeerID
-
- channelRepo
- .create(req.body.uploaderPeerID)
- .then((channel) => {
- res.statusCode = 200
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify(channel))
- })
- .catch((err) => {
- res.statusCode = 500
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify({ error: err.toString() }))
- })
-}
+export default routeHandler(
+ (req: NextApiRequest, _res: NextApiResponse): Promise => {
+ const uploaderPeerID = getBodyKey(req, 'uploaderPeerID')
+ return channelRepo.create(uploaderPeerID)
+ },
+)
diff --git a/src/pages/api/destroy.ts b/src/pages/api/destroy.ts
index 3fb6889..e6eb946 100644
--- a/src/pages/api/destroy.ts
+++ b/src/pages/api/destroy.ts
@@ -1,19 +1,10 @@
-import type { Request, Response } from 'express'
+import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel'
+import { routeHandler, getBodyKey } from '../../routes'
-export default (req: Request, res: Response): void => {
- // TODO: validate method and slug
-
- channelRepo
- .destroy(req.body.slug)
- .then((channel) => {
- res.statusCode = 200
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify(channel))
- })
- .catch((err) => {
- res.statusCode = 500
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify({ error: err.toString() }))
- })
-}
+export default routeHandler(
+ (req: NextApiRequest, _res: NextApiResponse): Promise => {
+ const slug = getBodyKey(req, 'slug')
+ return channelRepo.destroy(slug)
+ },
+)
diff --git a/src/pages/api/renew.ts b/src/pages/api/renew.ts
index cf57dc8..61ea8d9 100644
--- a/src/pages/api/renew.ts
+++ b/src/pages/api/renew.ts
@@ -1,19 +1,10 @@
-import type { Request, Response } from 'express'
+import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel'
+import { routeHandler, getBodyKey } from '../../routes'
-export default (req: Request, res: Response): void => {
- // TODO: validate method and slug
-
- channelRepo
- .renew(req.body.slug)
- .then((channel) => {
- res.statusCode = 200
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify(channel))
- })
- .catch((err) => {
- res.statusCode = 500
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify({ error: err.toString() }))
- })
-}
+export default routeHandler(
+ (req: NextApiRequest, _res: NextApiResponse): Promise => {
+ const slug = getBodyKey(req, 'slug')
+ return channelRepo.renew(slug)
+ },
+)
diff --git a/src/pages/download/[...slug].tsx b/src/pages/download/[...slug].tsx
index 269d885..e2f67ea 100644
--- a/src/pages/download/[...slug].tsx
+++ b/src/pages/download/[...slug].tsx
@@ -1,25 +1,49 @@
import React from 'react'
import WebRTCProvider from '../../components/WebRTCProvider'
-import { useRouter } from 'next/router'
import Downloader from '../../components/Downloader'
-import { NextPage } from 'next'
+import { NextPage, GetServerSideProps } from 'next'
+import { channelRepo } from '../../channel'
-const DownloadPage: NextPage = () => {
- const router = useRouter()
- const { slug } = router.query
+type Props = {
+ slug: string
+ uploaderPeerID: string
+ error?: string
+}
+const DownloadPage: NextPage = ({ slug, uploaderPeerID }) => {
return (
<>
- {JSON.stringify(slug)}
-
+ {slug}
+ {uploaderPeerID}
+
>
)
}
-DownloadPage.getInitialProps = () => {
- return {}
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+ const slug = normalizeSlug(ctx.query.slug)
+ const channel = await channelRepo.fetch(slug)
+
+ if (!channel) {
+ ctx.res.statusCode = 404
+ return {
+ props: { slug, uploaderPeerID: '', error: 'not found' },
+ }
+ }
+
+ return {
+ props: { slug, uploaderPeerID: channel.uploaderPeerID },
+ }
+}
+
+const normalizeSlug = (rawSlug: string | string[]): string => {
+ if (typeof rawSlug === 'string') {
+ return rawSlug
+ } else {
+ return rawSlug.join('/')
+ }
}
export default DownloadPage
diff --git a/src/redis.ts b/src/redis.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/routes.ts b/src/routes.ts
new file mode 100644
index 0000000..707dde4
--- /dev/null
+++ b/src/routes.ts
@@ -0,0 +1,53 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import config from './config'
+
+export type APIError = Error & { statusCode?: number }
+
+export type BodyKey = keyof typeof config.bodyKeys
+
+export function throwAPIError(message: string, statusCode = 500): void {
+ const err = new Error(message) as APIError
+ err.statusCode = statusCode
+ throw err
+}
+
+export function routeHandler(
+ fn: (req: NextApiRequest, res: NextApiResponse) => Promise,
+): (req: NextApiRequest, res: NextApiResponse) => Promise {
+ return async (req: NextApiRequest, res: NextApiResponse): Promise => {
+ if (req.method !== 'POST') {
+ res.statusCode = 405
+ res.json({ error: 'method not allowed' })
+ return
+ }
+
+ try {
+ const result = await fn(req, res)
+ res.statusCode = 200
+ res.json(result)
+ } catch (err) {
+ res.statusCode = err.statusCode || 500
+ res.json({ error: err.message })
+ }
+ }
+}
+
+export function getBodyKey(req: NextApiRequest, key: BodyKey): string {
+ const { min, max } = config.bodyKeys[key]
+
+ const val = req.body[key]
+
+ if (typeof val !== 'string') {
+ throwAPIError(`${key} must be a string`)
+ }
+
+ if (val.length < min) {
+ throwAPIError(`${key} must be at least ${min} chars`)
+ }
+
+ if (val.length > max) {
+ throwAPIError(`${key} must be at most ${max} chars`)
+ }
+
+ return val
+}
diff --git a/src/slugs.ts b/src/slugs.ts
new file mode 100644
index 0000000..6da53ab
--- /dev/null
+++ b/src/slugs.ts
@@ -0,0 +1,24 @@
+import xkcdPassword from 'xkcd-password'
+import config from './config'
+
+export const generateShortSlug = (): string => {
+ let result = ''
+ for (let i = 0; i < config.shortSlug.numChars; i++) {
+ result +=
+ config.shortSlug.chars[
+ Math.floor(Math.random() * config.shortSlug.chars.length)
+ ]
+ }
+ return result
+}
+
+const longSlugGenerator = new xkcdPassword()
+longSlugGenerator.initWithWordList(config.longSlug.words)
+
+export const generateLongSlug = (): Promise => {
+ return longSlugGenerator.generate({
+ numWords: config.longSlug.numWords,
+ minLength: 1,
+ maxLength: 256,
+ })
+}
diff --git a/src/util.ts b/src/util.ts
index 7ff4847..30e2b9b 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,10 @@
-// Taken from StackOverflow
+import xkcdPassword from 'xkcd-password'
+import toppings from './toppings'
+import config from './config'
+
+// Borrowed from StackOverflow
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
-export function formatSize(bytes) {
+export const formatSize = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes'
}
diff --git a/src/wt.ts b/src/wt.ts
deleted file mode 100644
index e601f64..0000000
--- a/src/wt.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import socket from 'filepizza-socket'
-
-export function getClient() {
- return new Promise((resolve, reject) => {
- socket.emit('trackerConfig', {}, (trackerConfig) => {
- const client = new WebTorrent({
- tracker: trackerConfig,
- })
- resolve(client)
- })
- })
-}