<repo_name>marvinkome/webdrop<file_sep>/src/hooks/receive.ts
import Peer from "peerjs"
import React, { useState, useEffect, useRef } from "react"
import { setupPeerJS } from "utils"
import { FileBuilder } from "utils/file"
import { useBitrate } from "./bitrate"

export function useReceiveFile(dataConn?: Peer.DataConnection) {
    const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>()

    const [transferStarted, setTransferStarted] = useState(false)
    const [transferCompleted, setTransferCompleted] = useState(false)
    const [transferedSize, setTransferredSize] = useState(0)

    const bitrateObj = useBitrate(false)
    const fileBuilder = useRef<FileBuilder>()

    function receiveFile(data: any) {
        if (typeof data === "string") {
            console.log("[receiveFile] Receive file details", data)
            const parsedData = JSON.parse(data)

            setFileInfo({ name: parsedData.name, size: parsedData.size })

            fileBuilder.current = new FileBuilder(parsedData)
            fileBuilder.current.onAddChunk = (newSize) => {
                setTransferredSize(newSize)
            }

            fileBuilder.current.onComplete = () => {
                setTransferStarted(false)
                setTransferCompleted(true)

                bitrateObj.cancel()
                dataConn?.close()
            }

            return
        }

        if (!transferStarted) setTransferStarted(true)
        fileBuilder.current?.addChunk(data)
    }

    useEffect(() => {
        dataConn?.on("open", async () => {
            bitrateObj.init(dataConn?.peerConnection!)
            dataConn?.on("data", receiveFile)
        })
    }, [dataConn])

    return {
        fileInfo,
        transferCompleted,
        transferStarted,
        transferedSize,
        bitrate: bitrateObj.bitrate,
    }
}

export function useConnectionSetup() {
    const [peer, setPeer] = useState<Peer>()
    const [connection, setConnection] = useState<Peer.DataConnection>()

    // connection state
    const [hasError, setHasError] = useState(false)
    const [loading, setLoading] = useState(false)
    const [hasConnected, setHasConnected] = useState(false)

    // on mount set peer
    useEffect(() => {
        setupPeerJS().then((peer) => setPeer(peer))
    }, [])

    // listen for error
    useEffect(() => {
        if (!peer) return

        const errorListener = (err: any) => {
            if (err.type === "peer-unavailable") {
                setHasError(true)
                setLoading(false)
            }
        }

        peer.on("error", errorListener)

        return () => {
            peer.off("error", errorListener)
        }
    }, [peer])

    const connectWithSender = async (e: React.FormEvent) => {
        e.preventDefault()
        setHasError(false)

        // @ts-ignores
        const accessCode = e.target["code"].value

        // try to connect
        setLoading(true)
        const connection = peer?.connect(accessCode, {
            reliable: true,
        })

        connection?.on("open", () => {
            setHasError(false)
            setLoading(false)
            setHasConnected(true)
        })

        setConnection(connection)
    }

    return {
        state: {
            hasError,
            loading,
            hasConnected,
        },

        peer,
        dataConn: connection,
        connect: connectWithSender,
    }
}
<file_sep>/src/utils/file.ts
import { dowloadUrl } from "utils"

// I'm doing this because EventTarget doens't exist on server side
export function fileSplitterCreator(file: File, chunkSize?: number) {
    if (typeof window === "undefined") return

    class FileSplitter extends EventTarget {
        CHUNK_SIZE = 16384

        offset: number = 0
        fileReader = new FileReader()
        file: File

        paused: boolean = false

        constructor(file: File, chunkSize?: number) {
            super()

            this.file = file
            if (chunkSize && chunkSize !== 0) {
                this.CHUNK_SIZE = chunkSize
            }

            // add event listener for fileReader
            this.fileReader.addEventListener("load", (e) => {
                this.dispatchEvent(
                    new CustomEvent("split-file", {
                        detail: { chunk: e.target?.result },
                    })
                )

                this.offset += (e.target?.result as ArrayBuffer).byteLength
                this.dispatchEvent(
                    new CustomEvent("update-offset", {
                        detail: { offset: this.offset },
                    })
                )

                if (!this.paused && this.offset < this.file.size) {
                    this.readSlice(this.offset)
                }
            })
        }

        readSlice(offset: number) {
            const slice = this.file.slice(this.offset, offset + this.CHUNK_SIZE)
            this.fileReader.readAsArrayBuffer(slice)
        }

        start() {
            this.readSlice(0)
            this.dispatchEvent(new Event("start"))
        }

        pause() {
            this.paused = true
            this.dispatchEvent(new Event("pause"))
        }

        resume() {
            this.paused = false
            this.dispatchEvent(new Event("resume"))

            // resume reading from file
            this.readSlice(this.offset)
        }
    }

    return new FileSplitter(file, chunkSize)
}

