import {createCryptoBackend} from './crypto-backend.js'
import {getServers, pickRandomServer} from './servers.js'
import {createProgressRing} from './progress.js'
import {
newXFTPAgent, closeXFTPAgent, uploadFile, encodeDescriptionURI,
type EncryptedFileMetadata
} from '../src/agent.js'
const MAX_SIZE = 100 * 1024 * 1024
export function initUpload(app: HTMLElement) {
app.innerHTML = `
SimpleX File Transfer
Drag & drop a file here
or
Max 100 MB
File uploaded
Files are typically available for 48 hours.
Your file was encrypted in the browser before upload — the server never sees file contents.
The link contains the decryption key in the hash fragment, which the browser never sends to any server.
For maximum security, use the SimpleX app.
`
const dropZone = document.getElementById('drop-zone')!
const fileInput = document.getElementById('file-input') as HTMLInputElement
const progressStage = document.getElementById('upload-progress')!
const completeStage = document.getElementById('upload-complete')!
const errorStage = document.getElementById('upload-error')!
const progressContainer = document.getElementById('progress-container')!
const statusText = document.getElementById('upload-status')!
const cancelBtn = document.getElementById('cancel-btn')!
const shareLink = document.getElementById('share-link') as HTMLInputElement
const copyBtn = document.getElementById('copy-btn')!
const errorMsg = document.getElementById('error-msg')!
const retryBtn = document.getElementById('retry-btn')!
let aborted = false
let pendingFile: File | null = null
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over') })
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'))
dropZone.addEventListener('drop', e => {
e.preventDefault()
dropZone.classList.remove('drag-over')
const f = e.dataTransfer?.files[0]
if (f) startUpload(f)
})
fileInput.addEventListener('change', () => {
if (fileInput.files?.[0]) startUpload(fileInput.files[0])
})
retryBtn.addEventListener('click', () => {
if (pendingFile) startUpload(pendingFile)
})
function showStage(stage: HTMLElement) {
for (const s of [dropZone, progressStage, completeStage, errorStage]) s.hidden = true
stage.hidden = false
}
function showError(msg: string) {
errorMsg.textContent = msg
showStage(errorStage)
}
async function startUpload(file: File) {
pendingFile = file
aborted = false
if (file.size > MAX_SIZE) {
showError(`File too large (${formatSize(file.size)}). Maximum is 100 MB.`)
return
}
if (file.size === 0) {
showError('File is empty.')
return
}
showStage(progressStage)
const ring = createProgressRing()
progressContainer.innerHTML = ''
progressContainer.appendChild(ring.canvas)
statusText.textContent = 'Encrypting…'
const backend = createCryptoBackend()
const agent = newXFTPAgent()
cancelBtn.onclick = () => {
aborted = true
backend.cleanup().catch(() => {})
closeXFTPAgent(agent)
showStage(dropZone)
}
try {
const fileData = new Uint8Array(await file.arrayBuffer())
if (aborted) return
const encrypted = await backend.encrypt(fileData, file.name, (done, total) => {
ring.update(done / total * 0.3)
})
if (aborted) return
statusText.textContent = 'Uploading…'
const metadata: EncryptedFileMetadata = {
digest: encrypted.digest,
key: encrypted.key,
nonce: encrypted.nonce,
chunkSizes: encrypted.chunkSizes
}
const servers = getServers()
const server = pickRandomServer(servers)
const result = await uploadFile(agent, server, metadata, {
readChunk: (off, sz) => backend.readChunk(off, sz),
onProgress: (uploaded, total) => {
ring.update(0.3 + (uploaded / total) * 0.7)
}
})
if (aborted) return
const url = window.location.origin + window.location.pathname + '#' + result.uri
shareLink.value = url
showStage(completeStage)
copyBtn.onclick = () => {
navigator.clipboard.writeText(url).then(() => {
copyBtn.textContent = 'Copied!'
setTimeout(() => { copyBtn.textContent = 'Copy' }, 2000)
})
}
} catch (err: any) {
if (!aborted) showError(err?.message ?? String(err))
} finally {
await backend.cleanup().catch(() => {})
closeXFTPAgent(agent)
}
}
}
function formatSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}