# Storage

Ce répertoire est le **module de stockage de fichiers**. Il fournit une couche d'accès générique aux providers S3-compatibles (Cloudflare R2, Backblaze B2) : upload, téléchargement, suppression, listage, URL présignées et validation de fichiers. Il gère également le système de contrôle d'accès aux fichiers servis via HTTP.

Il ne connaît aucune feature spécifique — les features l'importent pour leurs propres besoins et enregistrent leurs propres policies d'accès.

---

## Structure

```
src/core/storage/
├── index.js         Exports publics (uploadFile, deleteFile, validateUpload…)
├── api.js           Route HTTP /zen/api/storage/** avec contrôle d'accès par chemin
├── storage-config.js  Enregistrement des policies et préfixes publics
├── utils.js         Validation de fichiers, MIME types, magic bytes
├── signing.js       Signature AWS Signature V4 pour requêtes S3-compatibles
├── cloudflare-r2.js Configuration provider Cloudflare R2
└── backblaze.js     Configuration provider Backblaze B2
```

---

## Variables d'environnement

| Variable | Rôle |
|----------|------|
| `ZEN_STORAGE_PROVIDER` | Provider à utiliser : `r2` (défaut) ou `backblaze` |
| `ZEN_R2_ACCOUNT_ID` | ID de compte Cloudflare (R2) |
| `ZEN_R2_ACCESS_KEY_ID` | Clé d'accès R2 |
| `ZEN_R2_SECRET_ACCESS_KEY` | Secret R2 |
| `ZEN_R2_BUCKET_NAME` | Nom du bucket R2 |
| `ZEN_B2_KEY_ID` | ID de clé Backblaze B2 |
| `ZEN_B2_APPLICATION_KEY` | Clé d'application Backblaze B2 |
| `ZEN_B2_BUCKET_NAME` | Nom du bucket Backblaze B2 |
| `ZEN_B2_ENDPOINT` | Endpoint S3 Backblaze (ex: `s3.us-west-004.backblazeb2.com`) |

---

## Système de contrôle d'accès

### Principe : tout est privé par défaut

Aucun fichier n'est accessible sans configuration explicite. Pour qu'un fichier soit servi via `/zen/api/storage/**`, son chemin doit correspondre à l'un des deux mécanismes déclarés pendant `initializeZen()` : un préfixe public ou une policy d'accès.

### Préfixes publics

Les fichiers dont le chemin commence par un préfixe public sont servis **sans authentification**. Utile pour les assets publics (logos, images de produits…).

Le chemin doit avoir au minimum la forme `{prefixe}/{id}/{fichier}` — un préfixe seul ne suffit pas à exposer des fichiers.

```js
// src/features/myfeature/storage-policies.js
export const storagePublicPrefixes = ['products'];
// products/abc123/photo.webp → accessible sans session
```

### Policies d'accès (chemins privés)

Pour les chemins privés, une policy doit correspondre au premier segment du chemin (`pathParts[0]`). Deux types sont disponibles :

| Type | Règle |
|------|-------|
| `owner` | `pathParts[1]` doit être l'ID de l'utilisateur connecté. Les admins ont accès à tout. |
| `admin` | L'utilisateur doit avoir le rôle `admin`. |

```js
// src/features/myfeature/storage-policies.js
export const storageAccessPolicies = [
  { prefix: 'users',     type: 'owner' },  // users/{userId}/...
  { prefix: 'invoices',  type: 'admin'  },  // invoices/...
];
```

### Enregistrement dans initializeZen()

```js
// src/shared/lib/init.js
import { registerStoragePolicies, registerStoragePublicPrefixes } from '@zen/core/storage';
import { storageAccessPolicies, storagePublicPrefixes } from '../../features/myfeature/storage-policies.js';

registerStoragePolicies(storageAccessPolicies);
registerStoragePublicPrefixes(storagePublicPrefixes); // si nécessaire
```

L'enregistrement est **additif** — plusieurs features peuvent appeler `registerStoragePolicies` indépendamment.

---

## Fonctions d'opération

Toutes les fonctions retournent un objet `{ success, data, error }`. Elles ne lèvent jamais d'exception — les erreurs sont journalisées côté serveur et encapsulées dans `error`.

### `uploadFile({ key, body, contentType, metadata?, cacheControl? })`

Upload générique.

```js
const result = await uploadFile({
  key: 'users/abc123/profile/avatar_xxx.webp',
  body: buffer,
  contentType: 'image/webp',
  metadata: { userId: 'abc123' },
});
if (!result.success) throw new Error(result.error);
```

### `uploadImage({ key, body, contentType, metadata? })`

Équivalent à `uploadFile` avec `Cache-Control: public, max-age=31536000` (1 an).

### `deleteFile(key)`

Supprime un fichier.

```js
await deleteFile('users/abc123/profile/avatar_xxx.webp');
```

### `deleteFiles(keys[])`

Suppression en lot (S3 DeleteObjects). Retourne les clés supprimées et les erreurs éventuelles.

```js
const { data } = await deleteFiles(['a.jpg', 'b.jpg']);
// data.deleted → [{ Key: 'a.jpg' }, …]
// data.errors  → []
```

### `getFile(key)`

Récupère le contenu et les métadonnées d'un fichier.

```js
const { data } = await getFile('users/abc123/profile/avatar.webp');
// data.body, data.contentType, data.contentLength, data.lastModified, data.metadata
```

