More logic for upload/download

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

@ -1,20 +0,0 @@
workflow "Build on push" {
on = "push"
resolves = ["AWS deploy"]
}
action "Docker build, tag, and push" {
uses = "pangzineng/Github-Action-One-Click-Docker@master"
secrets = ["DOCKER_USERNAME", "DOCKER_PASSWORD"]
}
action "AWS deploy" {
uses = "actions/aws/cli@efb074ae4510f2d12c7801e4461b65bf5e8317e6"
needs = ["Docker build, tag, and push"]
secrets = [
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
]
args = "deploy --region us-west-2 create-deployment --application-name AppECS-filepizza-filepizza --deployment-config-name CodeDeployDefault.ECSAllAtOnce --deployment-group-name DgpECS-filepizza-filepizza --github-location repository=kern/filepizza,commitId=$GITHUB_REF"
runs = "aws"
}

@ -20,16 +20,21 @@
"dependencies": { "dependencies": {
"debug": "^4.2.0", "debug": "^4.2.0",
"express": "^4.12.0", "express": "^4.12.0",
"fp-ts": "^2.9.3",
"immer": "^8.0.0",
"io-ts": "^2.2.13",
"ioredis": "^4.17.3", "ioredis": "^4.17.3",
"next": "^9.5.3", "next": "^9.5.3",
"nodemon": "^1.4.1", "nodemon": "^1.4.1",
"peer": "^0.5.3", "peer": "^0.5.3",
"peerjs": "^1.3.1", "peerjs": "^1.3.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-device-detect": "^1.15.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-qr": "0.0.2", "react-qr": "0.0.2",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"twilio": "^2.9.1", "twilio": "^2.9.1",
"use-http": "^1.0.16",
"webrtcsupport": "^2.2.0", "webrtcsupport": "^2.2.0",
"xkcd-password": "^1.2.0" "xkcd-password": "^1.2.0"
}, },

@ -1,24 +0,0 @@
import React from 'react'
export default class DownloadButton extends React.Component {
constructor() {
super()
this.onClick = this.onClick.bind(this)
}
onClick(e) {
this.props.onClick(e)
}
render() {
return (
<button className="download-button" onClick={this.onClick}>
Download
</button>
)
}
}
DownloadButton.propTypes = {
onClick: React.PropTypes.func.isRequired,
}

@ -1,103 +0,0 @@
import React from 'react'
import DownloadActions from '../actions/DownloadActions'
import DownloadStore from '../stores/DownloadStore'
import { formatSize } from '../util'
import ChromeNotice from './ChromeNotice'
import DownloadButton from './DownloadButton'
import ErrorPage from './ErrorPage'
import ProgressBar from './ProgressBar'
import Spinner from './Spinner'
export default class DownloadPage extends React.Component {
constructor() {
super()
this.state = DownloadStore.getState()
this._onChange = () => {
this.setState(DownloadStore.getState())
}
this.downloadFile = this.downloadFile.bind(this)
}
componentDidMount() {
DownloadStore.listen(this._onChange)
}
componentWillUnmount() {
DownloadStore.unlisten(this._onChange)
}
downloadFile() {
DownloadActions.requestDownload()
}
render() {
switch (this.state.status) {
case 'ready':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<DownloadButton onClick={this.downloadFile} />
</div>
)
case 'requesting':
case 'downloading':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
animated
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<ProgressBar value={this.state.progress} />
</div>
)
case 'done':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<ProgressBar value={1} />
</div>
)
default:
return <ErrorPage />
}
}
}

