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 :
npx 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).
import { Form } from '@remix-run/react';
export function File() {
return (
<Form
method='POST'
className='mt-8 flex flex-col gap-2 w-full items-center'
>
<input type='file' name='file' />
<button type='submit'>Soumettre</button>
</Form>
);
}
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.
import type { ActionFunctionArgs } from '@remix-run/node';
import { Form } from '@remix-run/react';
export const action = async ({ request }: ActionFunctionArgs) => {
// Nous devons sauvegarder le fichier à cet endroit
return null;
};
export function File() {
return (
<Form
method='POST'
className='mt-8 flex flex-col gap-2 w-full items-center'
>
<input type='file' name='file' />
<button type='submit'>Soumettre</button>
</Form>
);
}
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.
import type { ActionFunctionArgs } from '@remix-run/node';
import { Form } from '@remix-run/react';
export const action = async ({ request }: ActionFunctionArgs) => {
// Nous devons sauvegarder le fichier à cet endroit
return null;
};
export function File() {
return (
<Form
method='POST'
encType='multipart/form-data'
className='mt-8 flex flex-col gap-2 w-full items-center'
>
<input type='file' name='file' />
<button type='submit'>Soumettre</button>
</Form>
);
}
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 :
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
import {
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
type ActionFunctionArgs,
} from '@remix-run/node';
import { Form } from '@remix-run/react';
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await unstable_parseMultipartFormData(
request,
unstable_createFileUploadHandler({
maxPartSize: 1024 * 1024 * 10, // 10MB
directory: './uploads',
})
);
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
return null;
};
export function File() {
return (
<Form
method='POST'
encType='multipart/form-data'
className='mt-8 flex flex-col gap-2 w-full items-center'
>
<input type='file' name='file' />
<button type='submit'>Soumettre</button>
</Form>
);
}
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 ...)
import {
unstable_createMemoryUploadHandler,
unstable_parseMultipartFormData,
type ActionFunctionArgs,
} from '@remix-run/node';
import { Form } from '@remix-run/react';
import { saveVideoToLocal } from '~/videos.server';
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await unstable_parseMultipartFormData(
request,
unstable_createMemoryUploadHandler({
maxPartSize: 1024 * 1024 * 10, // 10MB
})
);
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
// mark
const { name } = await saveVideoToLocal({ videoFile: file }); // 👈 On sauvegarde le fichier sur le serveur
return { name };
};
export function File() {
return (
<Form
method='POST'
encType='multipart/form-data'
className='mt-8 flex flex-col gap-2 w-full items-center'
>
<input type='file' name='file' />
<button type='submit'>Soumettre</button>
</Form>
);
}
export const saveVideoToLocal = async ({ videoFile }: { videoFile: File }) => {
const originalName = new Date().toISOString() + videoFile.name;
const baseDirectory = join(process.cwd(), './uploads');
const filePath = join(baseDirectory, originalName);
const arrayBuffer = await videoFile.arrayBuffer();
const arrayBufferView = new Uint8Array(arrayBuffer);
const fileExists = checkIfFileExists({
filePath,
});
if (fileExists) {
throw new Error('Le fichier existe déjà ...');
}
await access(baseDirectory);
await writeFile(filePath, arrayBufferView);
return { name: originalName };
};
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.
import { LoaderFunctionArgs } from '@remix-run/node';
import { readFileSync } from 'fs';
import mime from 'mime';
import { join } from 'path';
export const loader = async ({ params }: LoaderFunctionArgs) => {
const filename = params.filename;
if (!filename) {
// 👈 On vérifie que le nom du fichier est bien fourni
throw new Error('No filename provided');
}
const filePath = join(process.cwd(), './uploads', filename); // 👈 On construit le chemin absolu du fichier
const fileContent = readFileSync(filePath); // 👈 On lit le fichier
const mimeType = mime.getType(filePath); // 👈 On récupère le type MIME du fichier
console.log({ mimeType, filename, filePath, fileContent });
return new Response(fileContent, {
// 👈 On renvoie le fichier
headers: {
'Content-Type': mimeType || 'application/octet-stream', // 👈 On renvoie le type MIME du fichier,
},
});
};
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.
Top comments (0)