### `getFileMetadata(key)`

Requête HEAD : métadonnées uniquement, sans télécharger le contenu.

### `fileExists(key)`

Retourne `true` si le fichier existe.

```js
if (await fileExists('users/abc123/profile/avatar.webp')) { … }
```

### `listFiles({ prefix?, maxKeys?, continuationToken? })`

Liste les fichiers avec pagination (max 1 000 par appel).

```js
const { data } = await listFiles({ prefix: 'users/abc123/', maxKeys: 100 });
// data.files → [{ key, size, lastModified, etag }, …]
// data.isTruncated, data.nextContinuationToken
```

### `getPresignedUrl({ key, expiresIn?, operation? })`

Génère une URL signée temporaire. `expiresIn` en secondes (défaut 3600, max 604 800 = 7 jours). `operation` : `'get'` (défaut) ou `'put'`.

```js
const { data } = await getPresignedUrl({ key: 'docs/contrat.pdf', expiresIn: 300 });
// data.url → URL signée valide 5 minutes
```

### `copyFile({ sourceKey, destinationKey })`

Copie un fichier dans le même bucket.

### `moveFile({ sourceKey, destinationKey })`

Copie puis supprime la source. Si la suppression échoue, le fichier copié est conservé (non-fatal).

### `proxyFile(key, { filename? })`

Récupère un fichier pour le streamer directement au client.

---

## Validation de fichiers

### `validateUpload({ filename, size, allowedTypes, maxSize, buffer? })`

Validation complète avant upload. Retourne `{ valid, errors[] }`.

```js
const { valid, errors } = validateUpload({
  filename: file.name,
  size:     buffer.byteLength,
  allowedTypes: FILE_TYPE_PRESETS.IMAGES,
  maxSize:      FILE_SIZE_LIMITS.AVATAR,
  buffer,
});
if (!valid) return apiError('Bad Request', errors[0]);
```

Couches de validation appliquées dans l'ordre :
1. Nom de fichier requis
2. SVG explicitement interdit (risque d'exécution de scripts)
3. Extension contre la liste blanche `allowedTypes`
4. Taille contre `maxSize`
5. *(si buffer fourni)* Magic bytes — assertion positive que l'extension correspond au contenu réel
6. *(si buffer fourni)* Scan des 512 premiers octets pour HTML/SVG/XML (défense contre les fichiers polyglots)

### `FILE_TYPE_PRESETS`

| Constante | Extensions |
|-----------|-----------|
| `IMAGES` | `.jpg` `.jpeg` `.png` `.gif` `.webp` |
| `IMAGES_NO_GIF` | `.jpg` `.jpeg` `.png` `.webp` |
| `DOCUMENTS` | `.pdf` `.doc` `.docx` `.xls` `.xlsx` `.ppt` `.pptx` `.txt` `.csv` |
| `PDF_ONLY` | `.pdf` |
| `VIDEOS` | `.mp4` `.avi` `.mov` `.wmv` |
| `AUDIO` | `.mp3` `.wav` |
| `ARCHIVES` | `.zip` `.rar` `.7z` `.tar` `.gz` |

### `FILE_SIZE_LIMITS`

| Constante | Limite |
|-----------|--------|
| `AVATAR` | 5 MB |
| `IMAGE` | 10 MB |
| `DOCUMENT` | 50 MB |
| `VIDEO` | 500 MB |
| `LARGE_FILE` | 1 GB |

### Fonctions utilitaires

| Fonction | Description |
|----------|-------------|
| `generateUniqueFilename(originalName, prefix?)` | Génère `{prefix}_{timestamp}_{hash}{ext}` |
| `getFileExtension(filename)` | Retourne l'extension avec le point (`.jpg`) ou `''` |
| `getMimeType(filename)` | Retourne le MIME type depuis l'extension |
| `validateFileType(filename, allowedTypes)` | Vérifie l'extension contre une liste |
| `validateFileSize(size, maxSize)` | Vérifie la taille |
| `formatFileSize(bytes)` | Formate en lisible humain (`1.5 MB`) |
| `sanitizeFilename(filename)` | Supprime les caractères spéciaux |

---

## Usage depuis une feature

```js
// src/features/media/api.js
import { uploadImage, deleteFile, validateUpload, FILE_TYPE_PRESETS, FILE_SIZE_LIMITS, generateUniqueFilename } from '../../core/storage/index.js';

export async function uploadAvatar(userId, file, buffer) {
  const { valid, errors } = validateUpload({
    filename:     file.name,
    size:         buffer.byteLength,
    allowedTypes: FILE_TYPE_PRESETS.IMAGES,
    maxSize:      FILE_SIZE_LIMITS.AVATAR,
    buffer,
  });
  if (!valid) throw new Error(errors[0]);

  const filename = generateUniqueFilename(file.name, 'avatar');
  const key = `users/${userId}/profile/${filename}`;

  const result = await uploadImage({ key, body: buffer, contentType: getMimeType(file.name) });
  if (!result.success) throw new Error('Upload failed');

  return key;
}
```

```js
// src/features/media/storage-policies.js  ← la feature déclare ses propres policies
export const storageAccessPolicies = [
  { prefix: 'users', type: 'owner' },
];
```

## Usage depuis un module

```js
// src/modules/mymodule/actions.js
import { uploadFile, deleteFile, generateUniqueFilename } from '@zen/core/storage';
```
