Upload con Uppy
Uppy è un popolare uploader di file open source che gestisce automaticamente chunk, nuovi tentativi e tracciamento dell’avanzamento. Questa guida mostra come integrare Uppy con l’API Ignite Video per upload a file singolo e multipart.
Uppy gestisce automaticamente gli upload multipart per i file grandi ed è l’approccio consigliato per le applicazioni in produzione.
Per un’implementazione manuale senza dipendenze esterne, consulta la guida Upload multipart.
Installazione
Installa i pacchetti Uppy necessari:
npm
npm install @uppy/core @uppy/aws-s3Configurazione di base
Configurare upload a file singolo
Per file inferiori a 100 MB, usa l’upload diretto con un URL pre-firmato.
import Uppy from '@uppy/core'
import AwsS3 from '@uppy/aws-s3'
async function uploadVideo(file, title, token) {
const uppy = new Uppy()
// Step 1: Create video and get signed URL
const response = await fetch('https://app.ignitevideo.cloud/api/videos/upload', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
mimeType: file.type
})
})
const { videoId, signedUrl } = await response.json()
// Step 2: Configure Uppy for direct upload
uppy.use(AwsS3, {
shouldUseMultipart: false,
getUploadParameters: () => ({
method: 'PUT',
url: signedUrl,
headers: {
'Content-Type': file.type
}
})
})
// Step 3: Add file and upload
uppy.addFile({
name: file.name,
type: file.type,
data: file
})
await uppy.upload()
// Clean up
uppy.destroy()
return videoId
}Configurare upload multipart
Per file superiori a 100 MB, usa l’upload multipart per maggiore affidabilità.
import Uppy from '@uppy/core'
import AwsS3 from '@uppy/aws-s3'
async function uploadLargeVideo(file, title, token) {
const uppy = new Uppy()
// Step 1: Create video with multipart flag
const response = await fetch('https://app.ignitevideo.cloud/api/videos/upload', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
mimeType: file.type,
useMultipart: true
})
})
const { videoId, multipartUpload } = await response.json()
const { uploadId, key } = multipartUpload
// Step 2: Configure Uppy for multipart upload
uppy.use(AwsS3, {
shouldUseMultipart: true,
limit: 5, // Max concurrent part uploads
retryDelays: [0, 1000, 3000, 5000], // Retry delays in ms
// Return the existing multipart upload info
createMultipartUpload: () => Promise.resolve({ uploadId, key }),
// Get signed URL for each part
signPart: async (file, { partNumber }) => {
const response = await fetch(
`https://app.ignitevideo.cloud/api/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`,
{
headers: { 'Authorization': `Bearer ${token}` }
}
)
const data = await response.json()
return { url: data.url }
},
// Complete the multipart upload
completeMultipartUpload: async (file, { uploadId, key, parts }) => {
const response = await fetch(
`https://app.ignitevideo.cloud/api/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ parts })
}
)
return response.json()
}
})
// Step 3: Add file and upload
uppy.addFile({
name: file.name,
type: file.type,
data: file
})
await uppy.upload()
// Clean up
uppy.destroy()
return videoId
}Esempio completo con tracciamento dell’avanzamento
Ecco un’implementazione completa che sceglie automaticamente tra upload a file singolo e multipart in base alle dimensioni del file:
import Uppy from '@uppy/core'
import AwsS3 from '@uppy/aws-s3'
const API_BASE = 'https://app.ignitevideo.cloud/api'
const MULTIPART_THRESHOLD = 100 * 1024 * 1024 // 100MB
export async function uploadVideo(file, title, token, onProgress) {
const useMultipart = file.size > MULTIPART_THRESHOLD
// Step 1: Initialize video on server
const initResponse = await fetch(`${API_BASE}/videos/upload`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
mimeType: file.type,
useMultipart
})
})
if (!initResponse.ok) {
throw new Error('Failed to initialize upload')
}
const initData = await initResponse.json()
const { videoId } = initData
// Step 2: Create and configure Uppy
const uppy = new Uppy({
restrictions: {
maxFileSize: 60 * 1024 * 1024 * 1024,
allowedFileTypes: ['video/*']
}
})
if (!useMultipart) {
// Single file upload
uppy.use(AwsS3, {
shouldUseMultipart: false,
getUploadParameters: () => ({
method: 'PUT',
url: initData.signedUrl,
headers: { 'Content-Type': file.type }
})
})
} else {
// Multipart upload
const { uploadId, key } = initData.multipartUpload
uppy.use(AwsS3, {
shouldUseMultipart: true,
limit: 5,
retryDelays: [0, 1000, 3000, 5000],
createMultipartUpload: () => Promise.resolve({ uploadId, key }),
signPart: async (file, { partNumber }) => {
const response = await fetch(
`${API_BASE}/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
)
if (!response.ok) throw new Error(`Failed to sign part ${partNumber}`)
const data = await response.json()
return { url: data.url }
},
completeMultipartUpload: async (file, { uploadId, key, parts }) => {
const response = await fetch(
`${API_BASE}/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ parts })
}
)
if (!response.ok) throw new Error('Failed to complete multipart upload')
return response.json()
}
})
}
// Step 3: Set up event handlers
uppy.on('upload-progress', (file, progress) => {
const percent = (progress.bytesUploaded / progress.bytesTotal) * 100
onProgress?.(percent)
})
uppy.on('upload-error', (file, error) => {
console.error('Upload error:', error)
})
// Step 4: Add file and upload
uppy.addFile({
name: file.name,
type: file.type,
data: file
})
await uppy.upload()
// Clean up
uppy.destroy()
return videoId
}Integrazione React
Ecco un componente React di esempio che usa Uppy:
import React, { useState, useCallback } from 'react'
import { uploadVideo } from './uploadVideo' // The function from above
function VideoUploader({ token }) {
const [progress, setProgress] = useState(0)
const [status, setStatus] = useState('idle') // idle, uploading, complete, error
const [videoId, setVideoId] = useState(null)
const handleFileChange = useCallback(async (event) => {
const file = event.target.files?.[0]
if (!file) return
const title = file.name.replace(/\.[^/.]+$/, '') // Remove extension
setStatus('uploading')
setProgress(0)
try {
const id = await uploadVideo(file, title, token, (percent) => {
setProgress(Math.round(percent))
})
setVideoId(id)
setStatus('complete')
} catch (error) {
console.error('Upload failed:', error)
setStatus('error')
}
}, [token])
return (
<div>
<input
type="file"
accept="video/*"
onChange={handleFileChange}
disabled={status === 'uploading'}
/>
{status === 'uploading' && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
{status === 'complete' && (
<p>Upload complete! Video ID: {videoId}</p>
)}
{status === 'error' && (
<p>Upload failed. Please try again.</p>
)}
</div>
)
}
export default VideoUploaderAvanzato: recupero URL in batch
Per prestazioni migliori con file grandi, recupera gli URL firmati in batch invece che uno alla volta. Riduce sensibilmente il numero di chiamate API per gli upload di grandi dimensioni.
import Uppy from '@uppy/core'
import AwsS3 from '@uppy/aws-s3'
const API_BASE = 'https://app.ignitevideo.cloud/api'
const BATCH_SIZE = 100
async function uploadWithBatchFetching(file, title, token) {
// Initialize upload
const initResponse = await fetch(`${API_BASE}/videos/upload`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
mimeType: file.type,
useMultipart: true
})
})
const { videoId, multipartUpload } = await initResponse.json()
const { uploadId, key } = multipartUpload
// Batch fetching state
const urlCache = new Map()
const batchPromises = new Map()
const getBatchNumber = (partNumber) => Math.floor((partNumber - 1) / BATCH_SIZE)
const fetchBatch = async (batchNumber) => {
const startPart = batchNumber * BATCH_SIZE + 1
const endPart = startPart + BATCH_SIZE - 1
const response = await fetch(
`${API_BASE}/videos/upload/s3/multipart/${uploadId}/prepare-parts?key=${encodeURIComponent(key)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ startPart, endPart })
}
)
const data = await response.json()
for (const part of data.parts) {
urlCache.set(part.partNumber, part.url)
}
}
const ensureBatchLoaded = async (partNumber) => {
const batchNumber = getBatchNumber(partNumber)
// Start fetching this batch if not already in progress
if (!batchPromises.has(batchNumber)) {
batchPromises.set(batchNumber, fetchBatch(batchNumber))
}
await batchPromises.get(batchNumber)
// Look-ahead: pre-fetch next batch when past 50% of current batch
const positionInBatch = (partNumber - 1) % BATCH_SIZE
if (positionInBatch > BATCH_SIZE / 2) {
const nextBatch = batchNumber + 1
if (!batchPromises.has(nextBatch)) {
batchPromises.set(nextBatch, fetchBatch(nextBatch).catch(() => {
batchPromises.delete(nextBatch)
}))
}
}
}
// Configure Uppy
const uppy = new Uppy()
uppy.use(AwsS3, {
shouldUseMultipart: true,
limit: 5,
retryDelays: [0, 1000, 3000, 5000],
createMultipartUpload: () => {
// Pre-fetch first batch of URLs immediately
ensureBatchLoaded(1).catch(console.error)
return Promise.resolve({ uploadId, key })
},
signPart: async (file, { partNumber }) => {
// Check cache first
let url = urlCache.get(partNumber)
if (url) return { url }
// Ensure batch is loaded
await ensureBatchLoaded(partNumber)
url = urlCache.get(partNumber)
if (url) return { url }
// Fallback: fetch single part
const response = await fetch(
`${API_BASE}/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
)
const data = await response.json()
return { url: data.url }
},
completeMultipartUpload: async (file, { uploadId, key, parts }) => {
// For large uploads, let server fetch parts info
const useFetchFromStorage = parts.length > 100
const response = await fetch(
`${API_BASE}/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(
useFetchFromStorage ? { fetchPartsFromStorage: true } : { parts }
)
}
)
return response.json()
}
})
uppy.addFile({
name: file.name,
type: file.type,
data: file
})
await uppy.upload()
uppy.destroy()
return videoId
}Gestione errori e nuovi tentativi
Uppy include logica di retry integrata. Configura il comportamento dei nuovi tentativi:
uppy.use(AwsS3, {
shouldUseMultipart: true,
retryDelays: [0, 1000, 3000, 5000], // Retry delays in ms
limit: 5, // Max concurrent uploads
// ... other options
})Per una gestione personalizzata degli errori:
uppy.on('upload-error', (file, error, response) => {
console.error('Upload failed for file:', file.name)
console.error('Error:', error)
// You can retry the upload
// uppy.retryUpload(file.id)
})
uppy.on('error', (error) => {
console.error('Uppy error:', error)
})