@ -1,56 +1,108 @@
import React from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useWebRTC } from './WebRTCProvider'
import {
browserName,
browserVersion,
osName,
osVersion,
mobileVendor,
mobileModel,
} from 'react-device-detect'
import * as t from 'io-ts'
import { decodeMessage, Message, MessageType } from '../messages'
interface Props { export default function Downloader({
uploaderPeerID,
}: {
uploaderPeerID: string uploaderPeerID: string
} }): JSX.Element {
const peer = useWebRTC()
const Downloader: React.FC<Props> = ({ uploaderPeerID }: Props) => { const [password, setPassword] = useState('')
// const room = useRef<Room | null>(null) const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
// const peerData = usePeerData() const [open, setOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
// useEffect(() => {
// if (room.current) return useEffect(() => {
if (!shouldAttemptConnection) {
// room.current = peerData.connect(roomName) return
// console.log(room.current) }
// setInterval(() => console.log(room.current), 1000)
// room.current const conn = peer.connect(uploaderPeerID, {
// .on('participant', (participant) => { reliable: true,
// console.log(participant.getId() + ' joined') })
// participant.newDataChannel()
conn.on('open', () => {
// participant setOpen(true)
// .on('connected', () => {
// console.log('connected', participant.id) const request: t.TypeOf<typeof Message> = {
// }) type: MessageType.Start,
// .on('disconnected', () => { browserName: browserName,
// console.log('disconnected', participant.id) browserVersion: browserVersion,
// }) osName: osName,
// .on('track', (event) => { osVersion: osVersion,
// console.log('stream', participant.id, event.streams[0]) mobileVendor: mobileVendor,
// }) mobileModel: mobileModel,
// .on('message', (payload) => { password,
// console.log(participant.id, payload) }
// })
// .on('error', (event) => { conn.send(request)
// console.error('peer', participant.id, event) })
// participant.renegotiate()
// }) conn.on('data', (data) => {
try {
// console.log(participant) const message = decodeMessage(data)
// participant.send(`hello there, I'm the downloader`) switch (message.type) {
// }) case MessageType.Info:
// .on('error', (event) => { console.log(message)
// console.error('room', roomName, event) break
// })
// return () => {
// room.current.disconnect()
// room.current = null
// }
// }, [peerData])
return null
}
export default Downloader case MessageType.Error:
console.error(message.error)
setErrorMessage(message.error)
conn.close()
break
}
} catch (err) {
console.error(err)
}
})
conn.on('close', () => {
setOpen(false)
setShouldAttemptConnection(false)
})
return () => {
if (conn.open) conn.close()
}
}, [peer, password, shouldAttemptConnection])
const handleChangePassword = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
},
[],
)
const handleSubmit = useCallback((ev) => {
ev.preventDefault()
setShouldAttemptConnection(true)
}, [])
if (open) {
return <div>Downloading</div>
}
if (shouldAttemptConnection) {
return <div>Loading...</div>
}
return (
<form action="#" method="post" onSubmit={handleSubmit}>
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
<input type="password" value={password} onChange={handleChangePassword} />
<button>Start</button>
</form>
)
}

