Héberge des fichiers avec Remix

Dans cet article, nous allons implémenter ensemble un formulaire permettant d'héberger des fichiers (images, vidéos, PDFs...). Nous utilisons le framework Remix et son puissant système de routing pour y parvenir.
Vous pouvez aussi consulter ce guide au format vidéo sur YouTube.
Voici la commande pour initialiser un nouveau projet Remix :
1npx create-remix@latest
Comment héberger un formulaire avec Remix ?
On a besoin de trois éléments :
- un fichier à héberger
- un
input
de typefile
pour le sélectionner et l'envoyer au serveur - un serveur pour le sauvegarder et le servir sur une route
Un fichier à héberger
Voici une image. Je vous laisse la télécharger. C'est le document que nous allons héberger ensemble.
Un input de type file pour envoyer le fichier au serveur
Nous allons ajouter un fichier dans le dossier app/routes
, nommé file
. Dedans, nous allons exporter par défaut un composant React (ce sera notre vue).
1import { Form } from '@remix-run/react';23export function File() {4return (5<Form6method='POST'7className='mt-8 flex flex-col gap-2 w-full items-center'8>9<input type='file' name='file' />10<button type='submit'>Soumettre</button>11</Form>12);13}
Notez l'utilisation du composant Form de Remix. Bien que l'hébergement des fichiers fonctionne avec un formulaire classique, il est recommendé de l'utiliser.
Un serveur pour sauvegarder le fichier et le servir aux utilisateurs
Dans l'article 6 Routes à connaître si tu utilises Remix (guide complet), nous avons vu ensemble qu'il nous suffit d'ajouter une fonction à notre fichier pour ajouter une logique côté serveur.
Comme le formulaire effectue un POST
, nous allons ajouter une fonction nommée action dans notre composant, et nous allons l'exporter.
1import type { ActionFunctionArgs } from '@remix-run/node';2import { Form } from '@remix-run/react';34export const action = async ({ request }: ActionFunctionArgs) => {5// Nous devons sauvegarder le fichier à cet endroit6return null;7};89export function File() {10return (11<Form12method='POST'13className='mt-8 flex flex-col gap-2 w-full items-center'14>15<input type='file' name='file' />16<button type='submit'>Soumettre</button>17</Form>18);19}
Il ne nous reste plus qu'à coder la logique dans notre action
et nous avons terminé ! N'est-ce pas ?
Pas vraiment. Avez-vous entendu parlé de la propriété
encType
?
La propriété de formulaire 'encType'
Version courte
Il faut rajouter la propriété encType
à notre formulaire.
1import type { ActionFunctionArgs } from '@remix-run/node';2import { Form } from '@remix-run/react';34export const action = async ({ request }: ActionFunctionArgs) => {5// Nous devons sauvegarder le fichier à cet endroit6return null;7};89export function File() {10return (11<Form12method='POST'13encType='multipart/form-data'14className='mt-8 flex flex-col gap-2 w-full items-center'15>16<input type='file' name='file' />17<button type='submit'>Soumettre</button>18</Form>19);20}
Pourquoi rajouter la propriété encType ?
Je ne connaissais pas cette propriété avant d'en avoir besoin. Par défaut, la propriété encType (ou type d'encodage) prend comme valeur application/x-www-form-urlencoded
. Mais il en existe deux autres.
Voici la définition sur MDN
Lorsque la valeur de l'attribut method est post, cet attribut définit le type MIME qui sera utilisé pour encoder les données envoyées au serveur. C'est un attribut énuméré qui peut prendre les valeurs suivantes :
application/x-www-form-urlencoded
: la valeur par défaut si l'attribut n'est pas définimultipart/form-data
: la valeur utilisée par un élément input avec l'attribut type="file".text/plain
, correspondant au type MIME éponyme et utilisé à des fins de débogage.
Nous utilisons un input
de type file
. Nous avons donc besoin de rajouter la propriété encType='multipart/form-data'
pour envoyer notre fichier au format binaire.
Nous avons terminé l'implémentation côté client ! Le reste se passe côté serveur.
Sauvegarder un fichier côté serveur
Pour pouvoir sauvegarder notre fichier et la servir à nos utilisateurs, nous allons devoir
- l'extraire du FormData avec les API de Remix unstable_createFileUploadHandler et unstable_parseMultipartFormData
- (Bonus) Valider le
formData
avec Zod et Conform - (Bonus) Renommer le fichier et lui donner un identifiant unique
- Servir le fichier aux utilisateurs (le rendre disponible à l'URL
localhost:3000/files/image.png
Extraire le fichier depuis le FormData
Nous souhaitons conserver le document sur notre serveur. Pour ce faire, nous allons utiliser la méthode unstable_createFileUploadHandler.
Cette méthode prend un objet d'options en argument. Voici les options que nous allons utiliser :
maxPartSize
pour définir la taille max du fichier en bytes.directory
pour définir le dossier de sauvegarde du document
1import {2unstable_createFileUploadHandler,3unstable_parseMultipartFormData,4type ActionFunctionArgs,5} from '@remix-run/node';6import { Form } from '@remix-run/react';78export const action = async ({ request }: ActionFunctionArgs) => {9const formData = await unstable_parseMultipartFormData(10request,11unstable_createFileUploadHandler({12maxPartSize: 1024 * 1024 * 10, // 10MB13directory: './uploads',14})15);16const file = formData.get('file') as File; // 👈 notre fichier au format Buffer17console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur18return null;19};2021export function File() {22return (23<Form24method='POST'25encType='multipart/form-data'26className='mt-8 flex flex-col gap-2 w-full items-center'27>28<input type='file' name='file' />29<button type='submit'>Soumettre</button>30</Form>31);32}
Enregistrer le fichier sur le disque dur serveur
Au moment de récupérer le fichier, ligne 16
, le fichier a déjà été enregistré dans le dossier uploads
. Pour avoir plus de contrôle sur la sauvegarde de ce fichier, je préfère utiliser la méthode
unstable_createMemoryUploadHandler. Cela nous permet d'enregistrer nous-même le fichier récupéré ligne 17
(on peut ensuite l'envoyer sur AWS S3 ou un autre service ...)
1import {2unstable_createMemoryUploadHandler,3unstable_parseMultipartFormData,4type ActionFunctionArgs,5} from '@remix-run/node';6import { Form } from '@remix-run/react';7import { saveVideoToLocal } from '~/videos.server';89export const action = async ({ request }: ActionFunctionArgs) => {10const formData = await unstable_parseMultipartFormData(11request,12unstable_createMemoryUploadHandler({13maxPartSize: 1024 * 1024 * 10, // 10MB14})15);16const file = formData.get('file') as File; // 👈 notre fichier au format Buffer17console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur18// mark19const { name } = await saveVideoToLocal({ videoFile: file }); // 👈 On sauvegarde le fichier sur le serveur20return { name };21};2223export function File() {24return (25<Form26method='POST'27encType='multipart/form-data'28className='mt-8 flex flex-col gap-2 w-full items-center'29>30<input type='file' name='file' />31<button type='submit'>Soumettre</button>32</Form>33);34}
1export const saveVideoToLocal = async ({ videoFile }: { videoFile: File }) => {2const originalName = new Date().toISOString() + videoFile.name;3const baseDirectory = join(process.cwd(), './uploads');45const filePath = join(baseDirectory, originalName);6const arrayBuffer = await videoFile.arrayBuffer();7const arrayBufferView = new Uint8Array(arrayBuffer);8const fileExists = checkIfFileExists({9filePath,10});11if (fileExists) {12throw new Error('Le fichier existe déjà ...');13}14await access(baseDirectory);15await writeFile(filePath, arrayBufferView);16return { name: originalName };17};
Servir le fichier aux utilisateurs
Pour rendre accessible nos fichiers hébergés (par exemple à l'URL localhost:3000/file/image.jpeg
, nous avons besoin de :
- Créer une nouvelle route
- Vérifier que le fichier existe
- Le renvoyer en fonction de son mimetype (si c'est un JPEG, la réponse sera différente par rapport au format mp4)
Nous allons créer un nouveau fichier nommé file.$filename.tsx
dans le dossier app/routes
.
Pour ce faire, nous devons également installer la librairie mime. Les autres librairies fs
et path
sont natives à l'environnement NodeJS.
1import { LoaderFunctionArgs } from '@remix-run/node';2import { readFileSync } from 'fs';3import mime from 'mime';4import { join } from 'path';56export const loader = async ({ params }: LoaderFunctionArgs) => {7const filename = params.filename;8if (!filename) {9// 👈 On vérifie que le nom du fichier est bien fourni10throw new Error('No filename provided');11}1213const filePath = join(process.cwd(), './uploads', filename); // 👈 On construit le chemin absolu du fichier14const fileContent = readFileSync(filePath); // 👈 On lit le fichier15const mimeType = mime.getType(filePath); // 👈 On récupère le type MIME du fichier1617console.log({ mimeType, filename, filePath, fileContent });18return new Response(fileContent, {19// 👈 On renvoie le fichier20headers: {21'Content-Type': mimeType || 'application/octet-stream', // 👈 On renvoie le type MIME du fichier,22},23});24};
Conclusion
Conclusion Dans cet article, nous avons vu comment implémenter un formulaire d'upload de fichiers dans Remix. Nous avons utilisé le composant Form de Remix et les API unstable_createFileUploadHandler et unstable_parseMultipartFormData pour gérer le transfert de fichiers. Nous avons également vu comment sauvegarder les fichiers sur le serveur et les servir aux utilisateurs.