|
|
|
@ -12,6 +12,14 @@ import * as t from 'io-ts'
|
|
|
|
import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages'
|
|
|
|
import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages'
|
|
|
|
import { createZipStream } from '../zip-stream'
|
|
|
|
import { createZipStream } from '../zip-stream'
|
|
|
|
import { DataConnection } from 'peerjs'
|
|
|
|
import { DataConnection } from 'peerjs'
|
|
|
|
|
|
|
|
import PasswordField from './PasswordField'
|
|
|
|
|
|
|
|
import UnlockButton from './UnlockButton'
|
|
|
|
|
|
|
|
import { chakra, Box, Text, VStack } from '@chakra-ui/react'
|
|
|
|
|
|
|
|
import Loading from './Loading'
|
|
|
|
|
|
|
|
import UploadFileList from './UploadFileList'
|
|
|
|
|
|
|
|
import DownloadButton from './DownloadButton'
|
|
|
|
|
|
|
|
import StopButton from './StopButton'
|
|
|
|
|
|
|
|
import ProgressBar from './ProgressBar'
|
|
|
|
|
|
|
|
|
|
|
|
const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
|
|
|
|
const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
|
|
|
|
|
|
|
|
|
|
|
|
@ -28,6 +36,14 @@ function getZipFilename(): string {
|
|
|
|
return `filepizza-download-${Date.now()}.zip`
|
|
|
|
return `filepizza-download-${Date.now()}.zip`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function cleanErrorMessage(errorMessage: string): string {
|
|
|
|
|
|
|
|
if (errorMessage.startsWith('Could not connect to peer')) {
|
|
|
|
|
|
|
|
return 'Could not connect to the uploader. Did they close their browser?'
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return errorMessage
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type DownloadFileStream = {
|
|
|
|
type DownloadFileStream = {
|
|
|
|
name: string
|
|
|
|
name: string
|
|
|
|
size: number
|
|
|
|
size: number
|
|
|
|
@ -97,6 +113,7 @@ export default function Downloader({
|
|
|
|
const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
|
|
|
|
const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const [downloading, setDownloading] = useState(false)
|
|
|
|
const [downloading, setDownloading] = useState(false)
|
|
|
|
|
|
|
|
const [bytesDownloaded, setBytesDownloaded] = useState(0)
|
|
|
|
const [done, setDone] = useState(false)
|
|
|
|
const [done, setDone] = useState(false)
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
@ -111,7 +128,7 @@ export default function Downloader({
|
|
|
|
|
|
|
|
|
|
|
|
setDataConnection(conn)
|
|
|
|
setDataConnection(conn)
|
|
|
|
|
|
|
|
|
|
|
|
conn.on('open', () => {
|
|
|
|
const handleOpen = () => {
|
|
|
|
setOpen(true)
|
|
|
|
setOpen(true)
|
|
|
|
|
|
|
|
|
|
|
|
const request: t.TypeOf<typeof Message> = {
|
|
|
|
const request: t.TypeOf<typeof Message> = {
|
|
|
|
@ -126,13 +143,14 @@ export default function Downloader({
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conn.send(request)
|
|
|
|
conn.send(request)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conn.on('data', (data) => {
|
|
|
|
const handleData = (data: unknown) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const message = decodeMessage(data)
|
|
|
|
const message = decodeMessage(data)
|
|
|
|
switch (message.type) {
|
|
|
|
switch (message.type) {
|
|
|
|
case MessageType.Info:
|
|
|
|
case MessageType.Info:
|
|
|
|
|
|
|
|
console.log(message.files)
|
|
|
|
setFilesInfo(message.files)
|
|
|
|
setFilesInfo(message.files)
|
|
|
|
break
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
@ -149,27 +167,47 @@ export default function Downloader({
|
|
|
|
} catch (err) {
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conn.on('close', () => {
|
|
|
|
const handleClose = () => {
|
|
|
|
setDataConnection(null)
|
|
|
|
setDataConnection(null)
|
|
|
|
setOpen(false)
|
|
|
|
setOpen(false)
|
|
|
|
setDownloading(false)
|
|
|
|
setDownloading(false)
|
|
|
|
setShouldAttemptConnection(false)
|
|
|
|
setShouldAttemptConnection(false)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handlePeerError = (err: Error) => {
|
|
|
|
|
|
|
|
console.error(err)
|
|
|
|
|
|
|
|
setErrorMessage(cleanErrorMessage(err.message))
|
|
|
|
|
|
|
|
if (conn.open) {
|
|
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleConnectionError = (err: Error) => {
|
|
|
|
|
|
|
|
console.error(err)
|
|
|
|
|
|
|
|
setErrorMessage(cleanErrorMessage(err.message))
|
|
|
|
|
|
|
|
if (conn.open) conn.close()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conn.on('open', handleOpen)
|
|
|
|
|
|
|
|
conn.on('data', handleData)
|
|
|
|
|
|
|
|
conn.on('error', handleConnectionError)
|
|
|
|
|
|
|
|
conn.on('close', handleClose)
|
|
|
|
|
|
|
|
peer.on('error', handlePeerError)
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
return () => {
|
|
|
|
if (conn.open) conn.close()
|
|
|
|
if (conn.open) conn.close()
|
|
|
|
|
|
|
|
conn.off('open', handleOpen)
|
|
|
|
|
|
|
|
conn.off('data', handleData)
|
|
|
|
|
|
|
|
conn.off('error', handleConnectionError)
|
|
|
|
|
|
|
|
conn.off('close', handleClose)
|
|
|
|
|
|
|
|
peer.off('error', handlePeerError)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [peer, password, shouldAttemptConnection])
|
|
|
|
}, [peer, password, shouldAttemptConnection])
|
|
|
|
|
|
|
|
|
|
|
|
const handleChangePassword = useCallback(
|
|
|
|
|
|
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
|
|
setPassword(e.target.value)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[],
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmitPassword = useCallback((ev) => {
|
|
|
|
const handleSubmitPassword = useCallback((ev) => {
|
|
|
|
ev.preventDefault()
|
|
|
|
ev.preventDefault()
|
|
|
|
setShouldAttemptConnection(true)
|
|
|
|
setShouldAttemptConnection(true)
|
|
|
|
@ -225,6 +263,7 @@ export default function Downloader({
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength)
|
|
|
|
const uInt8 = new Uint8Array(message.bytes as ArrayBuffer)
|
|
|
|
const uInt8 = new Uint8Array(message.bytes as ArrayBuffer)
|
|
|
|
fileStream.enqueue(uInt8)
|
|
|
|
fileStream.enqueue(uInt8)
|
|
|
|
if (message.final) {
|
|
|
|
if (message.final) {
|
|
|
|
@ -264,31 +303,87 @@ export default function Downloader({
|
|
|
|
startNextFileOrFinish()
|
|
|
|
startNextFileOrFinish()
|
|
|
|
}, [dataConnection, filesInfo])
|
|
|
|
}, [dataConnection, filesInfo])
|
|
|
|
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
const handleStopDownload = useCallback(() => {
|
|
|
|
return <div>Done!</div>
|
|
|
|
// TODO(@kern): Implement me
|
|
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const totalSize = filesInfo
|
|
|
|
|
|
|
|
? filesInfo.reduce((acc, info) => acc + info.size, 0)
|
|
|
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (done && filesInfo) {
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<VStack spacing="20px" w="100%">
|
|
|
|
|
|
|
|
<Text textStyle="description">
|
|
|
|
|
|
|
|
You downloaded {filesInfo.length} files.
|
|
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
<UploadFileList files={filesInfo} />
|
|
|
|
|
|
|
|
<Box w="100%">
|
|
|
|
|
|
|
|
<ProgressBar value={bytesDownloaded} max={totalSize} />
|
|
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
</VStack>
|
|
|
|
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (downloading) {
|
|
|
|
if (downloading && filesInfo) {
|
|
|
|
return <div>Downloading</div>
|
|
|
|
return (
|
|
|
|
|
|
|
|
<VStack spacing="20px" w="100%">
|
|
|
|
|
|
|
|
<Text textStyle="description">
|
|
|
|
|
|
|
|
You are about to start downloading {filesInfo.length} files.
|
|
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
<UploadFileList files={filesInfo} />
|
|
|
|
|
|
|
|
<Box w="100%">
|
|
|
|
|
|
|
|
<ProgressBar value={bytesDownloaded} max={totalSize} />
|
|
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
<StopButton onClick={handleStopDownload} isDownloading />
|
|
|
|
|
|
|
|
</VStack>
|
|
|
|
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (open) {
|
|
|
|
if (open && filesInfo) {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<VStack spacing="20px" w="100%">
|
|
|
|
<button onClick={handleStartDownload}>Download</button>
|
|
|
|
<Text textStyle="description">
|
|
|
|
</div>
|
|
|
|
You are about to start downloading {filesInfo.length} files.
|
|
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
<UploadFileList files={filesInfo} />
|
|
|
|
|
|
|
|
<DownloadButton onClick={handleStartDownload} />
|
|
|
|
|
|
|
|
</VStack>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
|
|
return <Loading text="Listing uploaded files" />
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO(@kern): Connect immediately, then have server respond if password is needed.
|
|
|
|
if (shouldAttemptConnection) {
|
|
|
|
if (shouldAttemptConnection) {
|
|
|
|
return <div>Loading...</div>
|
|
|
|
return <Loading text="Connecting to uploader" />
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<form action="#" method="post" onSubmit={handleSubmitPassword}>
|
|
|
|
<chakra.form
|
|
|
|
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
|
|
|
|
action="#"
|
|
|
|
<input type="password" value={password} onChange={handleChangePassword} />
|
|
|
|
method="post"
|
|
|
|
<button>Unlock</button>
|
|
|
|
onSubmit={handleSubmitPassword}
|
|
|
|
</form>
|
|
|
|
w="100%"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<VStack spacing="20px" w="100%">
|
|
|
|
|
|
|
|
{errorMessage ? (
|
|
|
|
|
|
|
|
<Text textStyle="descriptionError">{errorMessage}</Text>
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<Text textStyle="description">
|
|
|
|
|
|
|
|
This download requires a password.
|
|
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
<PasswordField
|
|
|
|
|
|
|
|
value={password}
|
|
|
|
|
|
|
|
onChange={setPassword}
|
|
|
|
|
|
|
|
isRequired
|
|
|
|
|
|
|
|
isInvalid={Boolean(errorMessage)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<UnlockButton onClick={handleSubmitPassword} />
|
|
|
|
|
|
|
|
</VStack>
|
|
|
|
|
|
|
|
</chakra.form>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|