# Tybo Media Service

A **standalone, self-contained** upload module backed by Cloudflare R2.

Designed to be copied into its own repository with minimal changes.

---

## Responsibility

| Concern            | Owner                     |
|--------------------|---------------------------|
| Upload files       | `MediaUploadService`      |
| Validate uploads   | `UploadValidator`         |
| Store on R2        | `R2Storage`               |
| Generate thumbnails| `ThumbnailGenerator`      |
| Return metadata    | `MediaUploadService`      |
| Config / constants | `MediaConfig`             |

Business applications **never** talk to Cloudflare R2 directly.

---

## Folder Structure

```text
media-service/
├── config/
│   ├── env.php           # .env loader (copy of project version)
│   └── MediaConfig.php   # All constants: MIME types, folders, size limits, ACLs
├── src/
│   ├── Helpers/
│   │   └── JsonResponse.php
│   ├── Services/
│   │   └── MediaUploadService.php   # Orchestrates upload + thumbnail + response
│   ├── Storage/
│   │   └── R2Storage.php            # AWS S3Client wrapper for Cloudflare R2
│   ├── Thumbnail/
│   │   └── ThumbnailGenerator.php # GD-based WebP thumbnail creation
│   └── Validation/
│       └── UploadValidator.php    # Reusable validation rules
├── public/
│   ├── bootstrap.php     # CORS, autoloader, shared setup
│   ├── upload.php        # POST /upload
│   ├── delete.php        # DELETE /delete
│   └── health.php        # GET /health
├── .env.example
└── README.md
```

---

## Public Endpoints

| Method | Endpoint  | Description                                    |
|--------|-----------|------------------------------------------------|
| POST   | /upload   | Multipart file upload. Returns URL, key, etc.  |
| DELETE | /delete   | Delete an object by `?key=posts/…/uuid.jpg`    |
| GET    | /health   | Lightweight config check (no R2 API call)      |

### POST /upload

**Parameters (multipart/form-data)**

| Field  | Required | Description                                           |
|--------|----------|-------------------------------------------------------|
| file   | yes      | The file to upload                                    |
| folder | no       | Target folder. Default: `posts`. Allowed see below.   |

**Allowed folders:** `posts`, `products`, `profiles`, `videos`, `thumbnails`, `invoices`

**Response (200)**

```json
{
  "success": true,
  "url": "https://pub-XXXX.r2.dev/posts/2026/08/uuid.jpg",
  "key": "posts/2026/08/uuid.jpg",
  "thumbnail_url": "https://pub-XXXX.r2.dev/posts/2026/08/uuid_thumb.webp",
  "mime": "image/jpeg",
  "size": 12345,
  "original_name": "photo.jpg"
}
```

**Response (error)**

```json
{
  "success": false,
  "message": "Unsupported file type: image/svg+xml"
}
```

### DELETE /delete

**Parameters**

| Param | Required | Description              |
|-------|----------|--------------------------|
| key   | yes      | Full R2 object key       |

**Response (200)**

```json
{ "success": true }
```

### GET /health

**Response (200)**

```json
{
  "success": true,
  "status": "ok",
  "storage": "r2"
}
```

---

## Configuration

Copy `.env.example` to `.env` and fill in your Cloudflare R2 credentials:

```bash
R2_ACCOUNT_ID=your_cloudflare_account_id
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET=tybo-media
R2_PUBLIC_URL=https://pub-XXXX.r2.dev
```

All constants (MIME types, size limits, folders, ACL rules) live in `config/MediaConfig.php`.

---

## Design Decisions

1. **Zero framework dependency** — plain PHP 8.2+, no Composer packages beyond `aws/aws-sdk-php`.
2. **Bootstrap autoloader** — `public/bootstrap.php` tries the parent `vendor/` first (when nested inside the main project) then falls back to a local `vendor/` (when standalone).
3. **Best-effort thumbnails** — Thumbnail generation never fails the main upload; failures are silently swallowed.
4. **Validation extracted** — `UploadValidator` is stateless and reusable; rules are not trapped in controllers.
5. **Storage abstraction** — `R2Storage` hides bucket names, endpoints, and the AWS SDK from business code.

---

## Future Expansion

The architecture is intentionally open for these endpoints without structural changes:

| Endpoint            | Future Use Case                               |
|---------------------|-----------------------------------------------|
| POST /signed-upload | Pre-signed upload URLs for large files        |
| POST /signed-download | Pre-signed download URLs for private files  |
| GET /file/{id}      | Metadata lookup by key                        |
| POST /copy          | Duplicate an object to another folder          |
| POST /move          | Relocate an object                             |

Simply add new controller scripts in `public/` and delegate to `R2Storage` or new service classes.

---

## Copying to a New Repository

1. Copy the entire `media-service/` folder.
2. Run `composer init` and `composer require aws/aws-sdk-php`.
3. Ensure `vendor/autoload.php` exists at `media-service/vendor/autoload.php`.
4. Update `public/bootstrap.php` if you want to remove the parent-vendor fallback.
5. Point your web server to `media-service/public/`.

No code inside `src/` or `config/` needs to change.