@ -10,12 +10,13 @@ const Overlay = styled.div`
display: block; display: block;
` `
interface Props { export default function DropZone({
onDrop: (files: any) => void children,
onDrop,
}: {
onDrop: (files: File[]) => void
children?: React.ReactNode children?: React.ReactNode
} }): JSX.Element {
const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
const overlay = useRef() const overlay = useRef()
const [focus, setFocus] = useState(false) const [focus, setFocus] = useState(false)
@ -62,5 +63,3 @@ const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
</Wrapper> </Wrapper>
) )
} }
export default Dropzone

@ -1,104 +0,0 @@
import React from 'react'
import DropZone from './DropZone'
import Spinner from "./Spinner";
import Tempalink from "./Tempalink";
import UploadActions from "../actions/UploadActions";
import UploadStore from "../stores/UploadStore";
import socket from "filepizza-socket";
import { formatSize } from "../util";
export default class UploadPage extends React.Component {
constructor() {
super();
this.state = UploadStore.getState();
this._onChange = () => {
this.setState(UploadStore.getState());
};
this.uploadFile = this.uploadFile.bind(this);
}
componentDidMount() {
UploadStore.listen(this._onChange);
}
componentWillUnmount() {
UploadStore.unlisten(this._onChange);
}
uploadFile(file) {
UploadActions.uploadFile(file);
}
handleSelectedFile(event) {
const files = event.target.files;
if (files.length > 0) {
UploadActions.uploadFile(files[0]);
}
}
render() {
switch (this.state.status) {
case "ready":
return (
<DropZone onDrop={this.uploadFile}>
<div className="page">
<Spinner dir="up" />
<h1>FilePizza</h1>
<p>Free peer-to-peer file transfers in your browser.</p>
<small className="notice">
We never store anything. Files only served fresh.
</small>
<p>
<label className="select-file-label">
<input
type="file"
onChange={this.handleSelectedFile}
required
/>
<span>select a file</span>
</label>
</p>
</div>
</DropZone>
);
case "processing":
return (
<div className="page">
<Spinner dir="up" animated />
<h1>FilePizza</h1>
<p>Processing...</p>
</div>
);
case "uploading":
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="up"
animated
name={this.state.fileName}
size={this.state.fileSize}
/>
<p>Send someone this link to download.</p>
<small className="notice">
This link will work as long as this page is open.
</small>
<p>
Peers: {this.state.peers} &middot; Up:{" "}
{formatSize(this.state.speedUp)}
</p>
<Tempalink
token={this.state.token}
shortToken={this.state.shortToken}
/>
</div>
);
}
}

@ -1,54 +1,244 @@
import React, { useRef, useEffect } from 'react' import React, { useEffect, useState } from 'react'
import { UploadedFile } from '../types' import { UploadedFile } from '../types'
import { useWebRTC } from './WebRTCProvider' import { useWebRTC } from './WebRTCProvider'
import useFetch from 'use-http'
import Peer, { DataConnection } from 'peerjs'
import { decodeMessage, Message, MessageType } from '../messages'
import produce from 'immer'
import * as t from 'io-ts'
interface Props { enum UploaderConnectionStatus {
roomName: string Pending = 'PENDING',
files: UploadedFile[] Uploading = 'UPLOADING',
Done = 'DONE',
InvalidPassword = 'INVALID_PASSWORD',
Closed = 'CLOSED',
Paused = 'PAUSED',
}
type UploaderConnection = {
status: UploaderConnectionStatus
dataConnection: DataConnection
browserName?: string
browserVersion?: string
osName?: string
osVersion?: string
mobileVendor?: string
mobileModel?: string
}
const RENEW_INTERVAL = 5000 // 20 minutes
function useUploaderChannel(
uploaderPeerID: string,
): {
loading: boolean
error: Error | null
longSlug: string
shortSlug: string
} {
const { loading, error, data } = useFetch(
'/api/create',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploaderPeerID }),
},
[uploaderPeerID],
)
if (!data) {
return { loading, error, longSlug: null, shortSlug: null }
}
return {
loading: false,
error: null,
longSlug: data.longSlug,
shortSlug: data.shortSlug,
}
}
function useUploaderChannelRenewal(shortSlug: string): void {
const { post } = useFetch('/api/renew')
useEffect(() => {
let timeout = null
const run = (): void => {
timeout = setTimeout(() => {
post({ slug: shortSlug })
.then(() => {
run()
})
.catch((err) => {
console.error(err)
run()
})
}, RENEW_INTERVAL)
}
run()
return () => {
clearTimeout(timeout)
}
}, [shortSlug])
} }
const Uploader: React.FC<Props> = ({ roomName, files }: Props) => { function useUploaderConnections(
const room = useRef<Room | null>(null) peer: Peer,
files: UploadedFile[],
password: string,
): Array<UploaderConnection> {
const [connections, setConnections] = useState<Array<UploaderConnection>>([])
useEffect(() => {
peer.on('connection', (conn: DataConnection) => {
const newConn = {
status: UploaderConnectionStatus.Pending,
dataConnection: conn,
}
setConnections((conns) => [...conns, newConn])
const updateConnection = (
fn: (draftConn: UploaderConnection) => void,
) => {
setConnections((conns) =>
produce(conns, (draft) => {
const updatedConn = draft.find((c) => c.dataConnection === conn)
if (!updatedConn) {
return
}
fn(updatedConn)
}),
)
}
conn.on('data', (data): void => {
try {
const message = decodeMessage(data)
switch (message.type) {
case MessageType.RequestInfo: {
if (message.password !== password) {
const request: t.TypeOf<typeof Message> = {
type: MessageType.Error,
error: 'Invalid password',
}
conn.send(request)
updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Pending) {
return
}
draft.status = UploaderConnectionStatus.InvalidPassword
draft.browserName = message.browserName
draft.browserVersion = message.browserVersion
draft.osName = message.osName
draft.osVersion = message.osVersion
draft.mobileVendor = message.mobileVendor
draft.mobileModel = message.mobileModel
})
return
}
updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Pending) {
return
}
draft.status = UploaderConnectionStatus.Uploading
draft.browserName = message.browserName
draft.browserVersion = message.browserVersion
draft.osName = message.osName
draft.osVersion = message.osVersion
draft.mobileVendor = message.mobileVendor
draft.mobileModel = message.mobileModel
})
const fileInfo = files.map((f) => {
return {
fullPath: f.fullPath,
}
})
const request: t.TypeOf<typeof Message> = {
type: MessageType.Info,
files: fileInfo,
}
conn.send(request)
// TODO(@kern): Handle sending chunks
break
}
}
} catch (err) {
console.error(err)
}
})
conn.on('close', (): void => {
updateConnection((draft) => {
if (draft.status === UploaderConnectionStatus.InvalidPassword) {
return
}
draft.status = UploaderConnectionStatus.Closed
})
})
})
}, [peer, files, password])
return connections
}
export default function Uploader({
files,
password,
}: {
files: UploadedFile[]
password: string
}): JSX.Element {
const peer = useWebRTC() const peer = useWebRTC()
const { longSlug, shortSlug } = useUploaderChannel(peer.id)
useUploaderChannelRenewal(shortSlug)
const connections = useUploaderConnections(peer, files, password)
// useEffect(() => { if (!longSlug || !shortSlug) {
// room.current = peerData.connect(roomName) return null
// room.current }
// .on('participant', (participant) => {
// console.log(participant.getId() + ' joined') const longURL = `/download/${longSlug}`
// participant.newDataChannel() const shortURL = `/download/${shortSlug}`
//
// 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)
// })
//
// return () => {
// room.current.disconnect()
// room.current = null
// }
// }, [peerData])
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>) const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
return <ul>{items}</ul> return (
<>
<div>
Long:
<a href={longURL} target="_blank">
{longURL}
</a>
</div>
<div>
Short:
<a href={shortURL} target="_blank">
{shortURL}
</a>
</div>
<ul>{items}</ul>
<h2>Connections</h2>
<ul>
{connections.map((conn) => (
<li>
{conn.status} {conn.browserName} {conn.browserVersion}
</li>
))}
</ul>
</>
)
} }
export default Uploader

@ -14,36 +14,38 @@ const ICE_SERVERS: RTCConfiguration = {
], ],
} }
interface Props { const WebRTCContext = React.createContext<WebRTCValue | null>(null)
servers?: RTCConfiguration
children?: React.ReactNode
}
const WebRTCContext = React.createContext<WebRTCValue>(null)
export const useWebRTC = (): WebRTCValue => { export const useWebRTC = (): WebRTCValue => {
return useContext(WebRTCContext) return useContext(WebRTCContext)!
} }
export const WebRTCProvider: React.FC<Props> = ({ export function WebRTCProvider({
servers = ICE_SERVERS, servers = ICE_SERVERS,
children, children,
}: Props) => { }: {
const [pageLoaded, setPageLoaded] = useState(false) servers?: RTCConfiguration
const peer = useRef<WebRTCValue>(null) children?: React.ReactNode
}): JSX.Element {
const [loaded, setLoaded] = useState(false)
const peer = useRef<WebRTCValue | null>(null)
useEffect(() => { useEffect(() => {
const effect = async () => { const effect = async () => {
peer.current = new Peer(undefined, { const peerObj = new Peer(undefined, {
config: servers, config: servers,
}) })
setPageLoaded(true)
peerObj.on('open', () => {
peer.current = peerObj
setLoaded(true)
})
} }
effect() effect()
}, []) }, [])
if (!pageLoaded || !peer.current) { if (!loaded || !peer.current) {
return null return null
} }

@ -58,3 +58,15 @@ export const extractFileList = async (e: React.DragEvent): Promise<File[]> => {
return [scanResults, fileResults].flat(2) return [scanResults, fileResults].flat(2)
} }
// Borrowed from StackOverflow
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
export const formatSize = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1000
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`
}

