DEV Community

Cover image for Héberge des fichiers avec Remix
Virgile RIETSCH
Virgile RIETSCH

Posted on • Originally published at algomax.fr

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 :

npx create-remix@latest
Enter fullscreen mode Exit fullscreen mode

Comment héberger un formulaire avec Remix ?

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éberger

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 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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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é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é serveur

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

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode
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 };
};
Enter fullscreen mode Exit fullscreen mode

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,
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

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)