# ginxsom 🌸

A high-performance [Blossom](https://github.com/hzrd149/blossom) server implementation in C, designed for optimal integration with nginx for blazing-fast file serving.

## Overview

Ginxsom is a Blossom protocol server implemented as a FastCGI application that integrates seamlessly with nginx. Nginx handles static file serving directly while ginxsom processes authenticated operations (uploads, deletes, management) via FastCGI. This architecture provides optimal performance with nginx's excellent static file serving and C's efficiency for cryptographic operations.

### Why Ginxsom?

- **Performance**: C application with nginx static serving = maximum throughput
- **Simplicity**: Clean separation between static serving (nginx) and dynamic operations (C app)
- **Scalability**: nginx handles the heavy file serving load, C app handles auth and metadata
- **Standards Compliant**: Full Blossom protocol implementation with nostr authentication

## Architecture

```
┌─────────────┐    ┌──────────────┐    ┌─────────────────┐
│   Client    │    │    nginx     │    │ ginxsom FastCGI │
│             │───▶│              │───▶│                 │
│             │    │              │    │                 │
└─────────────┘    └──────────────┘    └─────────────────┘
                          │                      │
                          ▼                      ▼
                   ┌─────────────┐    ┌─────────────────┐
                   │    Blobs    │    │   SQLite DB     │
                   │ (flat files)│    │  (metadata)     │
                   └─────────────┘    └─────────────────┘
```

### Request Flow

1. **File Retrieval (`GET /<sha256>`)**: nginx serves directly from disk
2. **File Upload (`PUT /upload`)**: nginx calls ginxsom via FastCGI for authentication + storage
3. **File Management (`DELETE`, `LIST`)**: nginx calls ginxsom via FastCGI
4. **Metadata Operations**: ginxsom manages SQLite database

## Blossom Protocol Support

ginxsom implements the following Blossom Upgrade Documents (BUDs):

- [x] **BUD-01**: Server requirements and blob retrieval
- [x] **BUD-02**: Blob upload and management
- [x] **BUD-04**: Blob Mirroring
- [ ] **BUD-05**: Media Optimization *(Partial)*
- [x] **BUD-06**: Upload Requirements
- [ ] **BUD-07**: Payment Integration *(Not implemented)*
- [x] **BUD-08**: NIP-94 File Metadata Tags
- [x] **BUD-09**: Content Reporting

### Supported Endpoints

| Endpoint | Method | Description | Handler | Status |
|----------|---------|-------------|---------|---------|
| `/<sha256>` | GET | Retrieve blob | nginx → disk | ✅ **Implemented** |
| `/<sha256>` | HEAD | Check blob exists | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/upload` | PUT | Upload new blob | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/upload` | HEAD | Check upload requirements | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/list/<pubkey>` | GET | List user's blobs | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/<sha256>` | DELETE | Delete blob | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/mirror` | PUT | Mirror blob from URL | nginx → FastCGI ginxsom | ✅ **Implemented** |
| `/report` | PUT | Report blob content | nginx → FastCGI ginxsom | ✅ **Implemented** |

## Installation

### Prerequisites

```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install build-essential libssl-dev libsqlite3-dev libfcgi-dev pkg-config

# RHEL/CentOS/Fedora  
sudo dnf install gcc make openssl-devel sqlite-devel fcgi-devel pkgconfig

# macOS
brew install openssl sqlite fcgi pkg-config
```

### Building ginxsom

```bash
git clone https://github.com/yourusername/ginxsom.git
cd ginxsom
make
```

### Installation

```bash
sudo make install
# Installs to /usr/local/bin/ginxsom
```

## Configuration

### ginxsom Configuration

Create `/etc/ginxsom/config.conf`:

```ini
[server]
socket_path = /run/ginxsom.sock
max_file_size = 104857600  # 100MB
storage_path = /var/lib/ginxsom/blobs
database_path = /var/lib/ginxsom/blobs.db

[auth]
require_auth_upload = true
require_auth_get = false
require_auth_list = false

[limits]
max_blobs_per_user = 1000
rate_limit_uploads = 10  # per minute
```

### nginx Configuration

#### Production Configuration

Add to your nginx configuration:

```nginx
server {
    listen 80;
    server_name your-blossom-server.com;
    
    # CORS headers for all responses
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET, HEAD, PUT, DELETE, OPTIONS";
    add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, X-SHA-256, X-Content-Type, X-Content-Length";
    add_header Access-Control-Max-Age 86400;
    
    # Handle preflight OPTIONS requests
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    
    # Static blob serving - nginx handles directly
    location ~ ^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$ {
        root /var/lib/ginxsom/blobs;
        try_files /$1$2 =404;
        add_header Accept-Ranges bytes;
        
        # Set content type based on file extension if not detected
        location ~* \.(pdf)$ { add_header Content-Type application/pdf; }
        location ~* \.(jpg|jpeg)$ { add_header Content-Type image/jpeg; }
        location ~* \.(png)$ { add_header Content-Type image/png; }
        location ~* \.(mp4)$ { add_header Content-Type video/mp4; }
    }
    
    # All other requests go to ginxsom FastCGI
    location / {
        include fastcgi_params;
        fastcgi_pass unix:/run/ginxsom.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        
        # Handle large uploads
        client_max_body_size 100M;
        fastcgi_request_buffering off;
    }
}
```

#### Local Development Configuration

For local development, use the provided `config/local-nginx.conf`:

```nginx
# Local development server (runs on port 9001)
server {
    listen 9001;
    server_name localhost;
    root blobs;  # Relative to project directory
    
    # FastCGI backend
    upstream fastcgi_backend {
        server unix:/tmp/ginxsom-fcgi.sock;
    }
    
    # DELETE endpoint - requires authentication
    location ~ "^/([a-f0-9]{64}).*$" {
        if ($request_method != DELETE) {
            return 404;
        }
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
        fastcgi_pass fastcgi_backend;
    }
    
    # Static blob serving with extension fallback
    location ~ "^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$" {
        limit_except HEAD GET { deny all; }
        
        # HEAD requests go to FastCGI
        if ($request_method = HEAD) {
            rewrite ^/(.*)$ /fcgi-head/$1 last;
        }
        
        # GET requests served directly with extension fallback
        try_files /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.txt /$1.md =404;
    }
    
    # Upload endpoint
    location /upload {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
        fastcgi_pass fastcgi_backend;
        if ($request_method !~ ^(PUT)$ ) { return 405; }
    }
    
    # List blobs endpoint
    location ~ "^/list/([a-f0-9]{64}).*$" {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
        fastcgi_pass fastcgi_backend;
        if ($request_method !~ ^(GET)$ ) { return 405; }
    }
}
```

Start local development with:
```bash
# Start FastCGI daemon
./start-fcgi.sh

# Start nginx (uses local config)
./restart-nginx.sh
```

## Usage

### Starting the Server

```bash
# Start ginxsom FastCGI daemon
sudo spawn-fcgi -s /run/ginxsom.sock -f /usr/local/bin/ginxsom -u www-data -g www-data

# Or with systemd
sudo systemctl enable ginxsom
sudo systemctl start ginxsom
```

### Testing

```bash
# Check server is running
curl -I http://localhost/upload

# Upload a file (requires nostr authorization)
curl -X PUT http://localhost/upload \
  -H "Authorization: Nostr eyJ..." \
  -H "Content-Type: application/pdf" \
  --data-binary @document.pdf

# Retrieve a file
curl http://localhost/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf
```

## API Documentation

### Authentication

All write operations require nostr event authentication via the `Authorization` header:

```
Authorization: Nostr <base64-encoded-nostr-event>
```

The nostr event must be kind `24242` with appropriate tags:
- `t` tag: `upload`, `delete`, `list`, or `get`  
- `x` tag: SHA-256 hash for blob-specific operations
- `expiration` tag: Unix timestamp when event expires

### Blob Descriptors

Successful uploads return blob descriptors with optional NIP-94 metadata:

```json
{
  "url": "https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf",
  "sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
  "size": 184292,
  "type": "application/pdf",
  "uploaded": 1725105921,
  "nip94": [
    ["url", "https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"],
    ["m", "application/pdf"],
    ["x", "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"],
    ["size", "184292"]
  ]
}
```

**NIP-94 Tags:**
- `url`: Canonical blob URL with proper CDN origin
- `m`: MIME type (Content-Type)
- `x`: SHA-256 hash (lowercase hex)
- `size`: File size in bytes
- `dim`: Image dimensions (when available, e.g., "1920x1080")

The `nip94` field is included by default but can be disabled via server configuration.

### Content Reporting (BUD-09)

ginxsom implements BUD-09 Content Reporting using NIP-56 report events for moderating inappropriate content. Users can submit cryptographically signed reports about blobs containing objectionable material.

#### Report Event Structure

Reports use NIP-56 kind 1984 events with the following structure:

```json
{
  "kind": 1984,
  "content": "This content violates community guidelines",
  "tags": [
    ["x", "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553", "nudity"],
    ["x", "a8472f6d93e42c1e5b4e6f9a1b3c8d4e6f9a1b3c5d7e8f0a1b2c3d4e5f6a7b8", "spam"]
  ],
  "created_at": 1725105921,
  "pubkey": "reporter_public_key",
  "id": "event_id",
  "sig": "cryptographic_signature"
}
```

#### Supported Report Types

- **nudity**: Adult or sexually explicit content
- **malware**: Malicious software or files
- **profanity**: Offensive language or content
- **illegal**: Content that violates laws
- **spam**: Unwanted promotional content
- **impersonation**: Content impersonating others
- **other**: Any other inappropriate content
- **extensible**: Unknown types are accepted per NIP-56

#### Report Submission

Submit reports to the `/report` endpoint:

```bash
# Generate report with nak tool
REPORT=$(nak event -k 1984 -c "Report description" -t "x=deadbeef...;nudity")

# Submit report
curl -X PUT http://localhost:9001/report \
  -H "Content-Type: application/json" \
  -d "$REPORT"
```

#### Response Format

Successful reports return confirmation:

```json
{
  "message": "Report received",
  "reported_blobs": 2,
  "reporter": "reporter_pubkey_bytes"
}
```

Error responses include specific error codes:
- `invalid_json`: Malformed JSON
- `invalid_report_event`: Invalid NIP-56 structure
- `no_blob_hashes`: Missing valid SHA-256 hashes
- `unsupported_media_type`: Non-JSON Content-Type

## Administrator API

Ginxsom uses an **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key. All admin commands use **NIP-44 encrypted command arrays** for security.

### Authentication

All admin commands require signing with the admin private key configured in the server. The admin public key is stored in the database and checked against incoming Kind 23458 events.

### Event Structure

**Admin Command Event (Kind 23458):**
```json
{
  "id": "event_id",
  "pubkey": "admin_public_key",
  "created_at": 1234587890,
  "kind": 23458,
  "content": "NIP44_ENCRYPTED_COMMAND_ARRAY",
  "tags": [
    ["p", "blossom_server_pubkey"]
  ],
  "sig": "event_signature"
}
```

The `content` field contains a NIP-44 encrypted JSON array representing the command.

**Admin Response Event (Kind 23459):**
```json
{
  "id": "response_event_id",
  "pubkey": "blossom_server_pubkey",
  "created_at": 1234587890,
  "kind": 23459,
  "content": "NIP44_ENCRYPTED_RESPONSE_OBJECT",
  "tags": [
    ["p", "admin_public_key"],
    ["e", "request_event_id"]
  ],
  "sig": "response_event_signature"
}
```

The `content` field contains a NIP-44 encrypted JSON response object.

### Admin Commands

All commands are sent as NIP-44 encrypted JSON arrays in the event content:

| Command Type | Command Format | Description |
|--------------|----------------|-------------|
| **Configuration Management** |
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| `config_update` | `["config_update", [{"key": "max_file_size", "value": "209715200", ...}]]` | Update configuration parameters |
| **Statistics & Monitoring** |
| `stats_query` | `["stats_query"]` | Get comprehensive database and storage statistics |
| `system_status` | `["system_command", "system_status"]` | Get system status and health metrics |
| **Blossom Operations** |
| `blob_list` | `["blob_list", "all"]` or `["blob_list", "pubkey", "abc123..."]` | List blobs with filtering |
| `storage_stats` | `["storage_stats"]` | Get detailed storage statistics |
| `mirror_status` | `["mirror_status"]` | Get status of mirroring operations |
| `report_query` | `["report_query", "all"]` | Query content reports (BUD-09) |
| **Authorization Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
| **Database Queries** |
| `sql_query` | `["sql_query", "SELECT * FROM blobs LIMIT 10"]` | Execute read-only SQL query |

### Configuration Categories

**Blossom Settings:**
- `max_file_size`: Maximum upload size in bytes
- `storage_path`: Blob storage directory path
- `cdn_origin`: CDN URL for blob descriptors
- `enable_nip94`: Include NIP-94 tags in responses

**Relay Client Settings:**
- `enable_relay_connect`: Enable relay client functionality
- `kind_0_content`: Profile metadata JSON
- `kind_10002_tags`: Relay list JSON array

**Authentication Settings:**
- `auth_rules_enabled`: Enable auth rules system
- `require_auth_upload`: Require authentication for uploads
- `require_auth_delete`: Require authentication for deletes

**Authorization Rules:**
- `rule_type`: Type of rule (`pubkey_blacklist`, `pubkey_whitelist`, `hash_blacklist`, `mime_blacklist`, `mime_whitelist`)
- `pattern_type`: Pattern matching type (`pubkey`, `hash`, `mime`)
- `pattern_value`: The actual value to match (64-char hex for pubkey/hash, MIME type string for mime)
- `active`: Whether rule is active (1) or disabled (0)

**Limits:**
- `max_blobs_per_user`: Per-user blob limit
- `rate_limit_uploads`: Uploads per minute
- `max_total_storage`: Total storage limit in bytes

### Response Format

All admin commands return signed EVENT responses via the relay connection. Responses use NIP-44 encrypted JSON content with structured data.

**Success Response Example:**
```json
{
  "query_type": "stats_query",
  "timestamp": 1234587890,
  "database_size_bytes": 1048576,
  "storage_size_bytes": 10737418240,
  "total_blobs": 1543,
  "blob_types": [
    {"type": "image/jpeg", "count": 856, "size_bytes": 5368709120}
  ]
}
```

**Error Response Example:**
```json
{
  "query_type": "config_update",
  "status": "error",
  "error": "invalid configuration value",
  "timestamp": 1234587890
}
```

### Security Features

- **Cryptographic Authentication**: Only admin pubkey can send commands
- **NIP-44 Encryption**: All commands and responses are encrypted
- **Command Logging**: All admin actions logged to database
- **SQL Safety**: Only SELECT statements allowed with timeout and row limits
- **Rate Limiting**: Prevents admin command flooding

For detailed command specifications and examples, see [`docs/ADMIN_COMMANDS_PLAN.md`](docs/ADMIN_COMMANDS_PLAN.md).

## File Storage

### Current (Flat) Structure
```
/var/lib/ginxsom/blobs/
├── b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf
├── a8472f6d93e42c1e5b4e9f3a7b2c8d4e6f9a1b3c5d7e8f0a1b2c3d4e5f6a7b8.png
└── ...
```

### Future (Optimized) Structure
```
/var/lib/ginxsom/blobs/
├── b1/
│   └── 67/
│       └── b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf
├── a8/
│   └── 47/
│       └── a8472f6d93e42c1e5b4e6f9a1b3c5d7e8f0a1b2c3d4e6f9a1b3c5d7e8f0a1b2c3d4e5f6a7b8.png
```

## File and Extension Handling

ginxsom implements a sophisticated file and extension handling strategy that optimizes both performance and flexibility:

### Database-Driven Architecture

The system uses the SQLite database as the **single source of truth** for blob existence and metadata:

```sql
-- Database schema (clean SHA-256 hashes, no extensions)
CREATE TABLE blobs (
    sha256 TEXT PRIMARY KEY,           -- Clean hash: b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553
    size INTEGER NOT NULL,
    type TEXT NOT NULL,
    uploaded_at INTEGER NOT NULL,
    uploader_pubkey TEXT,
    filename TEXT                      -- Original filename: document.pdf
);
```

**Key Benefits:**
- **Database Lookup vs Filesystem**: FastCGI queries the database instead of checking filesystem
- **Single Source of Truth**: Database definitively knows if a blob exists
- **Extension Irrelevant**: Database uses clean SHA-256 hashes without extensions
- **Performance**: Database queries are faster than filesystem stat() calls

### URL and Extension Support

ginxsom supports flexible URL patterns for maximum client compatibility:

```
# Both forms work identically:
GET /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553
GET /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf

# HEAD requests work with or without extensions:
HEAD /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553
HEAD /b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf
```

### nginx File Storage Strategy

Files are stored on disk **with extensions** for nginx MIME type detection and optimal performance:

```nginx
# Blossom-compliant nginx configuration
location ~ ^/([a-f0-9]{64}).*$ {
    root /var/lib/ginxsom/blobs;
    try_files /$1* =404;
}
```

**How it works:**

1. **Hash-only lookup**: nginx extracts the 64-character SHA-256 hash from the URL, ignoring any extension
2. **Wildcard matching**: `try_files /$1*` finds any file starting with that hash
3. **Blossom compliance**: Serves correct file regardless of URL extension

**Examples:**

Client requests: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- nginx extracts hash: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553`
- nginx looks for: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553*`
- nginx finds: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- nginx serves the PDF with correct `Content-Type: application/pdf`

Client requests: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.mp3`
- nginx extracts same hash: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553`
- nginx looks for: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553*`
- nginx finds: `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- nginx serves the PDF (not 404) with correct `Content-Type: application/pdf`

**Key Benefits:**

- **Blossom Protocol Compliance**: Accepts any extension, returns correct MIME type
- **Performance**: nginx serves files directly without FastCGI involvement
- **Flexibility**: URLs work with correct extension, wrong extension, or no extension
- **MIME Detection**: nginx determines Content-Type from actual file extension on disk

This approach ensures that files are always found by their SHA-256 hash regardless of what extension (if any) is used in the request URL, while maintaining nginx's excellent static file serving performance.

### HEAD Request Handling

HEAD requests are processed differently to ensure accuracy:

1. **nginx receives HEAD request**: `/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
2. **nginx forwards to FastCGI**: All HEAD requests go to ginxsom for metadata
3. **ginxsom extracts clean hash**: Strips `.pdf` → `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553`
4. **ginxsom queries database**: `SELECT size, type FROM blobs WHERE sha256 = ?`
5. **ginxsom returns headers**: Content-Length, Content-Type, etc.

### Why This Approach?

**Performance:**
- GET requests: nginx serves directly from disk (no database hit)
- HEAD requests: Single database query (no filesystem checking)
- Extension mismatches: Eliminated by database-driven approach

**Reliability:**
- Database is authoritative source of blob existence
- No race conditions between filesystem and metadata
- Consistent behavior regardless of URL format

**Flexibility:**
- Clients can use URLs with or without extensions
- Browser-friendly URLs with proper file extensions
- API-friendly clean hash URLs

**Scalability:**
- nginx handles the heavy lifting (file serving)
- FastCGI only processes metadata operations
- Database queries scale better than filesystem operations

## Development

### Project Structure

```
ginxsom/
├── src/
│   ├── main.c              # Main server loop
│   ├── config.c            # Configuration parsing
│   ├── server.c            # HTTP server logic
│   ├── auth.c              # Nostr authentication
│   ├── storage.c           # File storage operations
│   ├── database.c          # SQLite operations
│   └── utils.c             # Utility functions
├── include/
│   └── ginxsom.h           # Main header file
├── tests/
│   └── test_*.c            # Unit tests
├── docs/
│   └── api.md              # API documentation
├── Makefile
└── README.md
```

### Building for Development

```bash
make debug    # Build with debug symbols
make test     # Run test suite
make clean    # Clean build artifacts
```

### Dependencies

- **libssl**: For cryptographic operations (nostr event verification)
- **libsqlite3**: For metadata storage
- **libfcgi**: For FastCGI functionality

## Performance Characteristics

- **Static File Serving**: nginx performance (typically >10k req/s)
- **Upload Processing**: Limited by disk I/O and crypto verification
- **Memory Usage**: Minimal - FastCGI processes are lightweight
- **Concurrent Operations**: nginx manages FastCGI process pool efficiently
- **Process Management**: nginx spawns/manages ginxsom FastCGI processes

## Security Considerations

- All uploads require valid nostr event signatures
- SHA-256 verification prevents data corruption
- Rate limiting prevents abuse
- File size limits prevent disk exhaustion
- No script execution - blobs stored as static files

## Monitoring

ginxsom provides metrics via `/metrics` endpoint:

```
# HELP ginxsom_uploads_total Total number of uploads processed
# TYPE ginxsom_uploads_total counter
ginxsom_uploads_total 1234

# HELP ginxsom_storage_bytes Total bytes stored
# TYPE ginxsom_storage_bytes gauge  
ginxsom_storage_bytes 1073741824
```

## License

[License information here]

## Contributing

[Contributing guidelines here]

---

**Note**: This project is under active development. Please report bugs and feature requests via GitHub issues.
