Retour aux articles

Héberge des fichiers avec Remix

Héberge des fichiers avec Remix!
7 minutes de lecture

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 :


_10
npx create-remix@latest

Comment héberger un formulaire avec Remix ?Header Icon

On a besoin de trois éléments :

  • un fichier à héberger
  • un input de type file pour le sélectionner et l'envoyer au serveur
  • un serveur pour le sauvegarder et le servir sur une route

Un fichier à hébergerHeader Icon

Voici une image. Je vous laisse la télécharger. C'est le document que nous allons héberger ensemble.

un vinyle, l'emoji utilisé par l'équipe de développement de Remix

Un input de type file pour envoyer le fichier au serveurHeader Icon

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).

app/routes/file.tsx

_13
import { Form } from '@remix-run/react';
_13
_13
export function File() {
_13
return (
_13
<Form
_13
method='POST'
_13
className='mt-8 flex flex-col gap-2 w-full items-center'
_13
>
_13
<input type='file' name='file' />
_13
<button type='submit'>Soumettre</button>
_13
</Form>
_13
);
_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 utilisateursHeader Icon

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.

app/routes/file.tsx

_19
import type { ActionFunctionArgs } from '@remix-run/node';
_19
import { Form } from '@remix-run/react';
_19
_19
export const action = async ({ request }: ActionFunctionArgs) => {
_19
// Nous devons sauvegarder le fichier à cet endroit
_19
return null;
_19
};
_19
_19
export function File() {
_19
return (
_19
<Form
_19
method='POST'
_19
className='mt-8 flex flex-col gap-2 w-full items-center'
_19
>
_19
<input type='file' name='file' />
_19
<button type='submit'>Soumettre</button>
_19
</Form>
_19
);
_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'Header Icon

Version courteHeader Icon

Il faut rajouter la propriété encType à notre formulaire.

app/routes/file.tsx

_20
import type { ActionFunctionArgs } from '@remix-run/node';
_20
import { Form } from '@remix-run/react';
_20
_20
export const action = async ({ request }: ActionFunctionArgs) => {
_20
// Nous devons sauvegarder le fichier à cet endroit
_20
return null;
_20
};
_20
_20
export function File() {
_20
return (
_20
<Form
_20
method='POST'
_20
encType='multipart/form-data'
_20
className='mt-8 flex flex-col gap-2 w-full items-center'
_20
>
_20
<input type='file' name='file' />
_20
<button type='submit'>Soumettre</button>
_20
</Form>
_20
);
_20
}

Pourquoi rajouter la propriété encType ?Header Icon

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éfini
  • multipart/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é serveurHeader Icon

Pour pouvoir sauvegarder notre fichier et la servir à nos utilisateurs, nous allons devoir

Extraire le fichier depuis le FormDataHeader Icon

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
app/routes/file.tsx

_32
import {
_32
unstable_createFileUploadHandler,
_32
unstable_parseMultipartFormData,
_32
type ActionFunctionArgs,
_32
} from '@remix-run/node';
_32
import { Form } from '@remix-run/react';
_32
_32
export const action = async ({ request }: ActionFunctionArgs) => {
_32
const formData = await unstable_parseMultipartFormData(
_32
request,
_32
unstable_createFileUploadHandler({
_32
maxPartSize: 1024 * 1024 * 10, // 10MB
_32
directory: './uploads',
_32
})
_32
);
_32
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
_32
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
_32
return null;
_32
};
_32
_32
export function File() {
_32
return (
_32
<Form
_32
method='POST'
_32
encType='multipart/form-data'
_32
className='mt-8 flex flex-col gap-2 w-full items-center'
_32
>
_32
<input type='file' name='file' />
_32
<button type='submit'>Soumettre</button>
_32
</Form>
_32
);
_32
}

Enregistrer le fichier sur le disque dur serveurHeader Icon

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 ...)

app/routes/file.tsx
videos.server.ts

_33
import {
_33
unstable_createMemoryUploadHandler,
_33
unstable_parseMultipartFormData,
_33
type ActionFunctionArgs,
_33
} from '@remix-run/node';
_33
import { Form } from '@remix-run/react';
_33
import { saveVideoToLocal } from '~/videos.server';
_33
_33
export const action = async ({ request }: ActionFunctionArgs) => {
_33
const formData = await unstable_parseMultipartFormData(
_33
request,
_33
unstable_createMemoryUploadHandler({
_33
maxPartSize: 1024 * 1024 * 10, // 10MB
_33
})
_33
);
_33
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
_33
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
_33
const { name } = await saveVideoToLocal({ videoFile: file }); // 👈 On sauvegarde le fichier sur le serveur
_33
return { name };
_33
};
_33
_33
export function File() {
_33
return (
_33
<Form
_33
method='POST'
_33
encType='multipart/form-data'
_33
className='mt-8 flex flex-col gap-2 w-full items-center'
_33
>
_33
<input type='file' name='file' />
_33
<button type='submit'>Soumettre</button>
_33
</Form>
_33
);
_33
}

Servir le fichier aux utilisateursHeader Icon

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.

app/routes/file.$filename.tsx

_24
import { LoaderFunctionArgs } from '@remix-run/node';
_24
import { readFileSync } from 'fs';
_24
import mime from 'mime';
_24
import { join } from 'path';
_24
_24
export const loader = async ({ params }: LoaderFunctionArgs) => {
_24
const filename = params.filename;
_24
if (!filename) {
_24
// 👈 On vérifie que le nom du fichier est bien fourni
_24
throw new Error('No filename provided');
_24
}
_24
_24
const filePath = join(process.cwd(), './uploads', filename); // 👈 On construit le chemin absolu du fichier
_24
const fileContent = readFileSync(filePath); // 👈 On lit le fichier
_24
const mimeType = mime.getType(filePath); // 👈 On récupère le type MIME du fichier
_24
_24
console.log({ mimeType, filename, filePath, fileContent });
_24
return new Response(fileContent, {
_24
// 👈 On renvoie le fichier
_24
headers: {
_24
'Content-Type': mimeType || 'application/octet-stream', // 👈 On renvoie le type MIME du fichier,
_24
},
_24
});
_24
};

ConclusionHeader Icon

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.

Articles similaires
Gère les erreurs avec le framework Remix.js
6 minutes de lectureRemixReactJS

Comment gérer les erreurs avec Remix ? (ErrorBoundary)

Avoir des erreurs Javascript ne fait jamais plaisir. Mais il existe le composant ErrorBoundary). Dans ce guide, tu vas découvrir comment afficher un composant d'erreur personnalisé en pour protéger toutes les pages de ton application.

Rejoins la
newsletter