npm package: @jamx-framework/storage
Description: JAMX Framework â€” File storage with local and S3-compatible drivers
Author: Stiven-21
Latest version: 1.0.0

# @jamx-framework/storage

## Descripción
Sistema de almacenamiento de archivos para JAMX Framework. Proporciona una API unificada para guardar, recuperar, eliminar y gestionar archivos con múltiples backends: almacenamiento local (filesystem) y S3-compatible (AWS S3, MinIO, Cloudflare R2, etc.). Soporta metadatos, URLs públicas, listado de archivos y operaciones de copia/movimiento.

## Cómo funciona
El módulo implementa un patrón de driver:

1. **Storage**: Clase principal que delega operaciones al driver configurado
2. **Drivers**: Implementaciones concretas (`LocalDriver`, `S3Driver`) que definen la persistencia
3. **StorageFile**: Objeto que representa un archivo con metadatos (key, size, contentType, etc.)

## Componentes principales

### Storage (`src/storage.ts`)
Clase principal que proporciona la API unificada:
- `put(key, data, options?)`: Sube un archivo
- `get(key)`: Descarga un archivo
- `delete(key)`: Elimina un archivo
- `exists(key)`: Verifica existencia
- `stat(key)`: Obtiene metadatos
- `list(prefix?)`: Lista archivos
- `url(key)`: Obtiene URL pública (si aplica)
- `copy(source, dest)`: Copia archivo
- `move(source, dest)`: Mueve archivo

### Drivers

#### LocalDriver (`src/drivers/local.ts`)
Almacena archivos en el filesystem local:
- Config: `root` (directorio base), `baseUrl` (URL pública opcional)
- Los archivos se guardan en `${root}/${key}`
- `url(key)` retorna `${baseUrl}/${key}` si `baseUrl` está configurado

#### S3Driver (`src/drivers/s3.ts`)
Almacena en S3 o servicios compatibles:
- Config: `bucket`, `region`, `accessKey`, `secretKey`, `endpoint` (opcional)
- Usa AWS SDK v3 (o similar)
- `url(key)` retorna URL pública de S3 (o signed URL si privado)

### Tipos

#### StorageDriver
```typescript
interface StorageDriver {
  put(key: string, data: Buffer, options?: PutOptions): Promise<void>;
  get(key: string): Promise<Buffer | null>;
  delete(key: string): Promise<boolean>;
  exists(key: string): Promise<boolean>;
  stat(key: string): Promise<StorageFile | null>;
  list(prefix?: string): Promise<StorageFile[]>;
  url(key: string): Promise<string>;
}
```

#### StorageFile
```typescript
interface StorageFile {
  key: string;
  size: number;
  contentType: string;
  lastModified: Date;
  etag?: string;
}
```

#### PutOptions
```typescript
interface PutOptions {
  contentType?: string;
  metadata?: Record<string, string>;
  acl?: 'private' | 'public-read' | 'public-read-write';
}
```

#### StorageConfig
```typescript
interface StorageConfig {
  driver: 'local' | 's3';
  local?: LocalDriverConfig;
  s3?: S3Config;
}
```

## Uso básico

### Configuración local
```typescript
import { Storage, LocalDriver } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 'local',
  local: {
    root: './uploads',      // directorio donde guardar archivos
    baseUrl: 'http://localhost:3000/uploads', // URL pública (opcional)
  },
});
```

### Configuración S3
```typescript
import { Storage, S3Driver } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
    // endpoint: 'https://nyc3.digitaloceanspaces.com', // opcional para S3-compatible
  },
});
```

### Subir archivos
```typescript
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

// Subir archivo
await storage.put('avatars/user123.png', buffer, {
  contentType: 'image/png',
});

// Con metadata
await storage.put('documents/report.pdf', buffer, {
  contentType: 'application/pdf',
  metadata: {
    uploadedBy: 'user123',
    description: 'Monthly report',
  },
});
```

### Descargar archivos
```typescript
// Descargar como Buffer
const data = await storage.get('avatars/user123.png');
if (data) {
  // usar data (Buffer)
  console.log('File size:', data.length);
}

// Verificar existencia
const exists = await storage.exists('avatars/user123.png');
if (exists) {
  // ...
}
```

