Héberge des fichiers avec Remix
Tags: webdev, remix, react, javascript
Published: 2024-05-20 07:05:54

Dans cet article, nous allons implémenter ensemble un [formulaire](https://algomax.fr/blog/implemente-ton-premier-formulaire-avec-remix-full-typesafe-ux-friendly) permettant d'héberger des fichiers (images, vidéos, PDFs...). Nous utilisons le framework [Remix](https://remix.run) et son [puissant système de routing](https://algomax.fr/blog/les-6-routes-a-connaitre-avec-le-framework-remix-framework-react) pour y parvenir.

Vous pouvez aussi consulter ce [guide au format vidéo](https://youtu.be/c7nLBwfg_K8) sur YouTube.

{% youtube https://youtu.be/c7nLBwfg_K8 %}

Voici la commande pour initialiser un nouveau projet Remix :
```bash
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](https://unsplash.com/fr/photos/disque-vinyle-noir-et-blanc-hrUhyFq6u-A). C'est le document que nous allons héberger ensemble.
![un vinyle, l'emoji utilisé par l'équipe de développement de Remix](https://algomax-public.s3.eu-west-3.amazonaws.com/brett_jordan_hr_Uhy_Fq6u_A_unsplash_3698c66818.jpg)

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

```tsx
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](https://remix.run/docs/en/main/components/form) de Remix. Bien que l'hébergement des fichiers fonctionne avec un [formulaire classique](https://developer.mozilla.org/fr/docs/Web/HTML/Element/form), 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)](https://algomax.fr/blog/les-6-routes-a-connaitre-avec-le-framework-remix-framework-react), 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](https://remix.run/docs/en/main/route/action) dans notre composant, et nous allons l'exporter.

```tsx
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.

```tsx
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](https://developer.mozilla.org/fr/docs/Web/API/HTMLFormElement/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](https://developer.mozilla.org/fr/docs/Web/HTML/Element/form#enctype)

> Lorsque la valeur de l'attribut method est post, cet attribut définit le [type MIME](https://fr.wikipedia.org/wiki/Type_de_m%C3%A9dias) 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](https://developer.mozilla.org/fr/docs/Web/HTML/Element/input) avec l'attribut type="file".
- `text/plain`, correspondant au [type MIME](https://fr.wikipedia.org/wiki/Type_de_m%C3%A9dias) é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](https://developer.mozilla.org/en-US/docs/Web/API/FormData) avec les API de Remix [
unstable_createFileUploadHandler](https://remix.run/docs/en/main/utils/unstable-create-file-upload-handler) et [
unstable_parseMultipartFormData](https://remix.run/docs/en/main/utils/parse-multipart-form-data#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](https://remix.run/docs/en/main/utils/unstable-create-file-upload-handler).

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

```tsx
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](https://remix.run/docs/en/main/utils/unstable-create-memory-upload-handler). 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 ...)

```tsx
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>
	);
}
```

```ts
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](https://npmjs.com/package/mime). Les autres librairies `fs` et `path` sont natives à l'environnement NodeJS.

```tsx
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](https://remix.run/docs/en/main/components/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.