Just keep going

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

@ -1,16 +1,18 @@
'use strict'; 'use strict';
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', root: true,
parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',
}, },
extends: [ extends: [
'@strv/typescript', "eslint:recommended",
'@strv/typescript/optional', "plugin:@typescript-eslint/eslint-recommended",
'@strv/typescript/style' "plugin:@typescript-eslint/recommended"
], ],
plugins: [ plugins: [
"@typescript-eslint",
"prettier" "prettier"
], ],
rules: { rules: {
@ -25,7 +27,7 @@ module.exports = {
"new-cap": "off", "new-cap": "off",
"no-inline-comments": "off", "no-inline-comments": "off",
"no-shadow": "warn", "no-shadow": "warn",
"no-use-before-define": ["error", { "variables": false }], "no-use-before-define": "off",
"prettier/prettier": "error", "prettier/prettier": "error",
} }
}; };

@ -7,7 +7,7 @@
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "https://github.com/kern/filepizza", "homepage": "https://github.com/kern/filepizza",
"scripts": { "scripts": {
"dev": "next dev", "dev": "node src/server.js",
"build": "next build", "build": "next build",
"start": "next start" "start": "next start"
}, },
@ -21,6 +21,7 @@
"dependencies": { "dependencies": {
"alt": "^0.14.4", "alt": "^0.14.4",
"classnames": "^1.2.0", "classnames": "^1.2.0",
"debug": "^4.2.0",
"express": "^4.12.0", "express": "^4.12.0",
"express-force-ssl": "^0.3.1", "express-force-ssl": "^0.3.1",
"express-winston": "^0.3.1", "express-winston": "^0.3.1",
@ -30,6 +31,8 @@
"nib": "^1.1.0", "nib": "^1.1.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"nodemon": "^1.4.1", "nodemon": "^1.4.1",
"peer-data": "^3.2.5",
"peer-data-server": "^1.0.10",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-frozenhead": "^0.3.0", "react-frozenhead": "^0.3.0",
@ -39,6 +42,7 @@
"react-router": "^0.13.1", "react-router": "^0.13.1",
"socket.io": "^1.3.5", "socket.io": "^1.3.5",
"socket.io-client": "^1.3.5", "socket.io-client": "^1.3.5",
"styled-components": "^5.2.0",
"stylus": "^0.52.4", "stylus": "^0.52.4",
"twilio": "^2.9.1", "twilio": "^2.9.1",
"webrtcsupport": "^2.2.0", "webrtcsupport": "^2.2.0",
@ -46,16 +50,31 @@
"xkcd-password": "^1.2.0" "xkcd-password": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@strv/eslint-config-typescript": "^2.3.0", "@types/debug": "^4.1.5",
"@types/node": "^14.11.1", "@types/node": "^14.11.1",
"@types/react": "^16.9.49", "@types/react": "^16.9.49",
"@types/styled-components": "^5.1.3",
"@typescript-eslint/eslint-plugin": "^4.1.1", "@typescript-eslint/eslint-plugin": "^4.1.1",
"@typescript-eslint/parser": "^4.1.1", "@typescript-eslint/parser": "^4.1.1",
"eslint": "^7.9.0", "eslint": "^7.9.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0", "eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"typescript": "^4.0.3" "typescript": "^4.0.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"git add"
]
} }
} }

@ -0,0 +1,58 @@
import React, { useEffect, useRef } from 'react'
import { usePeerData } from 'react-peer-data'
import { Room } from 'peer-data'
interface Props {
roomName: 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])
return null
}
export default Downloader