### Obtener metadatos
```typescript
const file = await storage.stat('avatars/user123.png');
if (file) {
  console.log('Key:', file.key);
  console.log('Size:', file.size);
  console.log('Content-Type:', file.contentType);
  console.log('Last Modified:', file.lastModified);
  console.log('ETag:', file.etag);
}
```

### Listar archivos
```typescript
// Listar todos
const files = await storage.list();
for (const file of files) {
  console.log(file.key, file.size);
}

// Listar con prefijo
const userFiles = await storage.list('avatars/user123/');
// Retorna archivos que empiezan con 'avatars/user123/'
```

### URLs públicas
```typescript
// Local driver
const url = await storage.url('avatars/user123.png');
// → 'http://localhost:3000/uploads/avatars/user123.png'

// S3 driver (depende de configuración de bucket)
const url = await storage.url('avatars/user123.png');
// → 'https://my-bucket.s3.amazonaws.com/avatars/user123.png'
```

### Eliminar archivos
```typescript
const deleted = await storage.delete('avatars/user123.png');
if (deleted) {
  console.log('File deleted');
}
```

### Copiar y mover
```typescript
// Copiar
await storage.copy('avatars/old.png', 'avatars/new.png');

// Mover (copy + delete)
await storage.move('avatars/temp.png', 'avatars/permanent.png');
```

## API Reference

### Storage

#### Constructor
```typescript
new Storage(config: StorageConfig)
```
Crea una instancia de storage con el driver especificado.

#### put()
```typescript
async put(key: string, data: Buffer, options?: PutOptions): Promise<void>
```
Sube un archivo.

- `key`: Ruta del archivo en el storage (ej: `'users/123/avatar.png'`)
- `data`: Contenido del archivo como Buffer
- `options.contentType`: MIME type (ej: `'image/png'`)
- `options.metadata`: Metadatos personalizados (S3 only)
- `options.acl`: Permisos de acceso (S3 only)

**Ejemplo:**
```typescript
await storage.put('docs/report.pdf', pdfBuffer, {
  contentType: 'application/pdf',
  metadata: { author: 'john' },
});
```

#### get()
```typescript
async get(key: string): Promise<Buffer | null>
```
Descarga un archivo. Retorna `null` si no existe.

**Ejemplo:**
```typescript
const data = await storage.get('image.jpg');
if (data) {
  // usar data
}
```

#### delete()
```typescript
async delete(key: string): Promise<boolean>
```
Elimina un archivo. Retorna `true` si existía y fue eliminado, `false` si no existía.

#### exists()
```typescript
async exists(key: string): Promise<boolean>
```
Verifica si un archivo existe.

#### stat()
```typescript
async stat(key: string): Promise<StorageFile | null>
```
Obtiene metadatos del archivo.

**StorageFile:**
```typescript
interface StorageFile {
  key: string;
  size: number;          // bytes
  contentType: string;   // MIME type
  lastModified: Date;    // fecha de modificación
  etag?: string;         // ETag (S3)
}
```

#### list()
```typescript
async list(prefix?: string): Promise<StorageFile[]>
```
Lista archivos. Si `prefix` se especifica, solo lista archivos que empiezan con ese prefijo.

**Ejemplo:**
```typescript
// Todos los archivos
const all = await storage.list();

// Solo avatares
const avatars = await storage.list('avatars/');
```

#### url()
```typescript
async url(key: string): Promise<string>
```
Obtiene la URL pública del archivo. Depende del driver:
- **Local**: `${baseUrl}/${key}` (requiere `baseUrl` en config)
- **S3**: URL pública de S3 (bucket debe ser público o usar signed URLs)

#### copy()
```typescript
async copy(source: string, dest: string): Promise<void>
```
Copia un archivo dentro del mismo storage.

#### move()
```typescript
async move(source: string, dest: string): Promise<void>
```
Mueve un archivo (copia + elimina origen).

### LocalDriver

#### Configuración
```typescript
interface LocalDriverConfig {
  root: string;           // directorio base (absoluto o relativo)
  baseUrl?: string;       // URL base para acceder a archivos (opcional)
}
```

**Ejemplo:**
```typescript
{
  driver: 'local',
  local: {
    root: '/var/uploads',
    baseUrl: 'https://cdn.example.com/uploads',
  },
}
```

