Upload avec Uppy
Uppy est un outil de téléchargement de fichiers open source très utilisé qui gère le découpage, les nouvelles tentatives et le suivi de progression automatiquement. Ce guide montre comment intégrer Uppy avec l’API Ignite Video pour les téléchargements fichier unique et multipart.
Uppy gère automatiquement les téléchargements multipart pour les gros fichiers, ce qui en fait l’approche recommandée pour les applications en production.
Pour une implémentation manuelle sans dépendance externe, voir le guide Upload multipart.
Installation
Installe les paquets Uppy nécessaires :
npm
npm install @uppy/core @uppy/aws-s3Configuration de base
Configurer téléchargement fichier unique
Pour les fichiers de moins de 100 Mo, utilise un téléchargement direct avec une URL pré-signée.
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
}Configurer téléchargement multipart
Pour les fichiers de plus de 100 Mo, utilise le téléchargement multipart pour une meilleure fiabilité.
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
}Exemple complet avec suivi de progression
Voici une implémentation complète qui choisit automatiquement entre téléchargement fichier unique et multipart selon la taille du fichier :
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
}Intégration React
Voici un exemple de composant React utilisant 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 VideoUploaderAvancé : récupération d’URL par lots
Pour de meilleures performances sur les gros fichiers, récupère les URL signées par lots plutôt qu’une par une. Cela réduit fortement le nombre d’appels API pour les gros téléchargements.
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
}Gestion des erreurs et nouvelles tentatives
Uppy inclut une logique de nouvelle tentative intégrée. Configure le comportement :
uppy.use(AwsS3, {
shouldUseMultipart: true,
retryDelays: [0, 1000, 3000, 5000], // Retry delays in ms
limit: 5, // Max concurrent uploads
// ... other options
})Pour une gestion d’erreurs personnalisée :
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)
})