Add coturn support

pull/162/head
Alex Kern 12 months ago
parent 1c1cc16e60
commit 9e7d78c755
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

@ -1,4 +1,5 @@
.DS_Store .DS_Store
.next .next
node_modules node_modules
dist dist
.env

1
.gitignore vendored

@ -3,3 +3,4 @@
node_modules node_modules
dist dist
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
.env

@ -7,6 +7,20 @@ services:
- filepizza - filepizza
volumes: volumes:
- redis_data:/data - redis_data:/data
coturn:
image: coturn/coturn
ports:
- 3478:3478
- 3478:3478/udp
- 5349:5349
- 5349:5349/udp
- 49152-65535:49152-65535/udp
environment:
- DETECT_EXTERNAL_IP=yes
- DETECT_RELAY_IP=yes
command: -n --log-file=stdout --redis-userdb="ip=redis connect_timeout=30"
networks:
- filepizza
filepizza: filepizza:
build: . build: .
image: kern/filepizza:latest image: kern/filepizza:latest
@ -15,10 +29,13 @@ services:
environment: environment:
- PORT=80 - PORT=80
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- COTURN_ENABLED=true
networks: networks:
- filepizza - filepizza
depends_on: depends_on:
- redis - redis
env_file:
- .env
networks: networks:
filepizza: filepizza:

@ -7,6 +7,21 @@ services:
- filepizza - filepizza
volumes: volumes:
- redis_data:/data - redis_data:/data
coturn:
image: coturn/coturn
ports:
- 3478:3478
- 3478:3478/udp
- 5349:5349
- 5349:5349/udp
# Relay Ports
# - 49152-65535:49152-65535/udp
environment:
- DETECT_EXTERNAL_IP=yes
- DETECT_RELAY_IP=yes
command: -n --log-file=stdout --redis-userdb="ip=redis connect_timeout=30"
networks:
- filepizza
filepizza: filepizza:
build: . build: .
image: kern/filepizza:latest image: kern/filepizza:latest

@ -7,7 +7,7 @@
"homepage": "https://github.com/kern/filepizza", "homepage": "https://github.com/kern/filepizza",
"scripts": { "scripts": {
"dev": "next", "dev": "next",
"dev:redis": "docker compose up redis -d && REDIS_URL=redis://localhost:6379 next", "dev:full": "docker compose up redis coturn -d && COTURN_ENABLED=true REDIS_URL=redis://localhost:6379 next",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"start:peerjs": "./bin/peerjs.js", "start:peerjs": "./bin/peerjs.js",

@ -0,0 +1,37 @@
import { NextResponse } from 'next/server'
import crypto from 'crypto'
import { setTurnCredentials } from '../../../coturn'
const turnHost = process.env.TURN_HOST || '127.0.0.1'
export async function POST(): Promise<NextResponse> {
if (!process.env.COTURN_ENABLED) {
return NextResponse.json({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
})
}
// Generate ephemeral credentials
const username = crypto.randomBytes(8).toString('hex')
const password = crypto.randomBytes(8).toString('hex')
const ttl = 86400 // 24 hours
// Store credentials in Redis
await setTurnCredentials(username, password, ttl)
return NextResponse.json({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: [
`turn:${turnHost}:3478`,
`turns:${turnHost}:5349`
],
username,
credential: password
}
]
})
}