@ -1,52 +0,0 @@
const twilio = require('twilio')
const winston = require('winston')
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN) {
const twilioSID = process.env.TWILIO_SID
const twilioToken = process.env.TWILIO_TOKEN
var client = twilio(twilioSID, twilioToken)
winston.info('Using Twilio TURN service')
} else {
var client = null
}
let ICE_SERVERS = [
{
urls: 'stun:stun.l.google.com:19302',
},
]
if (process.env.ICE_SERVERS) {
ICE_SERVERS = JSON.parse(process.env.ICE_SERVERS)
}
const CACHE_LIFETIME = 5 * 60 * 1000 // 5 minutes
let cachedPromise = null
function clearCache() {
cachedPromise = null
}
exports.getICEServers = function() {
if (client == null) {
return Promise.resolve(ICE_SERVERS)
}
if (cachedPromise) {
return cachedPromise
}
cachedPromise = new Promise((resolve, reject) => {
client.tokens.create({}, (err, token) => {
if (err) {
winston.error(err)
return resolve(DEFAULT_ICE_SERVERS)
}
winston.info('Retrieved ICE servers from Twilio')
setTimeout(clearCache, CACHE_LIFETIME)
resolve(token.ice_servers)
})
})
return cachedPromise
}

@ -0,0 +1,77 @@
import * as t from 'io-ts'
import { pipe } from 'fp-ts/function'
import { fold } from 'fp-ts/Either'
export enum MessageType {
RequestInfo = 'REQUEST_INFO',
Info = 'INFO',
Start = 'START',
Chunk = 'CHUNK',
Pause = 'PAUSE',
Error = 'ERROR',
}
const RequestInfoMessage = t.type({
type: t.literal(MessageType.RequestInfo),
browserName: t.string,
browserVersion: t.string,
osName: t.string,
osVersion: t.string,
mobileVendor: t.string,
mobileModel: t.string,
password: t.string,
})
const InfoMessage = t.type({
type: t.literal(MessageType.Info),
files: t.array(
t.type({
fullPath: t.string,
}),
),
})
const StartMessage = t.type({
type: t.literal(MessageType.Start),
browserName: t.string,
browserVersion: t.string,
osName: t.string,
osVersion: t.string,
mobileVendor: t.string,
mobileModel: t.string,
password: t.string,
})
const ChunkMessage = t.type({
type: t.literal(MessageType.Chunk),
// TODO(@kern): Chunk
})
const PauseMessage = t.type({
type: t.literal(MessageType.Pause),
// TODO(@kern): Pausing
})
const ErrorMessage = t.type({
type: t.literal(MessageType.Error),
error: t.string,
})
export const Message = t.union([
RequestInfoMessage,
InfoMessage,
StartMessage,
ChunkMessage,
PauseMessage,
ErrorMessage,
])
export function decodeMessage(data: any): t.TypeOf<typeof Message> {
const onFailure = (errors: t.Errors): t.TypeOf<typeof Message> => {
throw new Error(`${errors.length} error(s) found`)
}
const onSuccess = (mesg: t.TypeOf<typeof Message>) => mesg
return pipe(Message.decode(data), fold(onFailure, onSuccess))
}

