# ADR-033: File Upload and Storage Strategy

**Date**: 2026-01-09

**Status**: Accepted

**Implemented**: 2026-01-09

## Context

The application handles file uploads for flyer images and user avatars. Without a consistent strategy, file uploads can introduce security vulnerabilities (path traversal, malicious file types), performance issues (unbounded file sizes), and maintenance challenges (inconsistent storage locations).

Key concerns:

1. **Security**: Preventing malicious file uploads, path traversal attacks, and unsafe filenames
2. **Storage Organization**: Consistent directory structure for uploaded files
3. **Size Limits**: Preventing resource exhaustion from oversized uploads
4. **File Type Validation**: Ensuring only expected file types are accepted
5. **Cleanup**: Managing temporary and orphaned files

## Decision

We will implement a centralized file upload strategy using `multer` middleware with custom storage configurations, file type validation, and size limits.

### Storage Types

| Type     | Directory                      | Purpose                        | Size Limit |
| -------- | ------------------------------ | ------------------------------ | ---------- |
| `flyer`  | `$STORAGE_PATH` (configurable) | Flyer images for AI processing | 100MB      |
| `avatar` | `public/uploads/avatars/`      | User profile pictures          | 5MB        |

### Filename Strategy

All uploaded files are renamed to prevent:

- Path traversal attacks
- Filename collisions
- Problematic characters in filenames

**Pattern**: `{fieldname}-{timestamp}-{random}-{sanitized-original}`

Example: `flyer-1704825600000-829461742-grocery-flyer.jpg`

### File Type Validation

Only image files (`image/*` MIME type) are accepted. Non-image uploads are rejected with a structured `ValidationError`.

## Implementation Details

### Multer Configuration Factory

```typescript
import { createUploadMiddleware } from '../middleware/multer.middleware';

// For flyer uploads (100MB limit)
const flyerUpload = createUploadMiddleware({
  storageType: 'flyer',
  fileSize: 100 * 1024 * 1024, // 100MB
  fileFilter: 'image',
});

// For avatar uploads (5MB limit)
const avatarUpload = createUploadMiddleware({
  storageType: 'avatar',
  fileSize: 5 * 1024 * 1024, // 5MB
  fileFilter: 'image',
});
```

### Storage Configuration

```typescript
// Configurable via environment variable
export const flyerStoragePath =
  process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images';

// Relative to project root
export const avatarStoragePath = path.join(process.cwd(), 'public', 'uploads', 'avatars');
```

### Filename Sanitization

The `sanitizeFilename` utility removes dangerous characters:

```typescript
// Removes: path separators, null bytes, special characters
// Keeps: alphanumeric, dots, hyphens, underscores
const sanitized = sanitizeFilename(file.originalname);
```

### Required File Validation Middleware

Ensures a file was uploaded before processing:

```typescript
import { requireFileUpload } from '../middleware/fileUpload.middleware';

router.post(
  '/upload',
  flyerUpload.single('flyerImage'),
  requireFileUpload('flyerImage'), // 400 error if missing
  handleMulterError,
  async (req, res) => {
    // req.file is guaranteed to exist
  },
);
```

### Error Handling

```typescript
import { handleMulterError } from '../middleware/multer.middleware';

// Catches multer-specific errors (file too large, etc.)
router.use(handleMulterError);
```

### Directory Initialization

Storage directories are created automatically at application startup:

```typescript
(async () => {
  await fs.mkdir(flyerStoragePath, { recursive: true });
  await fs.mkdir(avatarStoragePath, { recursive: true });
})();
```

### Test Environment Handling

In test environments, files use predictable names for easy cleanup:

```typescript
if (process.env.NODE_ENV === 'test') {
  return cb(null, `test-avatar${path.extname(file.originalname) || '.png'}`);
}
```

## Usage Example

```typescript
import { createUploadMiddleware, handleMulterError } from '../middleware/multer.middleware';
import { requireFileUpload } from '../middleware/fileUpload.middleware';
import { validateRequest } from '../middleware/validation.middleware';
import { aiUploadLimiter } from '../config/rateLimiters';

const flyerUpload = createUploadMiddleware({
  storageType: 'flyer',
  fileSize: 100 * 1024 * 1024,
  fileFilter: 'image',
});

router.post(
  '/upload-and-process',
  aiUploadLimiter,
  validateRequest(uploadSchema),
  flyerUpload.single('flyerImage'),
  requireFileUpload('flyerImage'),
  handleMulterError,
  async (req, res, next) => {
    const filePath = req.file!.path;
    // Process the uploaded file...
  },
);
```

## Key Files

- `src/middleware/multer.middleware.ts` - Multer configuration and storage handlers
- `src/middleware/fileUpload.middleware.ts` - File requirement validation
- `src/utils/stringUtils.ts` - Filename sanitization utilities
- `src/utils/fileUtils.ts` - File system utilities (deletion, etc.)

## Consequences

### Positive

- **Security**: Prevents path traversal and malicious uploads through sanitization and validation
- **Consistency**: All uploads follow the same patterns and storage organization
- **Predictability**: Test environments use predictable filenames for cleanup
- **Extensibility**: Factory pattern allows easy addition of new upload types

### Negative

- **Disk Storage**: Files stored on disk require backup and cleanup strategies
- **Single Server**: Current implementation doesn't support cloud storage (S3, etc.)
- **No Virus Scanning**: Files aren't scanned for malware before processing

## Future Enhancements

1. **Cloud Storage**: Support for S3/GCS as storage backend
2. **Virus Scanning**: Integrate ClamAV or cloud-based scanning
3. **Image Optimization**: Automatic resizing/compression before storage
4. **CDN Integration**: Serve uploaded files through CDN
5. **Cleanup Job**: Scheduled job to remove orphaned/temporary files
6. **Presigned URLs**: Direct upload to cloud storage to reduce server load