**Comportamiento:**
- Los archivos se guardan en `${root}/${key}`
- `root` se crea automáticamente si no existe
- `url(key)` retorna `${baseUrl}/${key}` si `baseUrl` está configurado
- Si `baseUrl` no está configurado, `url()` lanza error

### S3Driver

#### Configuración
```typescript
interface S3Config {
  bucket: string;         // nombre del bucket
  region: string;         // región de AWS
  accessKey: string;      // AWS_ACCESS_KEY_ID
  secretKey: string;      // AWS_SECRET_ACCESS_KEY
  endpoint?: string;      // endpoint personalizado (para S3-compatible)
  forcePathStyle?: boolean; // usar path-style URLs (para MinIO, etc.)
}
```

**Ejemplo AWS S3:**
```typescript
{
  driver: 's3',
  s3: {
    bucket: 'my-app-uploads',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
}
```

**Ejemplo MinIO:**
```typescript
{
  driver: 's3',
  s3: {
    bucket: 'uploads',
    region: 'us-east-1',
    accessKey: 'minioadmin',
    secretKey: 'minioadmin',
    endpoint: 'http://localhost:9000',
    forcePathStyle: true,
  },
}
```

**Ejemplo Cloudflare R2:**
```typescript
{
  driver: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'auto',
    accessKey: process.env.CF_ACCESS_KEY_ID!,
    secretKey: process.env.CF_SECRET_ACCESS_KEY!,
    endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
  },
}
```

**Comportamiento:**
- Usa AWS SDK S3 client
- `put()` sube con `PutObjectCommand`
- `get()` descarga con `GetObjectCommand`
- `url(key)` retorna URL pública si el bucket es público, o signed URL si es privado
- Soporta metadata personalizada

## Ejemplos completos

### Upload de avatar de usuario
```typescript
import { Storage } from '@jamx-framework/storage';

const storage = new Storage({
  driver: 's3',
  s3: {
    bucket: 'my-app-avatars',
    region: 'us-east-1',
    accessKey: process.env.AWS_ACCESS_KEY_ID!,
    secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

async function uploadAvatar(userId: string, file: Express.Multer.File) {
  const key = `avatars/${userId}/${Date.now()}-${file.originalname}`;

  await storage.put(key, file.buffer, {
    contentType: file.mimetype,
    metadata: {
      userId,
      originalName: file.originalname,
    },
  });

  const url = await storage.url(key);
  return url;
}
```

### Servir archivos estáticos
```typescript
import { Storage, LocalDriver } from '@jamx-framework/storage';
import { serve } from '@jamx-framework/server';

const storage = new Storage({
  driver: 'local',
  local: {
    root: './public/uploads',
    baseUrl: '/uploads',
  },
});

// En el servidor
server.use('/uploads', async (req, res) => {
  const key = req.path.slice('/uploads'.length); // remover prefijo
  const file = await storage.get(key);

  if (!file) {
    res.notFound();
    return;
  }

  const meta = await storage.stat(key);
  res.header('Content-Type', meta?.contentType ?? 'application/octet-stream');
  res.send(file);
});
```

### Limpieza de archivos antiguos
```typescript
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

async function cleanupOldFiles(days: number) {
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
  const files = await storage.list();

  for (const file of files) {
    if (file.lastModified.getTime() < cutoff) {
      await storage.delete(file.key);
      console.log(`Deleted ${file.key}`);
    }
  }
}

// Ejecutar diariamente
setInterval(() => cleanupOldFiles(30), 24 * 60 * 60 * 1000);
```

### Migración de local a S3
```typescript
import { Storage } from '@jamx-framework/storage';

const local = new Storage({
  driver: 'local',
  local: { root: './old-uploads' },
});

const s3 = new Storage({
  driver: 's3',
  s3: { /* config */ },
});

async function migrate() {
  const files = await local.list();

  for (const file of files) {
    const data = await local.get(file.key);
    if (data) {
      await s3.put(file.key, data, {
        contentType: file.contentType,
      });
      console.log(`Migrated ${file.key}`);
    }
  }
}
```

