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/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 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<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> {
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 {

@ -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<Props> = ({ roomName }: Props) => {
const room = useRef<Room | null>(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<Props> = ({ uploaderPeerID }: Props) => {
// const room = useRef<Room | null>(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
}

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

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

@ -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<Channel>(
(req: NextApiRequest, _res: NextApiResponse): Promise<Channel> => {
const uploaderPeerID = getBodyKey(req, 'uploaderPeerID')
return channelRepo.create(uploaderPeerID)
},
)

@ -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<void>(
(req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
const slug = getBodyKey(req, 'slug')
return channelRepo.destroy(slug)
},
)

@ -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<void>(
(req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
const slug = getBodyKey(req, 'slug')
return channelRepo.renew(slug)
},
)

@ -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<Props> = ({ slug, uploaderPeerID }) => {
return (
<WebRTCProvider>
<>
<div>{JSON.stringify(slug)}</div>
<Downloader roomName="my-room" />
<div>{slug}</div>
<div>{uploaderPeerID}</div>
<Downloader uploaderPeerID={uploaderPeerID} />
</>
</WebRTCProvider>
)
}
DownloadPage.getInitialProps = () => {
return {}
export const getServerSideProps: GetServerSideProps<Props> = 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

@ -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
export function formatSize(bytes) {
export const formatSize = (bytes: number): string => {
if (bytes === 0) {
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