export class FileBuilder {
    fileDetails: { name: string; size: number; type: string }

    chunkSize: number = 0
    fs: any
    fileEntry: any

    onComplete?: () => void
    onAddChunk?: (newSize: number) => void
    onError?: () => void

    constructor(details: { name: string; size: number; type: string }) {
        this.fileDetails = details
        this._requestFs()
    }

    addChunk(chunk: ArrayBuffer) {
        const onError = (e: any) => console.log(this._errorHandler(e))

        const onSuccess = (fileEntry: any) => {
            this.fileEntry = fileEntry

            fileEntry.createWriter((writer: any) => {
                const data = new Blob([chunk], { type: this.fileDetails.type })

                writer.onwriteend = () => this._onWrite(data.size)
                writer.onerror = (error: any) => {
                    this._errorHandler(error)
                    this.onError && this.onError()
                }

                writer.seek(writer.length)
                writer.write(data)
            }, onError)
        }

        this.fs.root.getFile(
            this.fileDetails.name,
            { create: !!!this.chunkSize },
            onSuccess,
            onError
        )
    }

    saveFile() {
        console.log("[FileBuilder] File ready for download")
        let fileUrl

        // @ts-ignore
        if (window.webkitRequestFileSystem) {
            fileUrl = this.fileEntry.toURL()
        } else {
            this.fileEntry.file((file: any) => {
                fileUrl = URL.createObjectURL(file)
            })
        }

        dowloadUrl(fileUrl, this.fileDetails.name)
        this.onComplete && this.onComplete()
    }

    _onWrite = (dataLength: number) => {
        this.chunkSize += dataLength

        const newSize = Math.floor((this.chunkSize / this.fileDetails.size) * 100)
        this.onAddChunk && this.onAddChunk(newSize)

        if (this.chunkSize >= this.fileDetails.size) {
            this.saveFile()
        }
    }

    _requestFs = () => {
        // @ts-ignore
        const requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem

        const onInit = (fs: any) => {
            this.fs = fs
            console.log("[FileBuilder]: File system setup - done.")
        }

        requestFileSystem(
            // @ts-ignore
            window.TEMPORARY,
            this.fileDetails.size,
            onInit,
            this._errorHandler
        )
    }

    _errorHandler = (e: any) => {
        let msg = ""

        switch (e.code) {
            // @ts-ignore
            case FileError.QUOTA_EXCEEDED_ERR:
                msg = "QUOTA_EXCEEDED_ERR"
                break
            // @ts-ignore
            case FileError.NOT_FOUND_ERR:
                msg = "NOT_FOUND_ERR"
                break
            // @ts-ignore
            case FileError.SECURITY_ERR:
                msg = "SECURITY_ERR"
                break
            // @ts-ignore
            case FileError.INVALID_MODIFICATION_ERR:
                msg = "INVALID_MODIFICATION_ERR"
                break
            // @ts-ignore
            case FileError.INVALID_STATE_ERR:
                msg = "INVALID_STATE_ERR"
                break
            default:
                msg = "Unknown Error"
                break
        }

        console.log("[FileSystem] Error: " + msg)
        return msg
    }
}
<file_sep>/README.md
# WebDrop