### Generar signed URLs (S3 privado)
```typescript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

// Si necesitas signed URLs, extiende S3Driver:
class SecureS3Driver extends S3Driver {
  async signedUrl(key: string, expiresIn: number = 3600): Promise<string> {
    const command = new GetObjectCommand({
      Bucket: this.config.bucket,
      Key: key,
    });

    return await getSignedUrl(this.client, command, { expiresIn });
  }
}

// Uso
const storage = new Storage({ driver: 's3', s3: { /* ... */ } });
const url = await (storage.driver as SecureS3Driver).signedUrl('private/file.pdf');
```

## Consideraciones de rendimiento

### LocalDriver
- **I/O bloqueante**: Node.js es single-threaded; muchos accesos concurrentes pueden bloquear event loop
- **Escalabilidad**: No escala en clúster (cada instancia tiene su propio filesystem)
- **Backup**: Responsabilidad del usuario (backup del directorio `root`)
- **CDN**: Para servir archivos eficientemente, usar `baseUrl` apuntando a CDN

### S3Driver
- **Latencia**: Cada operación es una llamada de red (100ms+ típicamente)
- **Throughput**: S3 soporta miles de requests/segundo por bucket
- **Costos**: Cada PUT/GET tiene costo; considerar CloudFront para caching
- **Consistencia**: S3 tiene read-after-write consistency para PUTs nuevos

### Concurrencia
- El storage no limita concurrencia
- Para uploads masivos, considera limitar con semáforos
- Los drivers son thread-safe (no comparten estado mutable)

### Caching
- El storage no incluye cache interno
- Para archivos frecuentemente accedidos, usar CDN o cache en memoria:
```typescript
import { Cache } from '@jamx-framework/cache';

const cache = new Cache({ driver: 'redis' });

async function getWithCache(key: string) {
  const cached = await cache.get(key);
  if (cached) return Buffer.from(cached);

  const data = await storage.get(key);
  if (data) {
    await cache.set(key, data, 3600); // cache 1 hora
  }
  return data;
}
```

## Testing

### Tests con LocalDriver
```typescript
import { Storage } from '@jamx-framework/storage';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { rmSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';

describe('LocalStorage', () => {
  const testDir = './test-uploads';

  beforeEach(() => {
    mkdirSync(testDir, { recursive: true });
  });

  afterEach(() => {
    rmSync(testDir, { recursive: true, force: true });
  });

  it('should upload and download files', async () => {
    const storage = new Storage({
      driver: 'local',
      local: { root: testDir, baseUrl: 'http://localhost/uploads' },
    });

    await storage.put('test.txt', Buffer.from('Hello World'));
    const data = await storage.get('test.txt');
    expect(data?.toString()).toBe('Hello World');
  });

  it('should list files', async () => {
    const storage = new Storage({
      driver: 'local',
      local: { root: testDir },
    });

    await storage.put('a.txt', Buffer.from('A'));
    await storage.put('b.txt', Buffer.from('B'));

    const files = await storage.list();
    expect(files).toHaveLength(2);
  });
});
```

### Tests con S3Driver (mock)
```typescript
import { Storage } from '@jamx-framework/storage';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { mockClient } from 'aws-sdk-client-mock';

const s3Mock = mockClient(S3Client);

describe('S3Storage', () => {
  beforeEach(() => {
    s3Mock.reset();
  });

  it('should upload file', async () => {
    s3Mock.on(PutObjectCommand).resolves({});

    const storage = new Storage({
      driver: 's3',
      s3: {
        bucket: 'test-bucket',
        region: 'us-east-1',
        accessKey: 'test',
        secretKey: 'test',
      },
    });

    await storage.put('key.txt', Buffer.from('content'));

    expect(s3Mock.calls()).toHaveLength(1);
  });
});
```

## Seguridad

### Validación de keys
- Los drivers deben sanitizar `key` para evitar path traversal
- LocalDriver previene `../` en paths
- S3Driver usa el key directamente (S3 ya previene problemas)

### Permisos
- **LocalDriver**: Permisos del filesystem del OS
- **S3Driver**: Permisos de IAM (accessKey/secretKey)
- Configurar `acl` apropiadamente (`private` por defecto)