@ -1,64 +1,66 @@
import React from 'react' import React, { useState, useRef, useCallback } from 'react'
import styled from 'styled-components'
import { extractFileList } from '../fs'
export default class DropZone extends React.Component { const Wrapper = styled.div`
constructor() { display: block;
super() `
this.state = { focus: false }
this.onDragEnter = this.onDragEnter.bind(this) const Overlay = styled.div`
this.onDragLeave = this.onDragLeave.bind(this) display: block;
this.onDragOver = this.onDragOver.bind(this) `
this.onDrop = this.onDrop.bind(this)
}
onDragEnter() { interface Props {
this.setState({ focus: true }) onDrop: (files: any) => void
} children?: React.ReactNode
}
onDragLeave(e) { const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
if (e.target !== this.refs.overlay.getDOMNode()) { const overlay = useRef()
return const [focus, setFocus] = useState(false)
}
this.setState({ focus: false })
}
onDragOver(e) { const handleDragEnter = useCallback(() => {
e.preventDefault() setFocus(true)
e.dataTransfer.dropEffect = 'copy' }, [])
}
const handleDragLeave = useCallback(
(e: React.DragEvent) => {
if (e.target !== overlay.current) {
return
}
onDrop(e) { setFocus(false)
},
[overlay.current],
)
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault() e.preventDefault()
this.setState({ focus: false }) e.dataTransfer.dropEffect = 'copy'
}, [])
const file = e.dataTransfer.files[0] const handleDrop = useCallback(
if (this.props.onDrop && file) { async (e: React.DragEvent) => {
this.props.onDrop(file) e.preventDefault()
} setFocus(false)
}
render() { const files = await extractFileList(e)
return ( onDrop(files)
<div },
className="drop-zone" [onDrop],
ref="root" )
onDragEnter={this.onDragEnter}
onDragLeave={this.onDragLeave}
onDragOver={this.onDragOver}
onDrop={this.onDrop}
>
<div
className="drop-zone-overlay"
hidden={!this.state.focus}
ref="overlay"
/>
{this.props.children} return (
</div> <Wrapper
) onDragEnter={handleDragEnter}
} onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Overlay ref={overlay} hidden={!focus} />
{children}
</Wrapper>
)
} }
DropZone.propTypes = { export default Dropzone
onDrop: React.PropTypes.func.isRequired,
}

@ -0,0 +1,27 @@
import React, { useCallback } from 'react'
import styled from 'styled-components'
const StyledPasswordInput = styled.input`
background: red;
`
interface Props {
value: string
onChange: (value: string) => void
}
export const PasswordField: React.FC<Props> = ({ value, onChange }: Props) => {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
}, [])
return (
<StyledPasswordInput
type="password"
value={value}
onChange={handleChange}
/>
)
}
export default PasswordField

@ -0,0 +1,16 @@
import React from 'react'
import styled from 'styled-components'
const StyledStartButton = styled.button`
background: green;
`
type Props = {
onClick: React.MouseEventHandler
}
const StartButton: React.FC<Props> = ({ onClick }: Props) => {
return <StyledStartButton onClick={onClick}>Start</StyledStartButton>
}
export default StartButton

@ -0,0 +1,16 @@
import React from 'react'
import styled from 'styled-components'
const StyledStopButton = styled.button`
background: blue;
`
type Props = {
onClick: React.MouseEventHandler
}
const StopButton: React.FC<Props> = ({ onClick }: Props) => {
return <StyledStopButton onClick={onClick}>Stop</StyledStopButton>
}
export default StopButton

@ -0,0 +1,13 @@
import React from 'react'
import { UploadedFile } from '../types'
interface Props {
files: UploadedFile[]
}
const UploadFileList: React.FC<Props> = ({ files }: Props) => {
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
return <ul>{items}</ul>
}
export default UploadFileList

@ -1,46 +1,54 @@
import Arrow from '@app/components/Arrow' import React, { useRef, useEffect } from 'react'
import React from 'react' import { usePeerData } from 'react-peer-data'
import UploadActions from '@app/actions/UploadActions' import { UploadedFile } from '../types'
import { Room } from 'peer-data'
export default class UploadPage extends React.Component { interface Props {
constructor() { roomName: string
super() files: UploadedFile[]
this.uploadFile = this.uploadFile.bind(this) }
}
uploadFile(file) { const Uploader: React.FC<Props> = ({ roomName, files }: Props) => {
UploadActions.uploadFile(file) const room = useRef<Room | null>(null)
} const peerData = usePeerData()
render() { useEffect(() => {
switch (this.props.status) { room.current = peerData.connect(roomName)
case 'ready': room.current
return ( .on('participant', (participant) => {
<div> console.log(participant.getId() + ' joined')
<DropZone onDrop={this.uploadFile} />
<Arrow dir="up" />
</div>
)
break
case 'processing': participant
return ( .on('connected', () => {
<div> console.log('connected', participant.id)
<Arrow dir="up" animated /> })
<FileDescription file={this.props.file} /> .on('disconnected', () => {
</div> console.log('disconnected', participant.id)
) })
break .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()
})
participant.send(`hello there, I'm the uploader`)
})
.on('error', (event) => {
console.error('room', roomName, event)
})
case 'uploading': return () => {
return ( room.current.disconnect()
<div> room.current = null
<Arrow dir="up" animated />
<FileDescription file={this.props.file} />
<Temaplink token={this.props.token} />
</div>
)
break
} }
} }, [peerData])
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
return <ul>{items}</ul>
} }
export default Uploader