@ -34,9 +34,6 @@ export default function RootLayout({
return ( return (
<ViewTransitions> <ViewTransitions>
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head>
<meta name="monetization" content="$twitter.xrptipbot.com/kernio" />
</head>
<body> <body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<FilePizzaQueryClientProvider> <FilePizzaQueryClientProvider>

@ -1,6 +1,6 @@
import 'server-only' import 'server-only'
import config from './config' import config from './config'
import Redis from 'ioredis' import { Redis, getRedisClient } from './redisClient'
import { generateShortSlug, generateLongSlug } from './slugs' import { generateShortSlug, generateLongSlug } from './slugs'
import crypto from 'crypto' import crypto from 'crypto'
import { z } from 'zod' import { z } from 'zod'
@ -211,8 +211,8 @@ export class MemoryChannelRepo implements ChannelRepo {
export class RedisChannelRepo implements ChannelRepo { export class RedisChannelRepo implements ChannelRepo {
client: Redis.Redis client: Redis.Redis
constructor(redisURL: string) { constructor() {
this.client = new Redis(redisURL) this.client = getRedisClient()
} }
async createChannel( async createChannel(
@ -289,7 +289,7 @@ let _channelRepo: ChannelRepo | null = null
export function getOrCreateChannelRepo(): ChannelRepo { export function getOrCreateChannelRepo(): ChannelRepo {
if (!_channelRepo) { if (!_channelRepo) {
if (process.env.REDIS_URL) { if (process.env.REDIS_URL) {
_channelRepo = new RedisChannelRepo(process.env.REDIS_URL) _channelRepo = new RedisChannelRepo()
console.log('[ChannelRepo] Using Redis storage') console.log('[ChannelRepo] Using Redis storage')
} else { } else {
_channelRepo = new MemoryChannelRepo() _channelRepo = new MemoryChannelRepo()

@ -231,8 +231,19 @@ export default function Downloader({
) )
} }
if (!isConnected) { if (isPasswordRequired) {
return <ConnectingToUploader /> return (
<PasswordEntry errorMessage={errorMessage} onSubmit={submitPassword} />
)
}
if (errorMessage) {
return (
<>
<ErrorMessage message={errorMessage} />
<ReturnHome />
</>
)
} }
if (isDownloading && filesInfo) { if (isDownloading && filesInfo) {
@ -250,10 +261,8 @@ export default function Downloader({
return <ReadyToDownload filesInfo={filesInfo} onStart={startDownload} /> return <ReadyToDownload filesInfo={filesInfo} onStart={startDownload} />
} }
if (isPasswordRequired) { if (!isConnected) {
return ( return <ConnectingToUploader />
<PasswordEntry errorMessage={errorMessage} onSubmit={submitPassword} />
)
} }
return <Loading text="Uh oh... Something went wrong." /> return <Loading text="Uh oh... Something went wrong." />

@ -10,6 +10,7 @@ import React, {
} from 'react' } from 'react'
import Loading from './Loading' import Loading from './Loading'
import Peer from 'peerjs' import Peer from 'peerjs'
import { ErrorMessage } from './ErrorMessage'
export type WebRTCPeerValue = { export type WebRTCPeerValue = {
peer: Peer peer: Peer
@ -30,7 +31,18 @@ let globalPeer: Peer | null = null
async function getOrCreateGlobalPeer(): Promise<Peer> { async function getOrCreateGlobalPeer(): Promise<Peer> {
if (!globalPeer) { if (!globalPeer) {
globalPeer = new Peer() const response = await fetch('/api/ice', {
method: 'POST'
})
const { iceServers } = await response.json()
console.log('[WebRTCProvider] ICE servers:', iceServers)
globalPeer = new Peer({
debug: 3,
config: {
iceServers
}
})
} }
if (globalPeer.id) { if (globalPeer.id) {
@ -56,8 +68,10 @@ export default function WebRTCPeerProvider({
}): JSX.Element { }): JSX.Element {
const [peerValue, setPeerValue] = useState<Peer | null>(globalPeer) const [peerValue, setPeerValue] = useState<Peer | null>(globalPeer)
const [isStopped, setIsStopped] = useState(false) const [isStopped, setIsStopped] = useState(false)
const [error, setError] = useState<Error | null>(null)
const stop = useCallback(() => { const stop = useCallback(() => {
console.log('[WebRTCProvider] Stopping peer')
globalPeer?.destroy() globalPeer?.destroy()
globalPeer = null globalPeer = null
setPeerValue(null) setPeerValue(null)
@ -65,11 +79,15 @@ export default function WebRTCPeerProvider({
}, []) }, [])
useEffect(() => { useEffect(() => {
getOrCreateGlobalPeer().then(setPeerValue) getOrCreateGlobalPeer().then(setPeerValue).catch(setError)
}, []) }, [])
const value = useMemo(() => ({ peer: peerValue!, stop }), [peerValue, stop]) const value = useMemo(() => ({ peer: peerValue!, stop }), [peerValue, stop])
if (error) {
return <ErrorMessage message={error.message} />
}
if (isStopped) { if (isStopped) {
return <></> return <></>
} }

@ -0,0 +1,30 @@
import crypto from 'crypto'
import { getRedisClient } from './redisClient'
function generateHMACKey(username: string, realm: string, password: string): string {
const str = `${username}:${realm}:${password}`
return crypto.createHash('md5').update(str).digest('hex')
}
export async function setTurnCredentials(
username: string,
password: string,
ttl: number
): Promise<void> {
if (!process.env.COTURN_ENABLED) {
return
}
const realm = process.env.TURN_REALM || 'file.pizza'
if (!realm) {
throw new Error('TURN_REALM environment variable not set')
}
const redis = getRedisClient()
const hmacKey = generateHMACKey(username, realm, password)
const key = `turn/realm/${realm}/user/${username}/key`
await redis.setex(key, ttl, hmacKey)
}

@ -62,6 +62,7 @@ export function useDownloader(uploaderPeerID: string): {
setDataConnection(conn) setDataConnection(conn)
const handleOpen = () => { const handleOpen = () => {
console.log('[Downloader] connection opened')
setIsConnected(true) setIsConnected(true)
conn.send({ conn.send({
type: MessageType.RequestInfo, type: MessageType.RequestInfo,
@ -77,6 +78,7 @@ export function useDownloader(uploaderPeerID: string): {
const handleData = (data: unknown) => { const handleData = (data: unknown) => {
try { try {
const message = decodeMessage(data) const message = decodeMessage(data)
console.log('[Downloader] received message', message.type)
switch (message.type) { switch (message.type) {
case MessageType.PasswordRequired: case MessageType.PasswordRequired:
setIsPasswordRequired(true) setIsPasswordRequired(true)
@ -90,21 +92,22 @@ export function useDownloader(uploaderPeerID: string): {
setRotating(true) setRotating(true)
break break
case MessageType.Error: case MessageType.Error:
console.error(message.error) console.error('[Downloader] received error message:', message.error)
setErrorMessage(message.error) setErrorMessage(message.error)
conn.close() conn.close()
break break
case MessageType.Report: case MessageType.Report:
// Hard-redirect downloader to reported page console.log('[Downloader] received report message, redirecting')
window.location.href = '/reported' window.location.href = '/reported'
break break
} }
} catch (err) { } catch (err) {
console.error(err) console.error('[Downloader] error handling message:', err)
} }
} }
const handleClose = () => { const handleClose = () => {
console.log('[Downloader] connection closed')
setRotating(false) setRotating(false)
setDataConnection(null) setDataConnection(null)
setIsConnected(false) setIsConnected(false)
@ -112,7 +115,7 @@ export function useDownloader(uploaderPeerID: string): {
} }
const handleError = (err: Error) => { const handleError = (err: Error) => {
console.error(err) console.error('[Downloader] connection error:', err)
setErrorMessage(cleanErrorMessage(err.message)) setErrorMessage(cleanErrorMessage(err.message))
if (conn.open) conn.close() if (conn.open) conn.close()
else handleClose() else handleClose()
@ -125,6 +128,7 @@ export function useDownloader(uploaderPeerID: string): {
peer.on('error', handleError) peer.on('error', handleError)
return () => { return () => {
console.log('[Downloader] cleaning up connection')
if (conn.open) { if (conn.open) {
conn.close() conn.close()
} else { } else {
@ -144,6 +148,7 @@ export function useDownloader(uploaderPeerID: string): {
const submitPassword = useCallback( const submitPassword = useCallback(
(pass: string) => { (pass: string) => {
if (!dataConnection) return if (!dataConnection) return
console.log('[Downloader] submitting password')
dataConnection.send({ dataConnection.send({
type: MessageType.UsePassword, type: MessageType.UsePassword,
password: pass, password: pass,
@ -154,6 +159,7 @@ export function useDownloader(uploaderPeerID: string): {
const startDownload = useCallback(() => { const startDownload = useCallback(() => {
if (!filesInfo || !dataConnection) return if (!filesInfo || !dataConnection) return
console.log('[Downloader] starting download')
setIsDownloading(true) setIsDownloading(true)
const fileStreamByPath: Record< const fileStreamByPath: Record<
@ -182,6 +188,7 @@ export function useDownloader(uploaderPeerID: string): {
let nextFileIndex = 0 let nextFileIndex = 0
const startNextFileOrFinish = () => { const startNextFileOrFinish = () => {
if (nextFileIndex >= filesInfo.length) return if (nextFileIndex >= filesInfo.length) return
console.log('[Downloader] starting next file:', filesInfo[nextFileIndex].fileName)
dataConnection.send({ dataConnection.send({
type: MessageType.Start, type: MessageType.Start,
fileName: filesInfo[nextFileIndex].fileName, fileName: filesInfo[nextFileIndex].fileName,
@ -193,12 +200,13 @@ export function useDownloader(uploaderPeerID: string): {
processChunk.current = (message: z.infer<typeof ChunkMessage>) => { processChunk.current = (message: z.infer<typeof ChunkMessage>) => {
const fileStream = fileStreamByPath[message.fileName] const fileStream = fileStreamByPath[message.fileName]
if (!fileStream) { if (!fileStream) {
console.error('no stream found for ' + message.fileName) console.error('[Downloader] no stream found for', message.fileName)
return return
} }
setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength) setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength)
fileStream.enqueue(new Uint8Array(message.bytes as ArrayBuffer)) fileStream.enqueue(new Uint8Array(message.bytes as ArrayBuffer))
if (message.final) { if (message.final) {
console.log('[Downloader] finished receiving', message.fileName)
fileStream.close() fileStream.close()
startNextFileOrFinish() startNextFileOrFinish()
} }
@ -217,12 +225,13 @@ export function useDownloader(uploaderPeerID: string): {
downloadPromise downloadPromise
.then(() => { .then(() => {
console.log('[Downloader] all files downloaded')
dataConnection.send({ type: MessageType.Done } as z.infer< dataConnection.send({ type: MessageType.Done } as z.infer<
typeof Message typeof Message
>) >)
setDone(true) setDone(true)
}) })
.catch(console.error) .catch((err) => console.error('[Downloader] download error:', err))
startNextFileOrFinish() startNextFileOrFinish()
}, [dataConnection, filesInfo]) }, [dataConnection, filesInfo])
@ -230,6 +239,7 @@ export function useDownloader(uploaderPeerID: string): {
const stopDownload = useCallback(() => { const stopDownload = useCallback(() => {
// TODO(@kern): Continue here with stop / pause logic // TODO(@kern): Continue here with stop / pause logic
if (dataConnection) { if (dataConnection) {
console.log('[Downloader] pausing download')
dataConnection.send({ type: MessageType.Pause }) dataConnection.send({ type: MessageType.Pause })
dataConnection.close() dataConnection.close()
} }

@ -24,15 +24,22 @@ export function useUploaderChannel(
const { isLoading, error, data } = useQuery({ const { isLoading, error, data } = useQuery({
queryKey: ['uploaderChannel', uploaderPeerID], queryKey: ['uploaderChannel', uploaderPeerID],
queryFn: async () => { queryFn: async () => {
console.log('[UploaderChannel] creating new channel for peer', uploaderPeerID)
const response = await fetch('/api/create', { const response = await fetch('/api/create', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploaderPeerID }), body: JSON.stringify({ uploaderPeerID }),
}) })
if (!response.ok) { if (!response.ok) {
console.error('[UploaderChannel] failed to create channel:', response.status)
throw new Error('Network response was not ok') throw new Error('Network response was not ok')
} }
return response.json() const data = await response.json()
console.log('[UploaderChannel] channel created successfully:', {
longSlug: data.longSlug,
shortSlug: data.shortSlug
})
return data
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
@ -48,15 +55,19 @@ export function useUploaderChannel(
const renewMutation = useMutation({ const renewMutation = useMutation({
mutationFn: async ({ secret: s }: { secret: string }) => { mutationFn: async ({ secret: s }: { secret: string }) => {
console.log('[UploaderChannel] renewing channel for slug', shortSlug)
const response = await fetch('/api/renew', { const response = await fetch('/api/renew', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug: shortSlug, secret: s }), body: JSON.stringify({ slug: shortSlug, secret: s }),
}) })
if (!response.ok) { if (!response.ok) {
console.error('[UploaderChannel] failed to renew channel', response.status)
throw new Error('Network response was not ok') throw new Error('Network response was not ok')
} }
return response.json() const data = await response.json()
console.log('[UploaderChannel] channel renewed successfully')
return data
}, },
}) })
@ -67,6 +78,7 @@ export function useUploaderChannel(
const run = (): void => { const run = (): void => {
timeout = setTimeout(() => { timeout = setTimeout(() => {
console.log('[UploaderChannel] scheduling channel renewal in', renewInterval, 'ms')
renewMutation.mutate({ secret }) renewMutation.mutate({ secret })
run() run()
}, renewInterval) }, renewInterval)
@ -75,7 +87,10 @@ export function useUploaderChannel(
run() run()
return () => { return () => {
if (timeout) clearTimeout(timeout) if (timeout) {
console.log('[UploaderChannel] clearing renewal timeout')
clearTimeout(timeout)
}
} }
}, [secret, shortSlug, renewMutation, renewInterval]) }, [secret, shortSlug, renewMutation, renewInterval])
@ -83,16 +98,15 @@ export function useUploaderChannel(
if (!shortSlug || !secret) return if (!shortSlug || !secret) return
const handleUnload = (): void => { const handleUnload = (): void => {
console.log('[UploaderChannel] destroying channel on page unload')
// Using sendBeacon for best-effort delivery during page unload // Using sendBeacon for best-effort delivery during page unload
navigator.sendBeacon('/api/destroy', JSON.stringify({ slug: shortSlug })) navigator.sendBeacon('/api/destroy', JSON.stringify({ slug: shortSlug }))
} }
window.addEventListener('beforeunload', handleUnload) window.addEventListener('beforeunload', handleUnload)
window.addEventListener('unload', handleUnload)
return () => { return () => {
window.removeEventListener('beforeunload', handleUnload) window.removeEventListener('beforeunload', handleUnload)
window.removeEventListener('unload', handleUnload)
} }
}, [shortSlug, secret]) }, [shortSlug, secret])

@ -34,11 +34,14 @@ export function useUploaderConnections(
const [connections, setConnections] = useState<Array<UploaderConnection>>([]) const [connections, setConnections] = useState<Array<UploaderConnection>>([])
useEffect(() => { useEffect(() => {
console.log('[UploaderConnections] initializing with', files.length, 'files')
const cleanupHandlers: Array<() => void> = [] const cleanupHandlers: Array<() => void> = []
const listener = (conn: DataConnection) => { const listener = (conn: DataConnection) => {
console.log('[UploaderConnections] new connection from peer', conn.peer)
// If the connection is a report, we need to hard-redirect the uploader to the reported page to prevent them from uploading more files. // If the connection is a report, we need to hard-redirect the uploader to the reported page to prevent them from uploading more files.
if (conn.metadata?.type === 'report') { if (conn.metadata?.type === 'report') {
console.log('[UploaderConnections] received report connection, redirecting')
// Broadcast report message to all connections // Broadcast report message to all connections
connections.forEach((c) => { connections.forEach((c) => {
c.dataConnection.send({ c.dataConnection.send({
@ -76,8 +79,14 @@ export function useUploaderConnections(
const onData = (data: any): void => { const onData = (data: any): void => {
try { try {
const message = decodeMessage(data) const message = decodeMessage(data)
console.log('[UploaderConnections] received message:', message.type)
switch (message.type) { switch (message.type) {
case MessageType.RequestInfo: { case MessageType.RequestInfo: {
console.log('[UploaderConnections] client info:', {
browser: `${message.browserName} ${message.browserVersion}`,
os: `${message.osName} ${message.osVersion}`,
mobile: message.mobileVendor ? `${message.mobileVendor} ${message.mobileModel}` : 'N/A'
})
const newConnectionState = { const newConnectionState = {
browserName: message.browserName, browserName: message.browserName,
browserVersion: message.browserVersion, browserVersion: message.browserVersion,
@ -88,6 +97,7 @@ export function useUploaderConnections(
} }
if (password) { if (password) {
console.log('[UploaderConnections] password required, requesting authentication')
const request: Message = { const request: Message = {
type: MessageType.PasswordRequired, type: MessageType.PasswordRequired,
} }
@ -128,6 +138,7 @@ export function useUploaderConnections(
} }
}) })
console.log('[UploaderConnections] sending file info:', fileInfo)
const request: Message = { const request: Message = {
type: MessageType.Info, type: MessageType.Info,
files: fileInfo, files: fileInfo,
@ -138,8 +149,10 @@ export function useUploaderConnections(
} }
case MessageType.UsePassword: { case MessageType.UsePassword: {
console.log('[UploaderConnections] password attempt received')
const { password: submittedPassword } = message const { password: submittedPassword } = message
if (submittedPassword === password) { if (submittedPassword === password) {
console.log('[UploaderConnections] password correct')
updateConnection((draft) => { updateConnection((draft) => {
if ( if (
draft.status !== UploaderConnectionStatus.Authenticating && draft.status !== UploaderConnectionStatus.Authenticating &&
@ -167,6 +180,7 @@ export function useUploaderConnections(
conn.send(request) conn.send(request)
} else { } else {
console.log('[UploaderConnections] password incorrect')
updateConnection((draft) => { updateConnection((draft) => {
if ( if (
draft.status !== UploaderConnectionStatus.Authenticating draft.status !== UploaderConnectionStatus.Authenticating
@ -192,6 +206,7 @@ export function useUploaderConnections(
case MessageType.Start: { case MessageType.Start: {
const fileName = message.fileName const fileName = message.fileName
let offset = message.offset let offset = message.offset
console.log('[UploaderConnections] starting transfer of', fileName, 'from offset', offset)
const file = validateOffset(files, fileName, offset) const file = validateOffset(files, fileName, offset)
const sendNextChunkAsync = () => { const sendNextChunkAsync = () => {
@ -211,7 +226,7 @@ export function useUploaderConnections(
updateConnection((draft) => { updateConnection((draft) => {
offset = end offset = end
if (final) { if (final) {
console.log('final chunk', draft.completedFiles + 1) console.log('[UploaderConnections] completed file', fileName, '- file', draft.completedFiles + 1, 'of', draft.totalFiles)
return { return {
...draft, ...draft,
status: UploaderConnectionStatus.Ready, status: UploaderConnectionStatus.Ready,
@ -253,6 +268,7 @@ export function useUploaderConnections(
} }
case MessageType.Pause: { case MessageType.Pause: {
console.log('[UploaderConnections] transfer paused')
updateConnection((draft) => { updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Uploading) { if (draft.status !== UploaderConnectionStatus.Uploading) {
return draft return draft
@ -272,6 +288,7 @@ export function useUploaderConnections(
} }
case MessageType.Done: { case MessageType.Done: {
console.log('[UploaderConnections] transfer completed successfully')
updateConnection((draft) => { updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Ready) { if (draft.status !== UploaderConnectionStatus.Ready) {
return draft return draft
@ -287,11 +304,12 @@ export function useUploaderConnections(
} }
} }
} catch (err) { } catch (err) {
console.error(err) console.error('[UploaderConnections] error handling message:', err)
} }
} }
const onClose = (): void => { const onClose = (): void => {
console.log('[UploaderConnections] connection closed')
if (sendChunkTimeout) { if (sendChunkTimeout) {
clearTimeout(sendChunkTimeout) clearTimeout(sendChunkTimeout)
} }
@ -326,6 +344,7 @@ export function useUploaderConnections(
peer.on('connection', listener) peer.on('connection', listener)
return () => { return () => {
console.log('[UploaderConnections] cleaning up connections')
peer.off('connection', listener) peer.off('connection', listener)
cleanupHandlers.forEach((fn) => fn()) cleanupHandlers.forEach((fn) => fn())
} }

@ -0,0 +1,12 @@
import Redis from 'ioredis'
export { Redis }
let redisClient: Redis.Redis | null = null
export function getRedisClient(): Redis.Redis {
if (!redisClient) {
redisClient = new Redis(process.env.REDIS_URL)
}
return redisClient
}
Loading…
Cancel
Save