### URLs firmadas (S3)
Para buckets privados, generar signed URLs:
```typescript
// No implementado por defecto, pero se puede extender
const url = await storage.driver.url(key); // puede fallar si bucket es privado
```

### Encriptación
- El storage no encripta datos en tránsito (usar HTTPS para S3)
- Para encriptación en reposo, usar S3 SSE o encriptar antes de `put()`

## Limitaciones

### LocalDriver
- No soporta directorios virtuales (todos los keys son "archivos")
- No hay límite de tamaño de archivo (depende del filesystem)
- No soporta metadatos extensivos (solo contentType en `PutOptions`)

### S3Driver
- Depende de AWS SDK (pesado, ~1MB)
- No soporta todos los features de S3 (multipart upload, versioning, etc.)
- `url()` puede no funcionar para buckets privados

### General
- No soporta streaming (todo es Buffer en memoria)
- No hay operaciones de bulk (delete masivo, copy masivo)
- No hay eventos (watcher de cambios)
- No soporta ranges (partial get)

## Buenas prácticas

### 1. Usar keys estructuradas
```typescript
// ✅ Bien: estructura jerárquica
const key = `users/${userId}/avatars/${Date.now()}.png`;

// ❌ No: keys planos
const key = 'avatar123.png';
```

### 2. Validar contenido
```typescript
import { validate } from '@jamx-framework/validator';

const schema = z.object({
  contentType: z.string().regex(/^image\//),
  size: z.number().max(5 * 1024 * 1024), // max 5MB
});

async function safeUpload(key: string, data: Buffer) {
  // Validar antes de subir
  const fileInfo = {
    contentType: detectMimeType(data),
    size: data.length,
  };

  const result = schema.parse(fileInfo);
  await storage.put(key, data, { contentType: result.contentType });
}
```

### 3. Manejar errores
```typescript
try {
  await storage.put(key, data);
} catch (err) {
  if (err.code === 'ENOENT') {
    // directorio no existe (LocalDriver)
    await fs.mkdirSync(path.dirname(localPath), { recursive: true });
    await storage.put(key, data);
  } else if (err.code === 'NoSuchBucket') {
    // bucket no existe (S3Driver)
    await createBucket();
    await storage.put(key, data);
  } else {
    throw err;
  }
}
```

### 4. Limpiar archivos huérfanos
```typescript
// Después de eliminar un usuario, eliminar sus archivos
async function deleteUserFiles(userId: string) {
  const prefix = `users/${userId}/`;
  const files = await storage.list(prefix);

  for (const file of files) {
    await storage.delete(file.key);
  }
}
```

### 5. Usar TTL para archivos temporales
```typescript
import { Scheduler } from '@jamx-framework/scheduler';

// Limpiar archivos temporales cada 24h
scheduler.add({
  name: 'cleanup-temp',
  cron: '0 3 * * *', // 3 AM diario
  handler: async () => {
    const files = await storage.list('temp/');
    const cutoff = Date.now() - 24 * 60 * 60 * 1000;

    for (const file of files) {
      if (file.lastModified.getTime() < cutoff) {
        await storage.delete(file.key);
      }
    }
  },
});
```

## Integración con otros paquetes

### Con `@jamx-framework/server`
```typescript
import { JamxServer, staticFiles } from '@jamx-framework/server';
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

server.use('/uploads', async (req, res) => {
  const key = req.path.slice('/uploads'.length);
  const file = await storage.get(key);

  if (!file) {
    res.notFound();
    return;
  }

  const meta = await storage.stat(key);
  res.header('Content-Type', meta?.contentType ?? 'application/octet-stream');
  res.send(file);
});
```

### Con `@jamx-framework/auth`
```typescript
import { authMiddleware } from '@jamx-framework/auth';
import { Storage } from '@jamx-framework/storage';

const storage = new Storage(config);

server.use(authMiddleware());

server.use('/profile/avatar', async (req, res) => {
  const user = req.locals.user;
  const key = `avatars/${user.id}.png`;

  const file = await storage.get(key);
  if (!file) {
    res.notFound();
    return;
  }

  res.header('Content-Type', 'image/png');
  res.send(file);
});
```

