Retour aux articles

Héberge des fichiers avec Remix

7 minutes de lecture512 vues
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 :

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

app/routes/file.tsx
1
import { Form } from '@remix-run/react';
2
3
export function File() {
4
return (
5
<Form
6
method='POST'
7
className='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.

app/routes/file.tsx focus=1,4:7
1
import type { ActionFunctionArgs } from '@remix-run/node';
2
import { Form } from '@remix-run/react';
3
4
export const action = async ({ request }: ActionFunctionArgs) => {
5
// Nous devons sauvegarder le fichier à cet endroit
6
return null;
7
};
8
9
export function File() {
10
return (
11
<Form
12
method='POST'
13
className='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.

app/routes/file.tsx mark=13[4:42]
1
import type { ActionFunctionArgs } from '@remix-run/node';
2
import { Form } from '@remix-run/react';
3
4
export const action = async ({ request }: ActionFunctionArgs) => {
5
// Nous devons sauvegarder le fichier à cet endroit
6
return null;
7
};
8
9
export function File() {
10
return (
11
<Form
12
method='POST'
13
encType='multipart/form-data'
14
className='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é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
app/routes/file.tsx focus=2:3,9:17
1
import {
2
unstable_createFileUploadHandler,
3
unstable_parseMultipartFormData,
4
type ActionFunctionArgs,
5
} from '@remix-run/node';
6
import { Form } from '@remix-run/react';
7
8
export const action = async ({ request }: ActionFunctionArgs) => {
9
const formData = await unstable_parseMultipartFormData(
10
request,
11
unstable_createFileUploadHandler({
12
maxPartSize: 1024 * 1024 * 10, // 10MB
13
directory: './uploads',
14
})
15
);
16
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
17
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
18
return null;
19
};
20
21
export function File() {
22
return (
23
<Form
24
method='POST'
25
encType='multipart/form-data'
26
className='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 ...)

app/routes/file.tsx focus=1:20
1
import {
2
unstable_createMemoryUploadHandler,
3
unstable_parseMultipartFormData,
4
type ActionFunctionArgs,
5
} from '@remix-run/node';
6
import { Form } from '@remix-run/react';
7
import { saveVideoToLocal } from '~/videos.server';
8
9
export const action = async ({ request }: ActionFunctionArgs) => {
10
const formData = await unstable_parseMultipartFormData(
11
request,
12
unstable_createMemoryUploadHandler({
13
maxPartSize: 1024 * 1024 * 10, // 10MB
14
})
15
);
16
const file = formData.get('file') as File; // 👈 notre fichier au format Buffer
17
console.log(file.name); // 👈 Le nom du fichier pour pouvoir le retrouver sur le serveur
18
// mark
19
const { name } = await saveVideoToLocal({ videoFile: file }); // 👈 On sauvegarde le fichier sur le serveur
20
return { name };
21
};
22
23
export function File() {
24
return (
25
<Form
26
method='POST'
27
encType='multipart/form-data'
28
className='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
}
videos.server.ts
1
export const saveVideoToLocal = async ({ videoFile }: { videoFile: File }) => {
2
const originalName = new Date().toISOString() + videoFile.name;
3
const baseDirectory = join(process.cwd(), './uploads');
4
5
const filePath = join(baseDirectory, originalName);
6
const arrayBuffer = await videoFile.arrayBuffer();
7
const arrayBufferView = new Uint8Array(arrayBuffer);
8
const fileExists = checkIfFileExists({
9
filePath,
10
});
11
if (fileExists) {
12
throw new Error('Le fichier existe déjà ...');
13
}
14
await access(baseDirectory);
15
await writeFile(filePath, arrayBufferView);
16
return { 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.

app/routes/file.$filename.tsx
1
import { LoaderFunctionArgs } from '@remix-run/node';
2
import { readFileSync } from 'fs';
3
import mime from 'mime';
4
import { join } from 'path';
5
6
export const loader = async ({ params }: LoaderFunctionArgs) => {
7
const filename = params.filename;
8
if (!filename) {
9
// 👈 On vérifie que le nom du fichier est bien fourni
10
throw new Error('No filename provided');
11
}
12
13
const filePath = join(process.cwd(), './uploads', filename); // 👈 On construit le chemin absolu du fichier
14
const fileContent = readFileSync(filePath); // 👈 On lit le fichier
15
const mimeType = mime.getType(filePath); // 👈 On récupère le type MIME du fichier
16
17
console.log({ mimeType, filename, filePath, fileContent });
18
return new Response(fileContent, {
19
// 👈 On renvoie le fichier
20
headers: {
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.

Articles similaires
Reste informé
Abonne-toi à notre newsletter pour recevoir les dernières mises à jour et insights.