project-nomad/admin/inertia/components/ZimUploader/index.tsx

96 lines
3.4 KiB
TypeScript

import Uppy from '@uppy/core'
import Dashboard from '@uppy/react/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
import { useEffect, useRef, useState } from 'react'
interface ZimUploaderProps {
onUploadComplete: (added: number) => void
existingFilenames: string[]
}
export default function ZimUploader({ onUploadComplete, existingFilenames }: ZimUploaderProps) {
const existingFilenamesRef = useRef(existingFilenames)
useEffect(() => {
existingFilenamesRef.current = existingFilenames
}, [existingFilenames])
// Keep the latest callback in a ref so the Uppy lifecycle effect below does
// not depend on it. The parent passes a new inline function on every render,
// and if that identity were in the effect deps, a re-render mid-upload (e.g.
// a window-focus refetch) would tear down and `uppy.destroy()` the instance,
// aborting the in-flight upload.
const onUploadCompleteRef = useRef(onUploadComplete)
useEffect(() => {
onUploadCompleteRef.current = onUploadComplete
}, [onUploadComplete])
const [uppy] = useState(() =>
new Uppy({
restrictions: {
maxNumberOfFiles: 5,
allowedFileTypes: ['.zim'],
},
autoProceed: false,
}).use(XHRUpload, {
endpoint: '/api/zim/upload',
fieldName: 'file',
getResponseError: (responseText) => {
try {
const body = JSON.parse(responseText)
if (body?.message) return new Error(body.message)
} catch {}
return new Error('Upload failed')
},
})
)
useEffect(() => {
const handleFileAdded = (file: { id: string; name: string }) => {
if (existingFilenamesRef.current.includes(file.name)) {
uppy.removeFile(file.id)
uppy.info('A ZIM file with that name already exists', 'error', 6000)
return
}
const isWikipedia = (name: string) => name.startsWith('wikipedia_en_')
if (isWikipedia(file.name)) {
const alreadyQueued = uppy.getFiles().some((f) => f.id !== file.id && isWikipedia(f.name))
if (alreadyQueued) {
uppy.removeFile(file.id)
uppy.info('Only one Wikipedia file can be uploaded at a time', 'error', 6000)
}
}
}
const handleComplete = (result: {
successful: Array<{ response?: { body?: { added?: number } } }>
failed: Array<unknown>
}) => {
// Uppy emits `complete` even when uploads fail or are aborted. Only treat
// the run as a success when nothing failed — otherwise leave the uploader
// open so the Dashboard can surface the error instead of closing it with a
// false "Upload complete" toast.
if (result.failed.length > 0) return
const added = result.successful.reduce((sum, f) => sum + (f.response?.body?.added ?? 0), 0)
onUploadCompleteRef.current(added)
}
uppy.on('file-added', handleFileAdded)
uppy.on('complete', handleComplete)
return () => {
uppy.off('file-added', handleFileAdded)
uppy.off('complete', handleComplete)
uppy.destroy()
}
}, [uppy])
return (
<Dashboard
uppy={uppy}
width="100%"
height={300}
note="ZIM files only. Large files (up to 20 GB) are supported. For best results, upload from the same machine or over a stable LAN connection. Larger files should be copied directly to the storage volume"
/>
)
}