Begin scaffolding for next.js rewrite

pull/152/head
Alex Kern 5 years ago
parent e70445530d
commit 2b1c473e45
No known key found for this signature in database
GPG Key ID: F3141D5EDF48F89F

2
next-env.d.ts vendored

@ -1,2 +1,4 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/types/global" /> /// <reference types="next/types/global" />
declare module 'xkcd-password'

@ -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)
})

@ -1,5 +1,6 @@
import config from './config' import config from './config'
import Redis from 'ioredis' import Redis from 'ioredis'
import { generateShortSlug, generateLongSlug } from './slugs'
export type Channel = { export type Channel = {
uploaderPeerID: string uploaderPeerID: string
@ -76,11 +77,27 @@ export class RedisChannelRepo implements ChannelRepo {
} }
private async generateShortSlug(): Promise<string> { private async generateShortSlug(): Promise<string> {
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<string> { private async generateLongSlug(): Promise<string> {
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 { private getShortSlugKey(shortSlug: string): string {

@ -1,56 +1,54 @@
import React, { useEffect, useRef } from 'react' import React from 'react'
import { usePeerData } from 'react-peer-data'
import { Room } from 'peer-data'
interface Props { interface Props {
roomName: string uploaderPeerID: string
} }
const Downloader: React.FC<Props> = ({ roomName }: Props) => { const Downloader: React.FC<Props> = ({ uploaderPeerID }: Props) => {
const room = useRef<Room | null>(null) // const room = useRef<Room | null>(null)
const peerData = usePeerData() // const peerData = usePeerData()
useEffect(() => { // useEffect(() => {
if (room.current) return // if (room.current) return
room.current = peerData.connect(roomName) // room.current = peerData.connect(roomName)
console.log(room.current) // console.log(room.current)
setInterval(() => console.log(room.current), 1000) // setInterval(() => console.log(room.current), 1000)
room.current // room.current
.on('participant', (participant) => { // .on('participant', (participant) => {
console.log(participant.getId() + ' joined') // console.log(participant.getId() + ' joined')
participant.newDataChannel() // participant.newDataChannel()
participant // participant
.on('connected', () => { // .on('connected', () => {
console.log('connected', participant.id) // console.log('connected', participant.id)
}) // })
.on('disconnected', () => { // .on('disconnected', () => {
console.log('disconnected', participant.id) // console.log('disconnected', participant.id)
}) // })
.on('track', (event) => { // .on('track', (event) => {
console.log('stream', participant.id, event.streams[0]) // console.log('stream', participant.id, event.streams[0])
}) // })
.on('message', (payload) => { // .on('message', (payload) => {
console.log(participant.id, payload) // console.log(participant.id, payload)
}) // })
.on('error', (event) => { // .on('error', (event) => {
console.error('peer', participant.id, event) // console.error('peer', participant.id, event)
participant.renegotiate() // participant.renegotiate()
}) // })
console.log(participant) // console.log(participant)
participant.send(`hello there, I'm the downloader`) // participant.send(`hello there, I'm the downloader`)
}) // })
.on('error', (event) => { // .on('error', (event) => {
console.error('room', roomName, event) // console.error('room', roomName, event)
}) // })
return () => { // return () => {
room.current.disconnect() // room.current.disconnect()
room.current = null // room.current = null
} // }
}, [peerData]) // }, [peerData])
return null return null
} }

@ -1,6 +1,28 @@
import toppings from './toppings'
export default { export default {
redisURL: 'redis://localhost:6379/0', redisURL: 'redis://localhost:6379/0',
channel: { channel: {
ttl: 60 * 60, // 1 hour 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,
},
} }

@ -48,9 +48,3 @@ export function find(token) {
export function findShort(shortToken) { export function findShort(shortToken) {
return shortTokens[shortToken.toLowerCase()] return shortTokens[shortToken.toLowerCase()]
} }
export function remove(client) {
if (client == null) { return }
delete tokens[client.token]
delete shortTokens[client.shortToken]
}

@ -1,19 +1,10 @@
import type { Request, Response } from 'express' import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel' import { Channel, channelRepo } from '../../channel'
import { routeHandler, getBodyKey } from '../../routes'
export default (req: Request, res: Response): void => { export default routeHandler<Channel>(
// TODO: validate method and uploaderPeerID (req: NextApiRequest, _res: NextApiResponse): Promise<Channel> => {
const uploaderPeerID = getBodyKey(req, 'uploaderPeerID')
channelRepo return channelRepo.create(uploaderPeerID)
.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() }))
})
}

@ -1,19 +1,10 @@
import type { Request, Response } from 'express' import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel' import { channelRepo } from '../../channel'
import { routeHandler, getBodyKey } from '../../routes'
export default (req: Request, res: Response): void => { export default routeHandler<void>(
// TODO: validate method and slug (req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
const slug = getBodyKey(req, 'slug')
channelRepo return channelRepo.destroy(slug)
.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() }))
})
}

@ -1,19 +1,10 @@
import type { Request, Response } from 'express' import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel' import { channelRepo } from '../../channel'
import { routeHandler, getBodyKey } from '../../routes'
export default (req: Request, res: Response): void => { export default routeHandler<void>(
// TODO: validate method and slug (req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
const slug = getBodyKey(req, 'slug')
channelRepo return channelRepo.renew(slug)
.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() }))
})
}

@ -1,25 +1,49 @@
import React from 'react' import React from 'react'
import WebRTCProvider from '../../components/WebRTCProvider' import WebRTCProvider from '../../components/WebRTCProvider'
import { useRouter } from 'next/router'
import Downloader from '../../components/Downloader' import Downloader from '../../components/Downloader'
import { NextPage } from 'next' import { NextPage, GetServerSideProps } from 'next'
import { channelRepo } from '../../channel'
const DownloadPage: NextPage = () => { type Props = {
const router = useRouter() slug: string
const { slug } = router.query uploaderPeerID: string
error?: string
}
const DownloadPage: NextPage<Props> = ({ slug, uploaderPeerID }) => {
return ( return (
<WebRTCProvider> <WebRTCProvider>
<> <>
<div>{JSON.stringify(slug)}</div> <div>{slug}</div>
<Downloader roomName="my-room" /> <div>{uploaderPeerID}</div>
<Downloader uploaderPeerID={uploaderPeerID} />
</> </>
</WebRTCProvider> </WebRTCProvider>
) )
} }
DownloadPage.getInitialProps = () => { export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
return {} 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 export default DownloadPage

@ -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<T>(
fn: (req: NextApiRequest, res: NextApiResponse) => Promise<T>,
): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {
return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
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
}

@ -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<string> => {
return longSlugGenerator.generate({
numWords: config.longSlug.numWords,
minLength: 1,
maxLength: 256,
})
}

@ -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 // 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) { if (bytes === 0) {
return '0 Bytes' return '0 Bytes'
} }

@ -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)
})
})
}
Loading…
Cancel
Save