mirror of https://github.com/kern/filepizza
Just keep going
parent
b601c89eeb
commit
6a0ab981f0
@ -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 {
|
||||
constructor() {
|
||||
super()
|
||||
this.state = { focus: false }
|
||||
const Wrapper = styled.div`
|
||||
display: block;
|
||||
`
|
||||
|
||||
this.onDragEnter = this.onDragEnter.bind(this)
|
||||
this.onDragLeave = this.onDragLeave.bind(this)
|
||||
this.onDragOver = this.onDragOver.bind(this)
|
||||
this.onDrop = this.onDrop.bind(this)
|
||||
}
|
||||
const Overlay = styled.div`
|
||||
display: block;
|
||||
`
|
||||
|
||||
onDragEnter() {
|
||||
this.setState({ focus: true })
|
||||
}
|
||||
interface Props {
|
||||
onDrop: (files: any) => void
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
onDragLeave(e) {
|
||||
if (e.target !== this.refs.overlay.getDOMNode()) {
|
||||
return
|
||||
}
|
||||
this.setState({ focus: false })
|
||||
}
|
||||
const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
|
||||
const overlay = useRef()
|
||||
const [focus, setFocus] = useState(false)
|
||||
|
||||
onDragOver(e) {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
const handleDragEnter = useCallback(() => {
|
||||
setFocus(true)
|
||||
}, [])
|
||||
|
||||
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()
|
||||
this.setState({ focus: false })
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}, [])
|
||||
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (this.props.onDrop && file) {
|
||||
this.props.onDrop(file)
|
||||
}
|
||||
}
|
||||
const handleDrop = useCallback(
|
||||
async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setFocus(false)
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="drop-zone"
|
||||
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"
|
||||
/>
|
||||
const files = await extractFileList(e)
|
||||
onDrop(files)
|
||||
},
|
||||
[onDrop],
|
||||
)
|
||||
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Wrapper
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Overlay ref={overlay} hidden={!focus} />
|
||||
{children}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
DropZone.propTypes = {
|
||||
onDrop: React.PropTypes.func.isRequired,
|
||||
}
|
||||
export default Dropzone
|
||||
|
||||
@ -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 from 'react'
|
||||
import UploadActions from '@app/actions/UploadActions'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { usePeerData } from 'react-peer-data'
|
||||
import { UploadedFile } from '../types'
|
||||
import { Room } from 'peer-data'
|
||||
|
||||
export default class UploadPage extends React.Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.uploadFile = this.uploadFile.bind(this)
|
||||
}
|
||||
interface Props {
|
||||
roomName: string
|
||||
files: UploadedFile[]
|
||||
}
|
||||
|
||||
uploadFile(file) {
|
||||
UploadActions.uploadFile(file)
|
||||
}
|
||||
const Uploader: React.FC<Props> = ({ roomName, files }: Props) => {
|
||||
const room = useRef<Room | null>(null)
|
||||
const peerData = usePeerData()
|
||||
|
||||
render() {
|
||||
switch (this.props.status) {
|
||||
case 'ready':
|
||||
return (
|
||||
<div>
|
||||
<DropZone onDrop={this.uploadFile} />
|
||||
<Arrow dir="up" />
|
||||
</div>
|
||||
)
|
||||
break
|
||||
useEffect(() => {
|
||||
room.current = peerData.connect(roomName)
|
||||
room.current
|
||||
.on('participant', (participant) => {
|
||||
console.log(participant.getId() + ' joined')
|
||||
|
||||
case 'processing':
|
||||
return (
|
||||
<div>
|
||||
<Arrow dir="up" animated />
|
||||
<FileDescription file={this.props.file} />
|
||||
</div>
|
||||
)
|
||||
break
|
||||
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()
|
||||
})
|
||||
participant.send(`hello there, I'm the uploader`)
|
||||
})
|
||||
.on('error', (event) => {
|
||||
console.error('room', roomName, event)
|
||||
})
|
||||
|
||||
case 'uploading':
|
||||
return (
|
||||
<div>
|
||||
<Arrow dir="up" animated />
|
||||
<FileDescription file={this.props.file} />
|
||||
<Temaplink token={this.props.token} />
|
||||
</div>
|
||||
)
|
||||
break
|
||||
return () => {
|
||||
room.current.disconnect()
|
||||
room.current = null
|
||||
}
|
||||
}
|
||||
}, [peerData])
|
||||
|
||||
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
|
||||
return <ul>{items}</ul>
|
||||
}
|
||||
|
||||
export default Uploader
|
||||
|
||||
@ -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 WebRTCProvider from '../../components/WebRTCProvider'
|
||||
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 { 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
|
||||
|
||||
@ -1,10 +1,67 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
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 = () => (
|
||||
<WebRTCProvider>
|
||||
<>Index page</>
|
||||
</WebRTCProvider>
|
||||
)
|
||||
export const IndexPage: NextPage = () => {
|
||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
|
||||
const [password, setPassword] = useState('')
|
||||
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
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
Loading…
Reference in New Issue