@ -2,7 +2,27 @@ import React, { useState, useEffect } from 'react'
import { EventDispatcher } from 'peer-data' import { EventDispatcher } from 'peer-data'
import { PeerDataProvider } from 'react-peer-data' import { PeerDataProvider } from 'react-peer-data'
export const WebRTCProvider: React.FC = ({ children }) => { const dispatcher = new EventDispatcher()
const constraints = { ordered: true }
const signaling = { dispatcher }
const ICE_SERVERS: RTCConfiguration = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
}
interface Props {
servers?: RTCConfiguration
children?: React.ReactNode
}
export const WebRTCProvider: React.FC<Props> = ({
servers = ICE_SERVERS,
children,
}: Props) => {
const [pageLoaded, setPageLoaded] = useState(false) const [pageLoaded, setPageLoaded] = useState(false)
useEffect(() => { useEffect(() => {
@ -15,9 +35,9 @@ export const WebRTCProvider: React.FC = ({ children }) => {
return ( return (
<PeerDataProvider <PeerDataProvider
servers={{ iceServers: [{ urls: 'stun:stun.1.google.com:19302' }] }} servers={servers}
constraints={{ ordered: true }} constraints={constraints}
signaling={{ dispatcher: new EventDispatcher() }} signaling={signaling}
> >
{children} {children}
</PeerDataProvider> </PeerDataProvider>

@ -0,0 +1,60 @@
const getAsFile = (entry: any): Promise<File> =>
new Promise((resolve, reject) => {
entry.file((file: File) => {
file.fullPath = entry.fullPath
resolve(file)
}, reject)
})
const readDirectoryEntries = (reader: any): Promise<any[]> =>
new Promise((resolve, reject) => {
reader.readEntries((entries) => {
resolve(entries)
}, reject)
})
const scanDirectoryEntry = async (entry: any): Promise<File[]> => {
const directoryReader = entry.createReader()
const result = []
// eslint-disable-next-line no-constant-condition
while (true) {
const subentries = await readDirectoryEntries(directoryReader)
if (!subentries.length) {
return result
}
for (const se of subentries) {
if (se.isDirectory) {
const ses = await scanDirectoryEntry(se)
result.push(...ses)
} else {
const file = await getAsFile(se)
result.push(file)
}
}
}
}
export const extractFileList = async (e: React.DragEvent): Promise<File[]> => {
if (!e.dataTransfer.items.length) {
return []
}
const items = e.dataTransfer.items
const scans = []
const files = []
for (const item of items) {
const entry = item.webkitGetAsEntry()
if (entry.isDirectory) {
scans.push(scanDirectoryEntry(entry))
} else {
files.push(getAsFile(entry))
}
}
const scanResults = await Promise.all(scans)
const fileResults = await Promise.all(files)
return [scanResults, fileResults].flat(2)
}

@ -0,0 +1,7 @@
import debug from 'debug'
export const error = debug('filepizza:error')
export const info = debug('filepizza:info')
export const warn = debug('filepizza:warn')

@ -1,11 +1,25 @@
import React from 'react' import React from 'react'
import WebRTCProvider from '../../components/WebRTCProvider'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Downloader from '../../components/Downloader'
import { NextPage } from 'next'
const DownloadPage: React.FC = () => { const DownloadPage: NextPage = () => {
const router = useRouter() const router = useRouter()
const { slug } = router.query const { slug } = router.query
return <div>{JSON.stringify(slug)}</div> return (
<WebRTCProvider>
<>
<div>{JSON.stringify(slug)}</div>
<Downloader roomName="my-room" />
</>
</WebRTCProvider>
)
}
DownloadPage.getInitialProps = () => {
return {}
} }
export default DownloadPage export default DownloadPage

@ -1,10 +1,67 @@
import React from 'react' import React, { useCallback, useState } from 'react'
import WebRTCProvider from '../components/WebRTCProvider' import WebRTCProvider from '../components/WebRTCProvider'
import Dropzone from '../components/Dropzone'
import UploadFileList from '../components/UploadFileList'
import Uploader from '../components/Uploader'
import PasswordField from '../components/PasswordField'
import StartButton from '../components/StartButton'
import StopButton from '../components/StopButton'
import { UploadedFile } from '../types'
import { NextPage } from 'next'
export const IndexPage: React.FC = () => ( export const IndexPage: NextPage = () => {
<WebRTCProvider> const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
<>Index page</> const [password, setPassword] = useState('')
</WebRTCProvider> const [uploading, setUploading] = useState(false)
)
const handleDrop = useCallback((files: UploadedFile[]): void => {
console.log('Received files', files)
setUploadedFiles(files)
}, [])
const handleChangePassword = useCallback((pw: string) => {
setPassword(pw)
}, [])
const handleStart = useCallback(() => {
setUploading(true)
}, [])
const handleStop = useCallback(() => {
setUploading(false)
}, [])
if (!uploadedFiles.length) {
return (
<>
<Dropzone onDrop={handleDrop}>Drop a file to get started.</Dropzone>
</>
)
}
if (!uploading) {
return (
<>
<UploadFileList files={uploadedFiles} />
<PasswordField value={password} onChange={handleChangePassword} />
<StartButton onClick={handleStart} />
</>
)
}
return (
<WebRTCProvider>
<>
<UploadFileList files={uploadedFiles} />
<StopButton onClick={handleStop} />
<Uploader roomName={'my-room'} files={uploadedFiles} />
</>
</WebRTCProvider>
)
}
IndexPage.getInitialProps = () => {
return {}
}
export default IndexPage export default IndexPage

@ -0,0 +1,26 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const PeerDataServer = require('peer-data-server')
const appendPeerDataServer = PeerDataServer.default || PeerDataServer
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
})
appendPeerDataServer(server)
server.listen(3000, (err) => {
if (err) {
throw err
}
console.log('> Ready on http://localhost:3000')
})
})

@ -1,112 +0,0 @@
const fs = require('fs')
const express = require('express')
const expressWinston = require('express-winston')
const socketIO = require('socket.io')
const winston = require('winston')
const ice = require('./ice')
const db = require('./db')
process.on('unhandledRejection', (reason, p) => {
p.catch(err => {
log.error('Exiting due to unhandled rejection!')
log.error(err)
process.exit(1)
})
})
process.on('uncaughtException', (err) => {
log.error('Exiting due to uncaught exception!')
log.error(err)
process.exit(1)
})
const app = express()
let port
process.env.PORT || (process.env.NODE_ENV === 'production' ? 80 : 3000)
if (!process.env.QUIET) {
app.use(
expressWinston.logger({
winstonInstance: winston,
expressFormat: true,
}))
}
app.get('/app.js', require('./middleware/javascript'))
app.use(require('./middleware/static'))
app.use([
require('./middleware/bootstrap'),
require('./middleware/error'),
require('./middleware/react'),
])
const TRACKERS = process.env.WEBTORRENT_TRACKERS
? process.env.WEBTORRENT_TRACKERS.split(',').map(t => [t.trim()])
: [
['wss://tracker.openwebtorrent.com'],
['wss://tracker.btorrent.xyz'],
['wss://tracker.fastcast.nz'],
]
function bootServer(server) {
const io = socketIO(server)
io.set('transports', ['polling'])
io.on('connection', (socket) => {
let upload = null
socket.on('upload', (metadata, res) => {
if (upload) {
return
}
db.create(socket).then(u => {
upload = u
upload.fileName = metadata.fileName
upload.fileSize = metadata.fileSize
upload.fileType = metadata.fileType
upload.infoHash = metadata.infoHash
res({ token: upload.token, shortToken: upload.shortToken })
})
})
socket.on('trackerConfig', (_, res) => {
ice.getICEServers().then(iceServers => {
res({ rtcConfig: { iceServers }, announce: TRACKERS })
})
})
socket.on('disconnect', () => {
db.remove(upload)
})
})
server.on('error', (err) => {
winston.error(err.message)
process.exit(1)
})
server.listen(port, (err) => {
const host = server.address().address
const port = server.address().port
winston.info('FilePizza listening on %s:%s', host, port)
})
}
if (process.env.HTTPS_KEY && process.env.HTTPS_CERT) {
// user-supplied HTTPS key/cert
const https = require('https')
var server = https.createServer(
{
key: fs.readFileSync(process.env.HTTPS_KEY),
cert: fs.readFileSync(process.env.HTTPS_CERT),
},
app,
)
bootServer(server)
} else {
// no HTTPS
const http = require('http')
var server = http.Server(app)
bootServer(server)
}

@ -0,0 +1 @@
export type UploadedFile = File & { fullPath: string }

@ -1,51 +0,0 @@
const nib = require("nib");
const webpack = require('webpack')
;module.exports = {
entry: "./src/client",
target: "web",
output: {
filename: "dist/bundle.js",
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
test: /\.json$/,
loader: "json",
},
{
test: /\.styl$/,
loader: "style-loader!css-loader!stylus-loader",
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
GA_ACCESS_TOKEN: JSON.stringify(process.env.GA_ACCESS_TOKEN),
},
}),
],
node: {
fs: "empty",
},
stylus: {
use: [nib()],
},
rules: [
{
test: /react-google-analytics/,
use: process.env.GA_ACCESS_TOKEN ? 'null-loader' : 'noop-loader',
},
],
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save