[https://webdrop.netlify.app](https://webdrop.netlify.app)
File transfer using WebRTC and PeerJS
<file_sep>/src/utils/index.ts
import { customAlphabet } from "nanoid"

const alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
const nanoid = customAlphabet(alphabet, 6)

export function dowloadUrl(url: string, name: string) {
    const a = document.createElement("a")

    a.href = url
    a.download = name

    a.click()
}

export function chunkFile(file: File, onSplit: (e: ProgressEvent<FileReader>) => void) {
    const chunkSize = 16384

    let offset = 0
    let fileReader = new FileReader()

    const readSlice = (o: number) => {
        console.log("[chunkFile] readSlice: ", o)
        const slice = file.slice(offset, o + chunkSize)
        fileReader.readAsArrayBuffer(slice)
    }

    fileReader.addEventListener("error", (error) => console.error("Error reading file:", error))
    fileReader.addEventListener("abort", (event) => console.log("File reading aborted:", event))
    fileReader.addEventListener("load", (e) => {
        console.log("[read-file] FileRead.onload", e)
        onSplit(e)

        offset += (e.target?.result as ArrayBuffer).byteLength
        if (offset < file.size) {
            readSlice(offset)
        }
    })

    return readSlice
}

export async function setupPeerJS() {
    const { default: PeerJS } = await import("peerjs")
    const code = nanoid()

    const options: any = {}
    if (process.env.NODE_ENV !== "production") {
        options.debug = 2
    }

    return new PeerJS(code, options)
}
<file_sep>/src/hooks/transfer.ts
import Peer from "peerjs"
import { useState, useEffect, useRef } from "react"
import { useToast } from "@chakra-ui/react"
import { fileSplitterCreator } from "utils/file"
import { setupPeerJS } from "utils"
import { useBitrate } from "./bitrate"

export function useFileTransfer(peer?: Peer, file?: File) {
    const [transferStarted, setTransferStarted] = useState(false)
    const [transferCompleted, setTransferCompleted] = useState(false)
    const [transferedSize, setTransferredSize] = useState(0)

    const bitrateObj = useBitrate(true)
    const fileSplitter = useRef<ReturnType<typeof fileSplitterCreator>>()

    function transferFile(dataConn: Peer.DataConnection) {
        if (!file) return

        // send file details to peer
        const fileDetails = JSON.stringify({
            name: file.name,
            size: file.size,
            type: file.type,
        })

        dataConn.send(fileDetails)

        // send file
        fileSplitter.current = fileSplitterCreator(file)
        fileSplitter.current?.start()

        // add listeners
        fileSplitter.current?.addEventListener("split-file", (e: any) => {
            setTransferStarted(true)
            dataConn.send(e.detail.chunk)
        })

        fileSplitter.current?.addEventListener("update-offset", (e: any) => {
            const offset = e.detail.offset

            setTransferredSize(Math.floor((offset / file.size) * 100))
            if (offset >= file.size) {
                setTransferStarted(false)
                setTransferCompleted(true)
            }
        })
    }

    useEffect(() => {
        if (!peer) return

        const onConnection = (dataConn: Peer.DataConnection) => {
            dataConn.on("open", async () => {
                console.log(dataConn.peerConnection)
                await bitrateObj.init(dataConn.peerConnection)
                transferFile(dataConn)
            })

            dataConn.on("close", bitrateObj.cancel)
        }

        peer.on("connection", onConnection)
    }, [peer, file])

    return {
        transferStarted,
        transferCompleted,
        transferedSize,
        bitrate: bitrateObj.bitrate,
    }
}

export function useTransferSetup() {
    const toast = useToast({ position: "top-right", isClosable: true })
    const [file, setFile] = useState<File>()
    const [peer, setPeer] = useState<Peer>()

    const onSelectFile = async (file: File) => {
        if (file.size === 0) {
            toast({ title: "File empty", description: "Please choose a valid file" })
            return
        }

        const peer = await setupPeerJS()

        setFile(file)
        setPeer(peer)
    }

    return {
        file,
        peer,
        onSelectFile,
    }
}
<file_sep>/src/hooks/bitrate.ts
import { useState, useRef } from "react"

export function useBitrate(sending: boolean) {
    const [bitrate, setBitrate] = useState(0)

    const statsInterval = useRef<any>()
    const timestampStart = useRef(0)
    const timestampPrev = useRef(0)
    const bytesPrev = useRef(0)

    async function calculateBitrate(remoteConnection: RTCPeerConnection) {
        if (remoteConnection.iceConnectionState !== "connected") return

        const stats = await remoteConnection.getStats()
        let activeCandidatePair: any
        stats.forEach((report) => {
            if (report.type === "transport") {
                activeCandidatePair = stats.get(report.selectedCandidatePairId)
            }
        })

        if (!activeCandidatePair) return
        if (timestampPrev.current === activeCandidatePair.timestamp) return

        const bytesNow = sending ? activeCandidatePair.bytesSent : activeCandidatePair.bytesReceived
        const bitrate = Math.round(
            ((bytesNow - bytesPrev.current) * 8) /
                ((activeCandidatePair.timestamp - timestampPrev.current) / 1000)
        )

        timestampPrev.current = activeCandidatePair.timestamp
        bytesPrev.current = bytesNow

        setBitrate(bitrate)
        return bitrate
    }

    async function init(remoteConnection: RTCPeerConnection) {
        timestampStart.current = new Date().getTime()
        timestampStart.current = new Date().getTime()
        timestampPrev.current = timestampStart.current
        statsInterval.current = setInterval(() => calculateBitrate(remoteConnection), 500)

        await calculateBitrate(remoteConnection)
    }

    function cancel() {
        clearInterval(statsInterval.current)
        setBitrate(0)
    }

    return { bitrate, init, cancel }
}
<file_sep>/src/server.ts
import express from "express"
import { createServer } from "http"
import next from "next"
import { ExpressPeerServer } from "peer"

const app = express()
const server = createServer(app)

const dev = process.env.NODE_ENV !== "production"
const port = process.env.PORT || 8081
const nextApp = next({ dev })
const handle = nextApp.getRequestHandler()

const peerServer = ExpressPeerServer(server, {
    path: "/peer",
})

app.use("/api", peerServer)

nextApp.prepare().then(() => {
    app.get("*", (req, res) => {
        return handle(req, res)
    })

    server.listen(port, () => {
        console.log(`Ready! on localhost:${port}`)
    })
})