@ -2,9 +2,10 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel' import { channelRepo } from '../../channel'
import { routeHandler, getBodyKey } from '../../routes' import { routeHandler, getBodyKey } from '../../routes'
export default routeHandler<void>( export default routeHandler<boolean>(
(req: NextApiRequest, _res: NextApiResponse): Promise<void> => { async (req: NextApiRequest, _res: NextApiResponse): Promise<boolean> => {
const slug = getBodyKey(req, 'slug') const slug = getBodyKey(req, 'slug')
return channelRepo.renew(slug) await channelRepo.renew(slug)
return true
}, },
) )

@ -10,14 +10,10 @@ type Props = {
error?: string error?: string
} }
const DownloadPage: NextPage<Props> = ({ slug, uploaderPeerID }) => { const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => {
return ( return (
<WebRTCProvider> <WebRTCProvider>
<> <Downloader uploaderPeerID={uploaderPeerID} />
<div>{slug}</div>
<div>{uploaderPeerID}</div>
<Downloader uploaderPeerID={uploaderPeerID} />
</>
</WebRTCProvider> </WebRTCProvider>
) )
} }

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import WebRTCProvider from '../components/WebRTCProvider' import WebRTCProvider from '../components/WebRTCProvider'
import Dropzone from '../components/Dropzone' import DropZone from '../components/DropZone'
import UploadFileList from '../components/UploadFileList' import UploadFileList from '../components/UploadFileList'
import Uploader from '../components/Uploader' import Uploader from '../components/Uploader'
import PasswordField from '../components/PasswordField' import PasswordField from '../components/PasswordField'
@ -15,7 +15,6 @@ export const IndexPage: NextPage = () => {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const handleDrop = useCallback((files: UploadedFile[]): void => { const handleDrop = useCallback((files: UploadedFile[]): void => {
console.log('Received files', files)
setUploadedFiles(files) setUploadedFiles(files)
}, []) }, [])
@ -34,7 +33,7 @@ export const IndexPage: NextPage = () => {
if (!uploadedFiles.length) { if (!uploadedFiles.length) {
return ( return (
<> <>
<Dropzone onDrop={handleDrop}>Drop a file to get started.</Dropzone> <DropZone onDrop={handleDrop}>Drop a file to get started.</DropZone>
</> </>
) )
} }
@ -51,11 +50,9 @@ export const IndexPage: NextPage = () => {
return ( return (
<WebRTCProvider> <WebRTCProvider>
<> <UploadFileList files={uploadedFiles} />
<UploadFileList files={uploadedFiles} /> <StopButton onClick={handleStop} />
<StopButton onClick={handleStop} /> <Uploader files={uploadedFiles} password={password} />
<Uploader roomName={'my-room'} files={uploadedFiles} />
</>
</WebRTCProvider> </WebRTCProvider>
) )
} }

@ -15,10 +15,11 @@ export const generateShortSlug = (): string => {
const longSlugGenerator = new xkcdPassword() const longSlugGenerator = new xkcdPassword()
longSlugGenerator.initWithWordList(config.longSlug.words) longSlugGenerator.initWithWordList(config.longSlug.words)
export const generateLongSlug = (): Promise<string> => { export const generateLongSlug = async (): Promise<string> => {
return longSlugGenerator.generate({ const parts = await longSlugGenerator.generate({
numWords: config.longSlug.numWords, numWords: config.longSlug.numWords,
minLength: 1, minLength: 1,
maxLength: 256, maxLength: 256,
}) })
return parts.join('/')
} }

@ -1,11 +0,0 @@
// Borrowed from StackOverflow
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
export const formatSize = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1000
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`
}

@ -3749,6 +3749,11 @@ forwarded@~0.1.2:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fp-ts@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.9.3.tgz#419b9103018ddf7c785acf2b3fecb2662afab5af"
integrity sha512-NjzcHYgigcbPQ6yJ52zwgsVDwKz3vwy9sjbxyzcvfXQm+j1BGeOPRuzLKEwsLyE4Xut6gG1FXJtsU9/gUB7tXg==
fragment-cache@^0.2.1: fragment-cache@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@ -4201,6 +4206,11 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.0.tgz#08763549ba9dd7d5e2eb4bec504a8315bd9440c2"
integrity sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg==
import-fresh@^3.0.0, import-fresh@^3.2.1: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@ -4278,6 +4288,11 @@ invariant@^2.2.2, invariant@^2.2.4:
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
io-ts@^2.2.13:
version "2.2.13"
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.13.tgz#e04016685e863dd5cffb2b5262c5c9c74432dfda"
integrity sha512-BYJgE/BanovJKDvCnAkrr7f3gTucSyk+Sr5VtpouBO1/YfBKUyIn2z1ODG8LEF+1D4sjKZ3Bd/A5/v8JrJe5UQ==
ioredis@^4.17.3: ioredis@^4.17.3:
version "4.17.3" version "4.17.3"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc"
@ -6171,6 +6186,13 @@ rc@^1.0.1, rc@^1.1.6:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-device-detect@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.15.0.tgz#5321f94ae3c4d51ef399b0502a6c739e32d0f315"
integrity sha512-ywjtWW04U7vaJK87IAFHhKozZhTPeDVWsfYx5CxQSQCjU5+fnMMxWZt9HnVWaNTqBEn6g8wCNWyqav7sXJrURg==
dependencies:
ua-parser-js "^0.7.23"
react-dom@^16.13.1: react-dom@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@ -7529,6 +7551,11 @@ typescript@^4.0.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5"
integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==
ua-parser-js@^0.7.23:
version "0.7.23"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b"
integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA==
undefsafe@^2.0.2: undefsafe@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
@ -7666,6 +7693,25 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
urs@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/urs/-/urs-0.0.7.tgz#2dbf320a68a54bbd67ab9c5ac103b0f2227ca6be"
integrity sha512-NDwuby5BP7D60RjHlow8Gch4yfglLdCpZagGeVDZNcL+jsA2v6RcBhPTJraqq1oQ1U3LT1bQfgmwgNUUIFht+g==
use-http@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/use-http/-/use-http-1.0.16.tgz#6d75f3e734abf990049c7a02b22d757c3abda883"
integrity sha512-t2/Q6Ic644Jn9z62sOVhfURXX58429NwgRDvJZikxhDjLG5FrZ0SLLxGgm4GYznOnKBqkXKVrhW/wdjwXkw1rA==
dependencies:
urs "^0.0.7"
use-ssr "^1.0.22"
utility-types "^3.10.0"
use-ssr@^1.0.22:
version "1.0.23"
resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.23.tgz#3bde1e10cd01b3b61ab6386d7cddb72e74828bf8"
integrity sha512-5bvlssgROgPgIrnILJe2mJch4e2Id0/bVm1SQzqvPvEAXmlsinCCVHWK3a2iHcPat7PkdJHBo0gmSmODIz6tNA==
use-subscription@1.4.1: use-subscription@1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.4.1.tgz#edcbcc220f1adb2dd4fa0b2f61b6cc308e620069" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.4.1.tgz#edcbcc220f1adb2dd4fa0b2f61b6cc308e620069"
@ -7697,6 +7743,11 @@ util@^0.11.0:
dependencies: dependencies:
inherits "2.0.3" inherits "2.0.3"
utility-types@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
utils-merge@1.0.1: utils-merge@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"

Loading…
Cancel
Save