### Con `@jamx-framework/validator`
```typescript
import { validate } from '@jamx-framework/validator';
import { Storage } from '@jamx-framework/storage';

const uploadSchema = z.object({
  key: z.string().regex(/^[a-zA-Z0-9_\-/\.]+$/),
  contentType: z.string(),
  size: z.number().max(10 * 1024 * 1024), // 10MB max
});

async function upload(req: JamxRequest, res: JamxResponse) {
  const result = validate(req.body, uploadSchema);
  if (!result.valid) {
    res.json({ errors: result.errors }, 400);
    return;
  }

  const { key, contentType, size } = result.value;
  const buffer = req.body as Buffer; // asumiendo bodyParser con raw

  if (buffer.length !== size) {
    res.json({ error: 'Size mismatch' }, 400);
    return;
  }

  await storage.put(key, buffer, { contentType });
  res.json({ url: await storage.url(key) });
}
```

## Preguntas frecuentes

### ¿Cómo manejar uploads grandes?
Para archivos > 10MB, usar streaming o multipart upload (S3):

```typescript
// S3 multipart upload (no implementado por defecto)
// Considerar usar AWS SDK directamente para casos complejos
```

### ¿Cómo servir archivos privados?
Para S3 privado, generar signed URLs:

```typescript
// Extender S3Driver
async signedUrl(key: string, expiresIn: number = 3600): Promise<string> {
  const command = new GetObjectCommand({
    Bucket: this.config.bucket,
    Key: key,
  });
  return await getSignedUrl(this.client, command, { expiresIn });
}
```

### ¿Cómo cambiar de driver?
La API es la misma para todos los drivers:

```typescript
// Cambiar de local a S3
const storage = new Storage({
  driver: 's3',
  s3: { /* config */ },
});

// El código de la aplicación no cambia
await storage.put('file.txt', buffer);
```

### ¿Qué pasa si el directorio local no existe?
LocalDriver crea el directorio automáticamente en el primer `put()`.

### ¿Cómo manejar conflictos de nombres?
El storage no previene overwrites. Si subes un archivo con la misma key, se sobrescribe.

```typescript
// Evitar overwrite
if (await storage.exists(key)) {
  throw new Error('File already exists');
}
await storage.put(key, data);
```

### ¿Cómo obtener tamaño total de storage?
```typescript
async function getTotalSize(): Promise<number> {
  let total = 0;
  const files = await storage.list();
  for (const file of files) {
    total += file.size;
  }
  return total;
}
```

## Referencia rápida

### Crear storage
```typescript
const storage = new Storage({
  driver: 'local', // o 's3'
  local: { root: './uploads' },
  // o
  s3: { bucket: '...', region: '...', accessKey: '...', secretKey: '...' },
});
```

### Operaciones CRUD
```typescript
await storage.put(key, buffer, options);
const data = await storage.get(key);
await storage.delete(key);
const exists = await storage.exists(key);
const file = await storage.stat(key);
```

### Listar
```typescript
const files = await storage.list(prefix);
```

### Utilidades
```typescript
const url = await storage.url(key);
await storage.copy(source, dest);
await storage.move(source, dest);
```

## Archivos importantes
- `src/storage.ts` - Clase Storage principal
- `src/drivers/local.ts` - Driver de filesystem local
- `src/drivers/s3.ts` - Driver de S3
- `src/drivers/types.ts` - Tipos compartidos
- `tests/unit/storage.test.ts` - Tests de Storage
- `tests/unit/drivers/local.test.ts` - Tests de LocalDriver

## Dependencias
- `@types/node` - Tipos de Node.js
- `vitest` - Testing
- `rimraf` - Limpieza
- (S3Driver) `@aws-sdk/client-s3` - AWS SDK (no listada en package.json, debe instalarse)

## Scripts del paquete
- `pnpm build` - Compila TypeScript
- `pnpm dev` - Watch mode
- `pnpm test` - Tests unitarios
- `pnpm test:watch` - Tests en watch
- `pnpm type-check` - Verificar tipos
- `pnpm clean` - Limpiar build

## Notas sobre S3Driver
El `S3Driver` requiere `@aws-sdk/client-s3` como dependencia adicional. Asegúrate de instalarla:

```bash
pnpm add -w @aws-sdk/client-s3
```

O si el paquete no la incluye, agregarla a `devDependencies` del paquete.