From 476b9bd2e413ed743e03ea002f533eb7d38764b8 Mon Sep 17 00:00:00 2001 From: kqjy Date: Mon, 20 Apr 2026 21:27:02 +0800 Subject: [PATCH] First porting of Python to Rust - update docs and bug fixes --- README.md | 393 +- docs.md | 2716 +--------- python/app/ui.py | 6 +- python/templates/bucket_detail.html | 8 +- rust/myfsio-engine/Cargo.lock | 178 +- rust/myfsio-engine/Cargo.toml | 7 +- .../crates/myfsio-auth/Cargo.toml | 5 +- .../crates/myfsio-auth/src/fernet.rs | 44 +- .../crates/myfsio-auth/src/iam.rs | 321 +- .../crates/myfsio-auth/src/lib.rs | 6 +- .../crates/myfsio-auth/src/sigv4.rs | 38 +- .../crates/myfsio-common/Cargo.toml | 4 +- .../crates/myfsio-crypto/Cargo.toml | 4 +- .../crates/myfsio-crypto/src/aes_gcm.rs | 23 +- .../crates/myfsio-crypto/src/encryption.rs | 52 +- .../crates/myfsio-crypto/src/hashing.rs | 10 +- .../crates/myfsio-crypto/src/kms.rs | 4 +- .../crates/myfsio-crypto/src/lib.rs | 4 +- .../crates/myfsio-server/Cargo.toml | 7 +- .../crates/myfsio-server/src/config.rs | 92 +- .../myfsio-server/src/handlers/admin.rs | 951 +++- .../myfsio-server/src/handlers/chunked.rs | 6 +- .../myfsio-server/src/handlers/config.rs | 379 +- .../crates/myfsio-server/src/handlers/kms.rs | 473 +- .../crates/myfsio-server/src/handlers/mod.rs | 327 +- .../myfsio-server/src/handlers/select.rs | 94 +- .../crates/myfsio-server/src/handlers/ui.rs | 56 +- .../myfsio-server/src/handlers/ui_api.rs | 3506 ++++++++++++ .../myfsio-server/src/handlers/ui_pages.rs | 2899 +++++++++- .../crates/myfsio-server/src/lib.rs | 462 +- .../crates/myfsio-server/src/main.rs | 348 +- .../myfsio-server/src/middleware/auth.rs | 962 +++- .../myfsio-server/src/middleware/mod.rs | 77 +- .../myfsio-server/src/middleware/session.rs | 65 +- .../src/services/access_logging.rs | 105 + .../crates/myfsio-server/src/services/gc.rs | 35 +- .../myfsio-server/src/services/integrity.rs | 666 ++- .../myfsio-server/src/services/lifecycle.rs | 26 +- .../myfsio-server/src/services/metrics.rs | 9 +- .../crates/myfsio-server/src/services/mod.rs | 4 +- .../myfsio-server/src/services/replication.rs | 145 +- .../myfsio-server/src/services/s3_client.rs | 4 +- .../src/services/site_registry.rs | 7 +- .../myfsio-server/src/services/site_sync.rs | 33 +- .../src/services/system_metrics.rs | 203 + .../src/services/website_domains.rs | 5 +- .../crates/myfsio-server/src/session.rs | 5 +- .../crates/myfsio-server/src/state.rs | 33 +- .../crates/myfsio-server/src/templates.rs | 95 +- .../crates/myfsio-server/static/css/main.css | 3157 +++++++++++ .../myfsio-server/static/images/MyFSIO.ico | Bin 0 -> 205086 bytes .../myfsio-server/static/images/MyFSIO.png | Bin 0 -> 893198 bytes .../static/js/bucket-detail-main.js | 4827 +++++++++++++++++ .../static/js/bucket-detail-operations.js | 192 + .../static/js/bucket-detail-upload.js | 600 ++ .../static/js/bucket-detail-utils.js | 120 + .../static/js/connections-management.js | 343 ++ .../myfsio-server/static/js/iam-management.js | 846 +++ .../crates/myfsio-server/static/js/ui-core.js | 334 ++ .../crates/myfsio-server/templates/base.html | 6 +- .../templates/bucket_detail.html | 108 +- .../myfsio-server/templates/buckets.html | 4 +- .../myfsio-server/templates/connections.html | 8 +- .../crates/myfsio-server/templates/docs.html | 91 +- .../crates/myfsio-server/templates/iam.html | 62 +- .../crates/myfsio-server/templates/login.html | 2 +- .../templates/replication_wizard.html | 2 +- .../crates/myfsio-server/templates/sites.html | 18 +- .../myfsio-server/templates/system.html | 43 +- .../templates/website_domains.html | 6 +- .../crates/myfsio-server/tests/integration.rs | 1364 ++++- .../myfsio-server/tests/template_render.rs | 343 ++ .../crates/myfsio-storage/Cargo.toml | 4 +- .../crates/myfsio-storage/src/error.rs | 14 +- .../crates/myfsio-storage/src/fs_backend.rs | 141 +- .../crates/myfsio-storage/src/lib.rs | 4 +- .../crates/myfsio-storage/src/traits.rs | 37 +- .../crates/myfsio-storage/src/validation.rs | 11 +- .../crates/myfsio-xml/Cargo.toml | 4 +- .../crates/myfsio-xml/src/lib.rs | 2 +- .../crates/myfsio-xml/src/request.rs | 6 +- .../crates/myfsio-xml/src/response.rs | 213 +- 82 files changed, 24682 insertions(+), 4132 deletions(-) create mode 100644 rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_api.rs create mode 100644 rust/myfsio-engine/crates/myfsio-server/src/services/access_logging.rs create mode 100644 rust/myfsio-engine/crates/myfsio-server/src/services/system_metrics.rs create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/css/main.css create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.ico create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.png create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-main.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-operations.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-upload.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-utils.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/connections-management.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/iam-management.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/static/js/ui-core.js create mode 100644 rust/myfsio-engine/crates/myfsio-server/tests/template_render.rs diff --git a/README.md b/README.md index 772178d..b9a6318 100644 --- a/README.md +++ b/README.md @@ -1,255 +1,212 @@ # MyFSIO -A lightweight, S3-compatible object storage system built with Flask. MyFSIO implements core AWS S3 REST API operations with filesystem-backed storage, making it ideal for local development, testing, and self-hosted storage scenarios. +MyFSIO is an S3-compatible object storage server with a Rust runtime and a filesystem-backed storage engine. The active server lives under `rust/myfsio-engine` and serves both the S3 API and the built-in web UI from a single process. + +The repository still contains a `python/` tree, but you do not need Python to run the current server. ## Features -**Core Storage** -- S3-compatible REST API with AWS Signature Version 4 authentication -- Bucket and object CRUD operations -- Object versioning with version history -- Multipart uploads for large files -- Presigned URLs (1 second to 7 days validity) +- S3-compatible REST API with Signature Version 4 authentication +- Browser UI for buckets, objects, IAM users, policies, replication, metrics, and site administration +- Filesystem-backed storage rooted at `data/` +- Bucket versioning, multipart uploads, presigned URLs, CORS, object and bucket tagging +- Server-side encryption and built-in KMS support +- Optional background services for lifecycle, garbage collection, integrity scanning, operation metrics, and system metrics history +- Replication, site sync, and static website hosting support -**Security & Access Control** -- IAM users with access key management and rotation -- Bucket policies (AWS Policy Version 2012-10-17) -- Server-side encryption (SSE-S3 and SSE-KMS) -- Built-in Key Management Service (KMS) -- Rate limiting per endpoint +## Runtime Model -**Advanced Features** -- Cross-bucket replication to remote S3-compatible endpoints -- Hot-reload for bucket policies (no restart required) -- CORS configuration per bucket +MyFSIO now runs as one Rust process: -**Management UI** -- Web console for bucket and object management -- IAM dashboard for user administration -- Inline JSON policy editor with presets -- Object browser with folder navigation and bulk operations -- Dark mode support +- API listener on `HOST` + `PORT` (default `127.0.0.1:5000`) +- UI listener on `HOST` + `UI_PORT` (default `127.0.0.1:5100`) +- Shared state for storage, IAM, policies, sessions, metrics, and background workers -## Architecture - -``` -+------------------+ +------------------+ -| API Server | | UI Server | -| (port 5000) | | (port 5100) | -| | | | -| - S3 REST API |<------->| - Web Console | -| - SigV4 Auth | | - IAM Dashboard | -| - Presign URLs | | - Bucket Editor | -+--------+---------+ +------------------+ - | - v -+------------------+ +------------------+ -| Object Storage | | System Metadata | -| (filesystem) | | (.myfsio.sys/) | -| | | | -| data// | | - IAM config | -| | | - Bucket policies| -| | | - Encryption keys| -+------------------+ +------------------+ -``` +If you want API-only mode, set `UI_ENABLED=false`. There is no separate "UI-only" runtime anymore. ## Quick Start +From the repository root: + ```bash -# Clone and setup -git clone https://gitea.jzwsite.com/kqjy/MyFSIO -cd s3 -python -m venv .venv - -# Activate virtual environment -# Windows PowerShell: -.\.venv\Scripts\Activate.ps1 -# Windows CMD: -.venv\Scripts\activate.bat -# Linux/macOS: -source .venv/bin/activate - -# Install dependencies -pip install -r requirements.txt - -# (Optional) Build Rust native extension for better performance -# Requires Rust toolchain: https://rustup.rs -pip install maturin -cd myfsio_core && maturin develop --release && cd .. - -# Start both servers -python run.py - -# Or start individually -python run.py --mode api # API only (port 5000) -python run.py --mode ui # UI only (port 5100) +cd rust/myfsio-engine +cargo run -p myfsio-server -- ``` -**Credentials:** Generated automatically on first run and printed to the console. If missed, check the IAM config file at `/.myfsio.sys/config/iam.json`. +Useful URLs: -- **Web Console:** http://127.0.0.1:5100/ui -- **API Endpoint:** http://127.0.0.1:5000 +- UI: `http://127.0.0.1:5100/ui` +- API: `http://127.0.0.1:5000/` +- Health: `http://127.0.0.1:5000/myfsio/health` + +On first boot, MyFSIO creates `data/.myfsio.sys/config/iam.json` and prints the generated admin access key and secret key to the console. + +### Common CLI commands + +```bash +# Show resolved configuration +cargo run -p myfsio-server -- --show-config + +# Validate configuration and exit non-zero on critical issues +cargo run -p myfsio-server -- --check-config + +# Reset admin credentials +cargo run -p myfsio-server -- --reset-cred + +# API only +UI_ENABLED=false cargo run -p myfsio-server -- +``` + +## Building a Binary + +```bash +cd rust/myfsio-engine +cargo build --release -p myfsio-server +``` + +Binary locations: + +- Linux/macOS: `rust/myfsio-engine/target/release/myfsio-server` +- Windows: `rust/myfsio-engine/target/release/myfsio-server.exe` + +Run the built binary directly: + +```bash +./target/release/myfsio-server +``` ## Configuration +The server reads environment variables from the process environment and also loads, when present: + +- `/opt/myfsio/myfsio.env` +- `.env` +- `myfsio.env` + +Core settings: + | Variable | Default | Description | -|----------|---------|-------------| -| `STORAGE_ROOT` | `./data` | Filesystem root for bucket storage | -| `IAM_CONFIG` | `.myfsio.sys/config/iam.json` | IAM user and policy store | -| `BUCKET_POLICY_PATH` | `.myfsio.sys/config/bucket_policies.json` | Bucket policy store | -| `API_BASE_URL` | `http://127.0.0.1:5000` | API endpoint for UI calls | -| `MAX_UPLOAD_SIZE` | `1073741824` | Maximum upload size in bytes (1 GB) | -| `MULTIPART_MIN_PART_SIZE` | `5242880` | Minimum multipart part size (5 MB) | -| `UI_PAGE_SIZE` | `100` | Default page size for listings | -| `SECRET_KEY` | `dev-secret-key` | Flask session secret | -| `AWS_REGION` | `us-east-1` | Region for SigV4 signing | -| `AWS_SERVICE` | `s3` | Service name for SigV4 signing | -| `ENCRYPTION_ENABLED` | `false` | Enable server-side encryption | -| `KMS_ENABLED` | `false` | Enable Key Management Service | -| `LOG_LEVEL` | `INFO` | Logging verbosity | -| `SIGV4_TIMESTAMP_TOLERANCE_SECONDS` | `900` | Max time skew for SigV4 requests | -| `PRESIGNED_URL_MAX_EXPIRY_SECONDS` | `604800` | Max presigned URL expiry (7 days) | -| `REPLICATION_CONNECT_TIMEOUT_SECONDS` | `5` | Replication connection timeout | -| `SITE_SYNC_ENABLED` | `false` | Enable bi-directional site sync | -| `OBJECT_TAG_LIMIT` | `50` | Maximum tags per object | +| --- | --- | --- | +| `HOST` | `127.0.0.1` | Bind address for API and UI listeners | +| `PORT` | `5000` | API port | +| `UI_PORT` | `5100` | UI port | +| `UI_ENABLED` | `true` | Disable to run API-only | +| `STORAGE_ROOT` | `./data` | Root directory for buckets and system metadata | +| `IAM_CONFIG` | `/.myfsio.sys/config/iam.json` | IAM config path | +| `API_BASE_URL` | unset | Public API base used by the UI and presigned URL generation | +| `AWS_REGION` | `us-east-1` | Region used in SigV4 scope | +| `SIGV4_TIMESTAMP_TOLERANCE_SECONDS` | `900` | Allowed request time skew | +| `PRESIGNED_URL_MIN_EXPIRY_SECONDS` | `1` | Minimum presigned URL expiry | +| `PRESIGNED_URL_MAX_EXPIRY_SECONDS` | `604800` | Maximum presigned URL expiry | +| `SECRET_KEY` | loaded from `.myfsio.sys/config/.secret` if present | Session signing key and IAM-at-rest encryption key | +| `ADMIN_ACCESS_KEY` | unset | Optional first-run or reset access key | +| `ADMIN_SECRET_KEY` | unset | Optional first-run or reset secret key | + +Feature toggles: + +| Variable | Default | +| --- | --- | +| `ENCRYPTION_ENABLED` | `false` | +| `KMS_ENABLED` | `false` | +| `GC_ENABLED` | `false` | +| `INTEGRITY_ENABLED` | `false` | +| `LIFECYCLE_ENABLED` | `false` | +| `METRICS_HISTORY_ENABLED` | `false` | +| `OPERATION_METRICS_ENABLED` | `false` | +| `WEBSITE_HOSTING_ENABLED` | `false` | +| `SITE_SYNC_ENABLED` | `false` | + +Metrics and replication tuning: + +| Variable | Default | +| --- | --- | +| `OPERATION_METRICS_INTERVAL_MINUTES` | `5` | +| `OPERATION_METRICS_RETENTION_HOURS` | `24` | +| `METRICS_HISTORY_INTERVAL_MINUTES` | `5` | +| `METRICS_HISTORY_RETENTION_HOURS` | `24` | +| `REPLICATION_CONNECT_TIMEOUT_SECONDS` | `5` | +| `REPLICATION_READ_TIMEOUT_SECONDS` | `30` | +| `REPLICATION_MAX_RETRIES` | `2` | +| `REPLICATION_STREAMING_THRESHOLD_BYTES` | `10485760` | +| `REPLICATION_MAX_FAILURES_PER_BUCKET` | `50` | +| `SITE_SYNC_INTERVAL_SECONDS` | `60` | +| `SITE_SYNC_BATCH_SIZE` | `100` | +| `SITE_SYNC_CONNECT_TIMEOUT_SECONDS` | `10` | +| `SITE_SYNC_READ_TIMEOUT_SECONDS` | `120` | +| `SITE_SYNC_MAX_RETRIES` | `2` | +| `SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS` | `1.0` | + +UI asset overrides: + +| Variable | Default | +| --- | --- | +| `TEMPLATES_DIR` | built-in crate templates directory | +| `STATIC_DIR` | built-in crate static directory | + +See [docs.md](./docs.md) for the full Rust-side operations guide. ## Data Layout -``` +```text data/ -├── / # User buckets with objects -└── .myfsio.sys/ # System metadata - ├── config/ - │ ├── iam.json # IAM users and policies - │ ├── bucket_policies.json # Bucket policies - │ ├── replication_rules.json - │ └── connections.json # Remote S3 connections - ├── buckets// - │ ├── meta/ # Object metadata (.meta.json) - │ ├── versions/ # Archived object versions - │ └── .bucket.json # Bucket config (versioning, CORS) - ├── multipart/ # Active multipart uploads - └── keys/ # Encryption keys (SSE-S3/KMS) + / + .myfsio.sys/ + config/ + iam.json + bucket_policies.json + connections.json + operation_metrics.json + metrics_history.json + buckets// + meta/ + versions/ + multipart/ + keys/ ``` -## API Reference - -All endpoints require AWS Signature Version 4 authentication unless using presigned URLs or public bucket policies. - -### Bucket Operations - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/` | List all buckets | -| `PUT` | `/` | Create bucket | -| `DELETE` | `/` | Delete bucket (must be empty) | -| `HEAD` | `/` | Check bucket exists | - -### Object Operations - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/` | List objects (supports `list-type=2`) | -| `PUT` | `//` | Upload object | -| `GET` | `//` | Download object | -| `DELETE` | `//` | Delete object | -| `HEAD` | `//` | Get object metadata | -| `POST` | `//?uploads` | Initiate multipart upload | -| `PUT` | `//?partNumber=N&uploadId=X` | Upload part | -| `POST` | `//?uploadId=X` | Complete multipart upload | -| `DELETE` | `//?uploadId=X` | Abort multipart upload | - -### Bucket Policies (S3-compatible) - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/?policy` | Get bucket policy | -| `PUT` | `/?policy` | Set bucket policy | -| `DELETE` | `/?policy` | Delete bucket policy | - -### Versioning - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `//?versionId=X` | Get specific version | -| `DELETE` | `//?versionId=X` | Delete specific version | -| `GET` | `/?versions` | List object versions | - -### Health Check - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/myfsio/health` | Health check endpoint | - -## IAM & Access Control - -### Users and Access Keys - -On first run, MyFSIO creates a default admin user (`localadmin`/`localadmin`). Use the IAM dashboard to: - -- Create and delete users -- Generate and rotate access keys -- Attach inline policies to users -- Control IAM management permissions - -### Bucket Policies - -Bucket policies follow AWS policy grammar (Version `2012-10-17`) with support for: - -- Principal-based access (`*` for anonymous, specific users) -- Action-based permissions (`s3:GetObject`, `s3:PutObject`, etc.) -- Resource patterns (`arn:aws:s3:::bucket/*`) -- Condition keys - -**Policy Presets:** -- **Public:** Grants anonymous read access (`s3:GetObject`, `s3:ListBucket`) -- **Private:** Removes bucket policy (IAM-only access) -- **Custom:** Manual policy editing with draft preservation - -Policies hot-reload when the JSON file changes. - -## Server-Side Encryption - -MyFSIO supports two encryption modes: - -- **SSE-S3:** Server-managed keys with automatic key rotation -- **SSE-KMS:** Customer-managed keys via built-in KMS - -Enable encryption with: -```bash -ENCRYPTION_ENABLED=true python run.py -``` - -## Cross-Bucket Replication - -Replicate objects to remote S3-compatible endpoints: - -1. Configure remote connections in the UI -2. Create replication rules specifying source/destination -3. Objects are automatically replicated on upload - ## Docker +Build the Rust image from the `rust/` directory: + ```bash -docker build -t myfsio . -docker run -p 5000:5000 -p 5100:5100 -v ./data:/app/data myfsio +docker build -t myfsio ./rust +docker run --rm -p 5000:5000 -p 5100:5100 -v "${PWD}/data:/app/data" myfsio ``` +If the instance sits behind a reverse proxy, set `API_BASE_URL` to the public S3 endpoint. + +## Linux Installation + +The repository includes `scripts/install.sh` for systemd-style Linux installs. Build the Rust binary first, then pass it to the installer: + +```bash +cd rust/myfsio-engine +cargo build --release -p myfsio-server + +cd ../.. +sudo ./scripts/install.sh --binary ./rust/myfsio-engine/target/release/myfsio-server +``` + +The installer copies the binary into `/opt/myfsio/myfsio`, writes `/opt/myfsio/myfsio.env`, and can register a `myfsio.service` unit. + ## Testing +Run the Rust test suite from the workspace: + ```bash -# Run all tests -pytest tests/ -v - -# Run specific test file -pytest tests/test_api.py -v - -# Run with coverage -pytest tests/ --cov=app --cov-report=html +cd rust/myfsio-engine +cargo test ``` -## References +## Health Check -- [Amazon S3 Documentation](https://docs.aws.amazon.com/s3/) -- [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) -- [S3 Bucket Policy Examples](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html) +`GET /myfsio/health` returns: + +```json +{ + "status": "ok", + "version": "0.5.0" +} +``` + +The `version` field comes from the Rust crate version in `rust/myfsio-engine/crates/myfsio-server/Cargo.toml`. diff --git a/docs.md b/docs.md index ba36f68..b2438b7 100644 --- a/docs.md +++ b/docs.md @@ -1,2605 +1,419 @@ -# MyFSIO Documentation +# MyFSIO Rust Operations Guide -This document expands on the README to describe the full workflow for running, configuring, and extending MyFSIO. Use it as a playbook for local S3-style experimentation. +This document describes the current Rust server in `rust/myfsio-engine`. It replaces the older Python-oriented runbook. -## 1. System Overview +## 1. What Changed -MyFSIO ships two Flask entrypoints that share the same storage, IAM, and bucket-policy state: +The active runtime is now Rust: -- **API server** – Implements the S3-compatible REST API, policy evaluation, and Signature Version 4 presign service. -- **UI server** – Provides the browser console for buckets, IAM, and policies. It proxies all storage operations through the S3 API via boto3 (SigV4-signed), mirroring the architecture used by MinIO and Garage. +- One process serves both the S3 API and the web UI. +- The server entrypoint is `myfsio-server`. +- The main development workflow is `cargo run -p myfsio-server --`. +- API-only mode is controlled with `UI_ENABLED=false`. -Both servers read `AppConfig`, so editing JSON stores on disk instantly affects both surfaces. +The `python/` directory may still contain older implementation code, templates, and tests, but it is not required to run the current server. -## 2. Quickstart +## 2. Quick Start + +From the repository root: ```bash -python -m venv .venv -. .venv/Scripts/activate # PowerShell: .\.venv\Scripts\Activate.ps1 -pip install -r requirements.txt - -# Run both API and UI -python run.py +cd rust/myfsio-engine +cargo run -p myfsio-server -- ``` -Visit `http://127.0.0.1:5100/ui` to use the console and `http://127.0.0.1:5000/` (with IAM headers) for raw API calls. +Default endpoints: -### Run modes +- UI: `http://127.0.0.1:5100/ui` +- API: `http://127.0.0.1:5000/` +- Health: `http://127.0.0.1:5000/myfsio/health` -You can run services individually if needed: +On first startup, MyFSIO bootstraps an admin user in `data/.myfsio.sys/config/iam.json` and prints the generated access key and secret key to stdout. -```bash -python run.py --mode api # API only (port 5000) -python run.py --mode ui # UI only (port 5100) -``` +### Windows -### Configuration validation - -Validate your configuration before deploying: - -```bash -# Show configuration summary -python run.py --show-config -./myfsio --show-config - -# Validate and check for issues (exits with code 1 if critical issues found) -python run.py --check-config -./myfsio --check-config -``` - -### Linux Installation (Recommended for Production) - -For production deployments on Linux, use the provided installation script: - -```bash -# Download the binary and install script -# Then run the installer with sudo: -sudo ./scripts/install.sh --binary ./myfsio - -# Or with custom paths: -sudo ./scripts/install.sh \ - --binary ./myfsio \ - --install-dir /opt/myfsio \ - --data-dir /mnt/storage/myfsio \ - --log-dir /var/log/myfsio \ - --api-url https://s3.example.com \ - --user myfsio - -# Non-interactive mode (for automation): -sudo ./scripts/install.sh --binary ./myfsio -y -``` - -The installer will: -1. Create a dedicated system user -2. Set up directories with proper permissions -3. Generate a secure `SECRET_KEY` -4. Create an environment file at `/opt/myfsio/myfsio.env` -5. Install and configure a systemd service - -After installation: -```bash -sudo systemctl start myfsio # Start the service -sudo systemctl enable myfsio # Enable on boot -sudo systemctl status myfsio # Check status -sudo journalctl -u myfsio -f # View logs -``` - -To uninstall: -```bash -sudo ./scripts/uninstall.sh # Full removal -sudo ./scripts/uninstall.sh --keep-data # Keep data directory -``` - -### Docker quickstart - -The repo now ships a `Dockerfile` so you can run both services in one container: - -```bash -docker build -t myfsio . -docker run --rm -p 5000:5000 -p 5100:5100 \ - -v "$PWD/data:/app/data" \ - -v "$PWD/logs:/app/logs" \ - -e SECRET_KEY="change-me" \ - --name myfsio myfsio -``` - -PowerShell (Windows) example: +From PowerShell: ```powershell -docker run --rm -p 5000:5000 -p 5100:5100 ` - -v ${PWD}\data:/app/data ` - -v ${PWD}\logs:/app/logs ` - -e SECRET_KEY="change-me" ` - --name myfsio myfsio +cd rust\myfsio-engine +cargo run -p myfsio-server -- ``` -Key mount points: -- `/app/data` → persists buckets directly under `/app/data/` while system metadata (IAM config, bucket policies, versions, multipart uploads, etc.) lives under `/app/data/.myfsio.sys` (for example, `/app/data/.myfsio.sys/config/iam.json`). -- `/app/logs` → captures the rotating app log. -- `/app/tmp-storage` (optional) if you rely on the demo upload staging folders. - -With these volumes attached you can rebuild/restart the container without losing stored objects or credentials. - -### Versioning - -The repo now tracks a human-friendly release string inside `app/version.py` (see the `APP_VERSION` constant). Edit that value whenever you cut a release. The constant flows into Flask as `APP_VERSION` and is exposed via `GET /myfsio/health`, so you can monitor deployments or surface it in UIs. - -## 3. Configuration Reference - -All configuration is done via environment variables. The table below lists every supported variable. - -### Core Settings - -| Variable | Default | Notes | -| --- | --- | --- | -| `STORAGE_ROOT` | `/data` | Filesystem home for all buckets/objects. | -| `MAX_UPLOAD_SIZE` | `1073741824` (1 GiB) | Bytes. Caps incoming uploads in both API + UI. | -| `UI_PAGE_SIZE` | `100` | `MaxKeys` hint shown in listings. | -| `SECRET_KEY` | Auto-generated | Flask session key. Auto-generates and persists if not set. **Set explicitly in production.** | -| `API_BASE_URL` | `http://127.0.0.1:5000` | Internal S3 API URL used by the web UI proxy. Also used for presigned URL generation. Set to your public URL if running behind a reverse proxy. | -| `AWS_REGION` | `us-east-1` | Region embedded in SigV4 credential scope. | -| `AWS_SERVICE` | `s3` | Service string for SigV4. | -| `DISPLAY_TIMEZONE` | `UTC` | Timezone for timestamps in the web UI (e.g., `US/Eastern`, `Asia/Tokyo`). | - -### IAM & Security - -| Variable | Default | Notes | -| --- | --- | --- | -| `IAM_CONFIG` | `data/.myfsio.sys/config/iam.json` | Stores users, secrets, and inline policies. Encrypted at rest when `SECRET_KEY` is set. | -| `BUCKET_POLICY_PATH` | `data/.myfsio.sys/config/bucket_policies.json` | Bucket policy store (auto hot-reload). | -| `AUTH_MAX_ATTEMPTS` | `5` | Failed login attempts before lockout. | -| `AUTH_LOCKOUT_MINUTES` | `15` | Lockout duration after max failed attempts. | -| `SESSION_LIFETIME_DAYS` | `30` | How long UI sessions remain valid. | -| `SECRET_TTL_SECONDS` | `300` | TTL for ephemeral secrets (presigned URLs). | -| `UI_ENFORCE_BUCKET_POLICIES` | `false` | Whether the UI should enforce bucket policies. | -| `ADMIN_ACCESS_KEY` | (none) | Custom access key for the admin user on first run or credential reset. If unset, a random key is generated. | -| `ADMIN_SECRET_KEY` | (none) | Custom secret key for the admin user on first run or credential reset. If unset, a random key is generated. | - -### CORS (Cross-Origin Resource Sharing) - -| Variable | Default | Notes | -| --- | --- | --- | -| `CORS_ORIGINS` | `*` | Comma-separated allowed origins. Use specific domains in production. | -| `CORS_METHODS` | `GET,PUT,POST,DELETE,OPTIONS,HEAD` | Allowed HTTP methods. | -| `CORS_ALLOW_HEADERS` | `*` | Allowed request headers. | -| `CORS_EXPOSE_HEADERS` | `*` | Response headers visible to browsers (e.g., `ETag`). | - -### Rate Limiting - -| Variable | Default | Notes | -| --- | --- | --- | -| `RATE_LIMIT_DEFAULT` | `200 per minute` | Default rate limit for API endpoints. | -| `RATE_LIMIT_LIST_BUCKETS` | `60 per minute` | Rate limit for listing buckets (`GET /`). | -| `RATE_LIMIT_BUCKET_OPS` | `120 per minute` | Rate limit for bucket operations (PUT/DELETE/GET/POST on `/`). | -| `RATE_LIMIT_OBJECT_OPS` | `240 per minute` | Rate limit for object operations (PUT/GET/DELETE/POST on `//`). | -| `RATE_LIMIT_HEAD_OPS` | `100 per minute` | Rate limit for HEAD requests (bucket and object). | -| `RATE_LIMIT_ADMIN` | `60 per minute` | Rate limit for admin API endpoints (`/admin/*`). | -| `RATE_LIMIT_STORAGE_URI` | `memory://` | Storage backend for rate limits. Use `redis://host:port` for distributed setups. | - -### Server Configuration - -| Variable | Default | Notes | -| --- | --- | --- | -| `SERVER_THREADS` | `0` (auto) | Granian blocking threads (1-64). Set to `0` for auto-calculation based on CPU cores (×2). | -| `SERVER_CONNECTION_LIMIT` | `0` (auto) | Maximum concurrent requests per worker (10-1000). Set to `0` for auto-calculation based on available RAM. | -| `SERVER_BACKLOG` | `0` (auto) | TCP listen backlog (128-4096). Set to `0` for auto-calculation (connection_limit × 2). | -| `SERVER_CHANNEL_TIMEOUT` | `120` | Seconds before idle connections are closed (10-300). | - -### Logging - -| Variable | Default | Notes | -| --- | --- | --- | -| `LOG_LEVEL` | `INFO` | Log verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR`. | -| `LOG_TO_FILE` | `true` | Enable file logging. | -| `LOG_DIR` | `/logs` | Directory for log files. | -| `LOG_FILE` | `app.log` | Log filename. | -| `LOG_MAX_BYTES` | `5242880` (5 MB) | Max log file size before rotation. | -| `LOG_BACKUP_COUNT` | `3` | Number of rotated log files to keep. | - -### Encryption - -| Variable | Default | Notes | -| --- | --- | --- | -| `ENCRYPTION_ENABLED` | `false` | Enable server-side encryption support. | -| `ENCRYPTION_MASTER_KEY_PATH` | `data/.myfsio.sys/keys/master.key` | Path to the master encryption key file. | -| `DEFAULT_ENCRYPTION_ALGORITHM` | `AES256` | Default algorithm for new encrypted objects. | -| `KMS_ENABLED` | `false` | Enable KMS key management for encryption. | -| `KMS_KEYS_PATH` | `data/.myfsio.sys/keys/kms_keys.json` | Path to store KMS key metadata. | - - -## Lifecycle Rules - -Lifecycle rules automate object management by scheduling deletions based on object age. - -### Enabling Lifecycle Enforcement - -By default, lifecycle enforcement is disabled. Enable it by setting the environment variable: +### API-only mode ```bash -LIFECYCLE_ENABLED=true python run.py +UI_ENABLED=false cargo run -p myfsio-server -- ``` -Or in your `myfsio.env` file: -``` -LIFECYCLE_ENABLED=true -LIFECYCLE_INTERVAL_SECONDS=3600 # Check interval (default: 1 hour) -``` +There is no separate UI-only mode in the Rust server. -### Configuring Rules - -Once enabled, configure lifecycle rules via: -- **Web UI:** Bucket Details → Lifecycle tab → Add Rule -- **S3 API:** `PUT /?lifecycle` with XML configuration - -### Available Actions - -| Action | Description | -|--------|-------------| -| **Expiration** | Delete current version objects after N days | -| **NoncurrentVersionExpiration** | Delete old versions N days after becoming noncurrent (requires versioning) | -| **AbortIncompleteMultipartUpload** | Clean up incomplete multipart uploads after N days | - -### Example Configuration (XML) - -```xml - - - DeleteOldLogs - Enabled - logs/ - 30 - - -``` - -## Garbage Collection - -The garbage collector (GC) automatically cleans up orphaned data that accumulates over time: stale temporary files from failed uploads, abandoned multipart uploads, stale lock files, orphaned metadata entries, orphaned version files, and empty directories. - -### Enabling GC - -By default, GC is disabled. Enable it by setting: +## 3. Build and Run a Binary ```bash -GC_ENABLED=true python run.py +cd rust/myfsio-engine +cargo build --release -p myfsio-server ``` -Or in your `myfsio.env` file: -``` -GC_ENABLED=true -GC_INTERVAL_HOURS=6 # Run every 6 hours (default) -GC_TEMP_FILE_MAX_AGE_HOURS=24 # Delete temp files older than 24h -GC_MULTIPART_MAX_AGE_DAYS=7 # Delete orphaned multipart uploads older than 7 days -GC_LOCK_FILE_MAX_AGE_HOURS=1 # Delete stale lock files older than 1h -GC_DRY_RUN=false # Set to true to log without deleting -``` - -### What Gets Cleaned - -| Type | Location | Condition | -|------|----------|-----------| -| **Temp files** | `.myfsio.sys/tmp/` | Older than `GC_TEMP_FILE_MAX_AGE_HOURS` | -| **Orphaned multipart uploads** | `.myfsio.sys/multipart/` and `/.multipart/` | Older than `GC_MULTIPART_MAX_AGE_DAYS` | -| **Stale lock files** | `.myfsio.sys/buckets//locks/` | Older than `GC_LOCK_FILE_MAX_AGE_HOURS` | -| **Orphaned metadata** | `.myfsio.sys/buckets//meta/` and `/.meta/` | Object file no longer exists | -| **Orphaned versions** | `.myfsio.sys/buckets//versions/` and `/.versions/` | Main object no longer exists | -| **Empty directories** | Various internal directories | Directory is empty after cleanup | - -### Admin API - -All GC endpoints require admin (`iam:*`) permissions. - -| Method | Route | Description | -|--------|-------|-------------| -| `GET` | `/admin/gc/status` | Get GC status and configuration | -| `POST` | `/admin/gc/run` | Trigger a manual GC run (body: `{"dry_run": true}` for preview) | -| `GET` | `/admin/gc/history` | Get GC execution history (query: `?limit=50&offset=0`) | - -### Dry Run Mode - -Set `GC_DRY_RUN=true` to log what would be deleted without actually removing anything. You can also trigger a one-time dry run via the admin API: +Run it directly: ```bash -curl -X POST "http://localhost:5000/admin/gc/run" \ - -H "X-Access-Key: " -H "X-Secret-Key: " \ - -H "Content-Type: application/json" \ - -d '{"dry_run": true}' +./target/release/myfsio-server ``` -### Performance Tuning +On Windows: -| Variable | Default | Notes | -| --- | --- | --- | -| `STREAM_CHUNK_SIZE` | `65536` (64 KB) | Chunk size for streaming large files. | -| `MULTIPART_MIN_PART_SIZE` | `5242880` (5 MB) | Minimum part size for multipart uploads. | -| `BUCKET_STATS_CACHE_TTL` | `60` | Seconds to cache bucket statistics. | -| `BULK_DELETE_MAX_KEYS` | `500` | Maximum keys per bulk delete request. | -| `BULK_DOWNLOAD_MAX_BYTES` | `1073741824` (1 GiB) | Maximum total size for bulk ZIP downloads. | -| `OBJECT_CACHE_TTL` | `60` | Seconds to cache object metadata. | +```powershell +.\target\release\myfsio-server.exe +``` -#### Gzip Compression +## 4. CLI Commands -API responses for JSON, XML, HTML, CSS, and JavaScript are automatically gzip-compressed when the client sends `Accept-Encoding: gzip`. Compression activates for responses larger than 500 bytes and is handled by a WSGI middleware (`app/compression.py`). Binary object downloads and streaming responses are never compressed. No configuration is needed. - -### Server Settings - -| Variable | Default | Notes | -| --- | --- | --- | -| `APP_HOST` | `0.0.0.0` | Network interface to bind to. | -| `APP_PORT` | `5000` | API server port (UI uses 5100). | -| `FLASK_DEBUG` | `0` | Enable Flask debug mode. **Never enable in production.** | - -### Production Checklist - -Before deploying to production, ensure you: - -1. **Set `SECRET_KEY`** - Use a strong, unique value (e.g., `openssl rand -base64 32`). This also enables IAM config encryption at rest. -2. **Restrict CORS** - Set `CORS_ORIGINS` to your specific domains instead of `*` -3. **Configure `API_BASE_URL`** - Required for correct presigned URLs behind proxies -4. **Enable HTTPS** - Use a reverse proxy (nginx, Cloudflare) with TLS termination -5. **Review rate limits** - Adjust `RATE_LIMIT_DEFAULT` based on your needs -6. **Secure master keys** - Back up `ENCRYPTION_MASTER_KEY_PATH` if using encryption -7. **Use `--prod` flag** - Runs with Granian instead of Flask dev server -8. **Set credential expiry** - Assign `expires_at` to non-admin users for time-limited access - -### Proxy Configuration - -If running behind a reverse proxy (e.g., Nginx, Cloudflare, or a tunnel), ensure the proxy sets the standard forwarding headers: -- `X-Forwarded-Host` -- `X-Forwarded-Proto` - -The application automatically trusts these headers to generate correct presigned URLs (e.g., `https://s3.example.com/...` instead of `http://127.0.0.1:5000/...`). Alternatively, you can explicitly set `API_BASE_URL` to your public endpoint. - -| Variable | Default | Notes | -| --- | --- | --- | -| `NUM_TRUSTED_PROXIES` | `1` | Number of trusted reverse proxies for `X-Forwarded-*` header processing. | -| `ALLOWED_REDIRECT_HOSTS` | `""` | Comma-separated whitelist of safe redirect targets. Empty allows only same-host redirects. | -| `ALLOW_INTERNAL_ENDPOINTS` | `false` | Allow connections to internal/private IPs for webhooks and replication targets. **Keep disabled in production unless needed.** | - -## Integrity Scanner - -The integrity scanner detects and optionally auto-repairs data inconsistencies: corrupted objects (ETag mismatch), orphaned files without metadata, phantom metadata without files, stale version archives, ETag cache drift, and unmigrated legacy `.meta.json` files. - -### Enabling Integrity Scanner - -By default, the integrity scanner is disabled. Enable it by setting: +The Rust CLI supports these operational commands: ```bash -INTEGRITY_ENABLED=true python run.py +# Start serving (default command) +cargo run -p myfsio-server -- + +# Print version +cargo run -p myfsio-server -- version + +# Show resolved configuration +cargo run -p myfsio-server -- --show-config + +# Validate configuration and exit with code 1 on critical issues +cargo run -p myfsio-server -- --check-config + +# Back up the current IAM file and generate fresh admin credentials +cargo run -p myfsio-server -- --reset-cred ``` -Or in your `myfsio.env` file: -``` -INTEGRITY_ENABLED=true -INTEGRITY_INTERVAL_HOURS=24 # Run every 24 hours (default) -INTEGRITY_BATCH_SIZE=1000 # Max objects to scan per cycle -INTEGRITY_AUTO_HEAL=false # Automatically repair detected issues -INTEGRITY_DRY_RUN=false # Set to true to log without healing -``` +If you are running a release build instead of `cargo run`, replace the `cargo run ... --` prefix with the binary path. -### What Gets Checked +## 5. Environment Files -| Check | Detection | Heal Action | -|-------|-----------|-------------| -| **Corrupted objects** | File MD5 does not match stored `__etag__` | Update `__etag__` in index (disk data is authoritative) | -| **Orphaned objects** | File exists on disk without metadata entry | Create index entry with computed MD5/size/mtime | -| **Phantom metadata** | Index entry exists but file is missing from disk | Remove stale entry from `_index.json` | -| **Stale versions** | `.json` manifest without `.bin` data or vice versa | Remove orphaned version file | -| **ETag cache inconsistency** | `etag_index.json` entry differs from metadata `__etag__` | Delete `etag_index.json` (auto-rebuilt on next list) | -| **Legacy metadata drift** | Legacy `.meta.json` differs from index or is unmigrated | Migrate to index and delete legacy file | +At startup, the server tries to load environment files from these locations when they exist: -### Admin API +1. `/opt/myfsio/myfsio.env` +2. `.env` in the current directory +3. `myfsio.env` in the current directory +4. `.env` and `myfsio.env` in a few parent directories -All integrity endpoints require admin (`iam:*`) permissions. +That makes local development and systemd installs behave consistently. -| Method | Route | Description | -|--------|-------|-------------| -| `GET` | `/admin/integrity/status` | Get scanner status and configuration | -| `POST` | `/admin/integrity/run` | Trigger a manual scan (body: `{"dry_run": true, "auto_heal": true}`) | -| `GET` | `/admin/integrity/history` | Get scan history (query: `?limit=50&offset=0`) | +## 6. Verified Configuration Reference -### Dry Run Mode +These values are taken from `rust/myfsio-engine/crates/myfsio-server/src/config.rs`. -Set `INTEGRITY_DRY_RUN=true` to log detected issues without making any changes. You can also trigger a one-time dry run via the admin API: - -```bash -curl -X POST "http://localhost:5000/admin/integrity/run" \ - -H "X-Access-Key: " -H "X-Secret-Key: " \ - -H "Content-Type: application/json" \ - -d '{"dry_run": true, "auto_heal": true}' -``` - -### Configuration Reference +### Network and runtime | Variable | Default | Description | -|----------|---------|-------------| -| `INTEGRITY_ENABLED` | `false` | Enable background integrity scanning | -| `INTEGRITY_INTERVAL_HOURS` | `24` | Hours between scan cycles | -| `INTEGRITY_BATCH_SIZE` | `1000` | Max objects to scan per cycle | -| `INTEGRITY_AUTO_HEAL` | `false` | Automatically repair detected issues | -| `INTEGRITY_DRY_RUN` | `false` | Log issues without healing | - -## 4. Upgrading and Updates - -### Version Checking - -The application version is tracked in `app/version.py` and exposed via: -- **Health endpoint:** `GET /myfsio/health` returns JSON with `version` field -- **Metrics dashboard:** Navigate to `/ui/metrics` to see the running version in the System Status card - -To check your current version: - -```bash -# API health endpoint -curl http://localhost:5000/myfsio/health - -# Or inspect version.py directly -cat app/version.py | grep APP_VERSION -``` - -### Pre-Update Backup Procedures - -**Always backup before upgrading to prevent data loss:** - -```bash -# 1. Stop the application -# Ctrl+C if running in terminal, or: -docker stop myfsio # if using Docker - -# 2. Backup configuration files (CRITICAL) -mkdir -p backups/$(date +%Y%m%d_%H%M%S) -cp -r data/.myfsio.sys/config backups/$(date +%Y%m%d_%H%M%S)/ - -# 3. Backup all data (optional but recommended) -tar -czf backups/data_$(date +%Y%m%d_%H%M%S).tar.gz data/ - -# 4. Backup logs for audit trail -cp -r logs backups/$(date +%Y%m%d_%H%M%S)/ -``` - -**Windows PowerShell:** - -```powershell -# Create timestamped backup -$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -New-Item -ItemType Directory -Path "backups\$timestamp" -Force - -# Backup configs -Copy-Item -Recurse "data\.myfsio.sys\config" "backups\$timestamp\" - -# Backup entire data directory -Compress-Archive -Path "data\" -DestinationPath "backups\data_$timestamp.zip" -``` - -**Critical files to backup:** -- `data/.myfsio.sys/config/iam.json` – User accounts and access keys -- `data/.myfsio.sys/config/bucket_policies.json` – Bucket access policies -- `data/.myfsio.sys/config/kms_keys.json` – Encryption keys (if using KMS) -- `data/.myfsio.sys/config/secret_store.json` – Application secrets - -### Update Procedures - -#### Source Installation Updates - -```bash -# 1. Backup (see above) -# 2. Pull latest code -git fetch origin -git checkout main # or your target branch/tag -git pull - -# 3. Check for dependency changes -pip install -r requirements.txt - -# 4. Review CHANGELOG/release notes for breaking changes -cat CHANGELOG.md # if available - -# 5. Run migration scripts (if any) -# python scripts/migrate_vX_to_vY.py # example - -# 6. Restart application -python run.py -``` - -#### Docker Updates - -```bash -# 1. Backup (see above) -# 2. Pull/rebuild image -docker pull yourregistry/myfsio:latest -# OR rebuild from source: -docker build -t myfsio:latest . - -# 3. Stop and remove old container -docker stop myfsio -docker rm myfsio - -# 4. Start new container with same volumes -docker run -d \ - --name myfsio \ - -p 5000:5000 -p 5100:5100 \ - -v "$(pwd)/data:/app/data" \ - -v "$(pwd)/logs:/app/logs" \ - -e SECRET_KEY="your-secret" \ - myfsio:latest - -# 5. Verify health -curl http://localhost:5000/myfsio/health -``` - -### Version Compatibility Checks - -Before upgrading across major versions, verify compatibility: - -| From Version | To Version | Breaking Changes | Migration Required | -|--------------|------------|------------------|-------------------| -| 0.1.x | 0.2.x | None expected | No | -| 0.1.6 | 0.1.7 | None | No | -| < 0.1.0 | >= 0.1.0 | New IAM config format | Yes - run migration script | - -**Automatic compatibility detection:** - -The application will log warnings on startup if config files need migration: - -``` -WARNING: IAM config format is outdated (v1). Please run: python scripts/migrate_iam.py -``` - -**Manual compatibility check:** - -```bash -# Compare version schemas -python -c "from app.version import APP_VERSION; print(f'Running: {APP_VERSION}')" -python scripts/check_compatibility.py data/.myfsio.sys/config/ -``` - -### Migration Steps for Breaking Changes - -When release notes indicate breaking changes, follow these steps: - -#### Config Format Migrations - -```bash -# 1. Backup first (critical!) -cp data/.myfsio.sys/config/iam.json data/.myfsio.sys/config/iam.json.backup - -# 2. Run provided migration script -python scripts/migrate_iam_v1_to_v2.py - -# 3. Validate migration -python scripts/validate_config.py - -# 4. Test with read-only mode first (if available) -# python run.py --read-only - -# 5. Restart normally -python run.py -``` - -#### Database/Storage Schema Changes - -If object metadata format changes: - -```bash -# 1. Run storage migration script -python scripts/migrate_storage.py --dry-run # preview changes - -# 2. Apply migration -python scripts/migrate_storage.py --apply - -# 3. Verify integrity -python scripts/verify_storage.py -``` - -#### IAM Policy Updates - -If IAM action names change (e.g., `s3:Get` → `s3:GetObject`): - -```bash -# Migration script will update all policies -python scripts/migrate_policies.py \ - --input data/.myfsio.sys/config/iam.json \ - --backup data/.myfsio.sys/config/iam.json.v1 - -# Review changes before committing -python scripts/diff_policies.py \ - data/.myfsio.sys/config/iam.json.v1 \ - data/.myfsio.sys/config/iam.json -``` - -### Rollback Procedures - -If an update causes issues, rollback to the previous version: - -#### Quick Rollback (Source) - -```bash -# 1. Stop application -# Ctrl+C or kill process - -# 2. Revert code -git checkout -# OR -git reset --hard HEAD~1 - -# 3. Restore configs from backup -cp backups/20241213_103000/config/* data/.myfsio.sys/config/ - -# 4. Downgrade dependencies if needed -pip install -r requirements.txt - -# 5. Restart -python run.py -``` - -#### Docker Rollback - -```bash -# 1. Stop current container -docker stop myfsio -docker rm myfsio - -# 2. Start previous version -docker run -d \ - --name myfsio \ - -p 5000:5000 -p 5100:5100 \ - -v "$(pwd)/data:/app/data" \ - -v "$(pwd)/logs:/app/logs" \ - -e SECRET_KEY="your-secret" \ - myfsio:0.1.3 # specify previous version tag - -# 3. Verify -curl http://localhost:5000/myfsio/health -``` - -#### Emergency Config Restore - -If only config is corrupted but code is fine: - -```bash -# Stop app -# Restore from latest backup -cp backups/20241213_103000/config/iam.json data/.myfsio.sys/config/ -cp backups/20241213_103000/config/bucket_policies.json data/.myfsio.sys/config/ - -# Restart app -python run.py -``` - -### Blue-Green Deployment (Zero Downtime) - -For production environments requiring zero downtime: - -```bash -# 1. Run new version on different port (e.g., 5001/5101) -APP_PORT=5001 UI_PORT=5101 python run.py & - -# 2. Health check new instance -curl http://localhost:5001/myfsio/health - -# 3. Update load balancer to route to new ports - -# 4. Monitor for issues - -# 5. Gracefully stop old instance -kill -SIGTERM -``` - -### Post-Update Verification - -After any update, verify functionality: - -```bash -# 1. Health check -curl http://localhost:5000/myfsio/health - -# 2. Login to UI -open http://localhost:5100/ui - -# 3. Test IAM authentication -curl -H "X-Amz-Security-Token: :" \ - http://localhost:5000/ - -# 4. Test presigned URL generation -# Via UI or API - -# 5. Check logs for errors -tail -n 100 logs/myfsio.log -``` - -### Automated Update Scripts - -Create a custom update script for your environment: - -```bash -#!/bin/bash -# update.sh - Automated update with rollback capability - -set -e # Exit on error - -VERSION_NEW="$1" -BACKUP_DIR="backups/$(date +%Y%m%d_%H%M%S)" - -echo "Creating backup..." -mkdir -p "$BACKUP_DIR" -cp -r data/.myfsio.sys/config "$BACKUP_DIR/" - -echo "Updating to version $VERSION_NEW..." -git fetch origin -git checkout "v$VERSION_NEW" -pip install -r requirements.txt - -echo "Starting application..." -python run.py & -APP_PID=$! - -# Wait and health check -sleep 5 -if curl -f http://localhost:5000/myfsio/health; then - echo "Update successful!" -else - echo "Health check failed, rolling back..." - kill $APP_PID - git checkout - - cp -r "$BACKUP_DIR/config/*" data/.myfsio.sys/config/ - python run.py & - exit 1 -fi -``` - -## 4. Authentication & IAM - -MyFSIO implements a comprehensive Identity and Access Management (IAM) system that controls who can access your buckets and what operations they can perform. The system supports both simple action-based permissions and AWS-compatible policy syntax. - -### Getting Started - -1. On first boot, `data/.myfsio.sys/config/iam.json` is created with a randomly generated admin user. The access key and secret key are printed to the console during first startup. You can set `ADMIN_ACCESS_KEY` and `ADMIN_SECRET_KEY` environment variables to use custom credentials instead of random ones. If `SECRET_KEY` is configured, the IAM config file is encrypted at rest using AES (Fernet). To reset admin credentials later, run `python run.py --reset-cred`. -2. Sign into the UI using the generated credentials, then open **IAM**: - - **Create user**: supply a display name, optional JSON inline policy array, and optional credential expiry date. - - **Set expiry**: assign an expiration date to any user's credentials. Expired credentials are rejected at authentication time. The UI shows expiry badges and preset durations (1h, 24h, 7d, 30d, 90d). - - **Rotate secret**: generates a new secret key; the UI surfaces it once. - - **Policy editor**: select a user, paste an array of objects (`{"bucket": "*", "actions": ["list", "read"]}`), and submit. An optional `"prefix"` field restricts object-level actions to a key prefix (e.g., `"uploads/"`). Alias support includes AWS-style verbs (e.g., `s3:GetObject`). -3. Wildcard action `iam:*` is supported for admin user definitions. - -> **Breaking Change (v0.2.0+):** Previous versions used fixed default credentials (`localadmin/localadmin`). If upgrading from an older version, your existing credentials remain unchanged, but new installations will generate random credentials. - -### Authentication - -The API expects every request to include authentication headers. The UI persists them in the Flask session after login. - -| Header | Description | +| --- | --- | --- | +| `HOST` | `127.0.0.1` | Bind address for both listeners | +| `PORT` | `5000` | S3 API port | +| `UI_PORT` | `5100` | Web UI port | +| `UI_ENABLED` | `true` | Disable to run API-only | +| `API_BASE_URL` | unset | Public-facing API base used by the UI and presigned URL generation | +| `TEMPLATES_DIR` | built-in templates dir | Optional override for UI templates | +| `STATIC_DIR` | built-in static dir | Optional override for static assets | + +### Storage and auth + +| Variable | Default | Description | +| --- | --- | --- | +| `STORAGE_ROOT` | `./data` | Root for buckets and internal state | +| `IAM_CONFIG` | `/.myfsio.sys/config/iam.json` | IAM config path | +| `AWS_REGION` | `us-east-1` | SigV4 region | +| `SIGV4_TIMESTAMP_TOLERANCE_SECONDS` | `900` | Allowed request time skew | +| `PRESIGNED_URL_MIN_EXPIRY_SECONDS` | `1` | Minimum presigned URL lifetime | +| `PRESIGNED_URL_MAX_EXPIRY_SECONDS` | `604800` | Maximum presigned URL lifetime | +| `SECRET_KEY` | unset, then fallback to `.myfsio.sys/config/.secret` if present | Session signing and IAM config encryption key | +| `ADMIN_ACCESS_KEY` | unset | Optional deterministic first-run/reset access key | +| `ADMIN_SECRET_KEY` | unset | Optional deterministic first-run/reset secret key | + +### Feature toggles + +| Variable | Default | Description | +| --- | --- | --- | +| `ENCRYPTION_ENABLED` | `false` | Enable object encryption support | +| `KMS_ENABLED` | `false` | Enable built-in KMS support | +| `GC_ENABLED` | `false` | Start the garbage collector worker | +| `INTEGRITY_ENABLED` | `false` | Start the integrity worker | +| `LIFECYCLE_ENABLED` | `false` | Start the lifecycle worker | +| `METRICS_HISTORY_ENABLED` | `false` | Persist system metrics snapshots | +| `OPERATION_METRICS_ENABLED` | `false` | Persist API operation metrics | +| `WEBSITE_HOSTING_ENABLED` | `false` | Enable website domain and hosting features | +| `SITE_SYNC_ENABLED` | `false` | Start the site sync worker | + +### Metrics tuning + +| Variable | Default | Description | +| --- | --- | --- | +| `OPERATION_METRICS_INTERVAL_MINUTES` | `5` | Snapshot interval for operation metrics | +| `OPERATION_METRICS_RETENTION_HOURS` | `24` | Retention window for operation metrics | +| `METRICS_HISTORY_INTERVAL_MINUTES` | `5` | Snapshot interval for system metrics | +| `METRICS_HISTORY_RETENTION_HOURS` | `24` | Retention window for system metrics | + +### Replication and site sync + +| Variable | Default | Description | +| --- | --- | --- | +| `REPLICATION_CONNECT_TIMEOUT_SECONDS` | `5` | Replication connect timeout | +| `REPLICATION_READ_TIMEOUT_SECONDS` | `30` | Replication read timeout | +| `REPLICATION_MAX_RETRIES` | `2` | Replication retry count | +| `REPLICATION_STREAMING_THRESHOLD_BYTES` | `10485760` | Switch to streaming for large copies | +| `REPLICATION_MAX_FAILURES_PER_BUCKET` | `50` | Failure budget before a bucket is skipped | +| `SITE_SYNC_INTERVAL_SECONDS` | `60` | Poll interval for the site sync worker | +| `SITE_SYNC_BATCH_SIZE` | `100` | Max objects processed per site sync batch | +| `SITE_SYNC_CONNECT_TIMEOUT_SECONDS` | `10` | Site sync connect timeout | +| `SITE_SYNC_READ_TIMEOUT_SECONDS` | `120` | Site sync read timeout | +| `SITE_SYNC_MAX_RETRIES` | `2` | Site sync retry count | +| `SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS` | `1.0` | Allowed skew between peers | + +### Site identity values used by the UI + +These are read directly by UI pages: + +| Variable | Description | | --- | --- | -| `X-Access-Key` | The user's access key identifier | -| `X-Secret-Key` | The user's secret key for signing | +| `SITE_ID` | Local site identifier shown in the UI | +| `SITE_ENDPOINT` | Public endpoint for this site | +| `SITE_REGION` | Display region for the local site | -**Security Features:** -- **Lockout Protection**: After `AUTH_MAX_ATTEMPTS` (default: 5) failed login attempts, the account is locked for `AUTH_LOCKOUT_MINUTES` (default: 15 minutes). -- **Credential Expiry**: Each user can have an optional `expires_at` timestamp (ISO 8601). Once expired, all API requests using those credentials are rejected. Set or clear expiry via the UI or API. -- **IAM Config Encryption**: When `SECRET_KEY` is set, the IAM config file (`iam.json`) is encrypted at rest using Fernet (AES-256-CBC with HMAC). Existing plaintext configs are automatically encrypted on next load. -- **Session Management**: UI sessions remain valid for `SESSION_LIFETIME_DAYS` (default: 30 days). -- **Hot Reload**: IAM configuration changes take effect immediately without restart. -- **Credential Reset**: Run `python run.py --reset-cred` to reset admin credentials. Supports `ADMIN_ACCESS_KEY` and `ADMIN_SECRET_KEY` env vars for deterministic keys. +## 7. Data Layout -### Permission Model +With the default `STORAGE_ROOT=./data`, the Rust server writes: -MyFSIO uses a two-layer permission model: - -1. **IAM User Policies** – Define what a user can do across the system (stored in `iam.json`) -2. **Bucket Policies** – Define who can access a specific bucket (stored in `bucket_policies.json`) - -Both layers are evaluated for each request. A user must have permission in their IAM policy AND the bucket policy must allow the action (or have no explicit deny). - -### Available IAM Actions - -#### S3 Actions (Bucket/Object Operations) - -| Action | Description | AWS Aliases | -| --- | --- | --- | -| `list` | List buckets and objects | `s3:ListBucket`, `s3:ListAllMyBuckets`, `s3:ListBucketVersions`, `s3:ListMultipartUploads`, `s3:ListParts` | -| `read` | Download objects, get metadata | `s3:GetObject`, `s3:GetObjectVersion`, `s3:GetObjectTagging`, `s3:GetObjectVersionTagging`, `s3:GetObjectAcl`, `s3:GetBucketVersioning`, `s3:HeadObject`, `s3:HeadBucket` | -| `write` | Upload objects, manage object tags | `s3:PutObject`, `s3:PutObjectTagging`, `s3:CreateMultipartUpload`, `s3:UploadPart`, `s3:CompleteMultipartUpload`, `s3:AbortMultipartUpload`, `s3:CopyObject` | -| `delete` | Remove objects and versions | `s3:DeleteObject`, `s3:DeleteObjectVersion`, `s3:DeleteObjectTagging` | -| `create_bucket` | Create new buckets | `s3:CreateBucket` | -| `delete_bucket` | Delete buckets | `s3:DeleteBucket` | -| `share` | Manage Access Control Lists (ACLs) | `s3:PutObjectAcl`, `s3:PutBucketAcl`, `s3:GetBucketAcl` | -| `policy` | Manage bucket policies | `s3:PutBucketPolicy`, `s3:GetBucketPolicy`, `s3:DeleteBucketPolicy` | -| `versioning` | Manage bucket versioning configuration | `s3:GetBucketVersioning`, `s3:PutBucketVersioning` | -| `tagging` | Manage bucket-level tags | `s3:GetBucketTagging`, `s3:PutBucketTagging`, `s3:DeleteBucketTagging` | -| `encryption` | Manage bucket encryption configuration | `s3:GetEncryptionConfiguration`, `s3:PutEncryptionConfiguration`, `s3:DeleteEncryptionConfiguration` | -| `lifecycle` | Manage lifecycle rules | `s3:GetLifecycleConfiguration`, `s3:PutLifecycleConfiguration`, `s3:DeleteLifecycleConfiguration`, `s3:GetBucketLifecycle`, `s3:PutBucketLifecycle` | -| `cors` | Manage CORS configuration | `s3:GetBucketCors`, `s3:PutBucketCors`, `s3:DeleteBucketCors` | -| `replication` | Configure and manage replication | `s3:GetReplicationConfiguration`, `s3:PutReplicationConfiguration`, `s3:DeleteReplicationConfiguration`, `s3:ReplicateObject`, `s3:ReplicateTags`, `s3:ReplicateDelete` | -| `quota` | Manage bucket storage quotas | `s3:GetBucketQuota`, `s3:PutBucketQuota`, `s3:DeleteBucketQuota` | -| `object_lock` | Manage object lock, retention, and legal holds | `s3:GetObjectLockConfiguration`, `s3:PutObjectLockConfiguration`, `s3:PutObjectRetention`, `s3:GetObjectRetention`, `s3:PutObjectLegalHold`, `s3:GetObjectLegalHold` | -| `notification` | Manage bucket event notifications | `s3:GetBucketNotificationConfiguration`, `s3:PutBucketNotificationConfiguration`, `s3:DeleteBucketNotificationConfiguration` | -| `logging` | Manage bucket access logging | `s3:GetBucketLogging`, `s3:PutBucketLogging`, `s3:DeleteBucketLogging` | -| `website` | Manage static website hosting configuration | `s3:GetBucketWebsite`, `s3:PutBucketWebsite`, `s3:DeleteBucketWebsite` | - -#### IAM Actions (User Management) - -| Action | Description | AWS Aliases | -| --- | --- | --- | -| `iam:list_users` | View all IAM users and their policies | `iam:ListUsers` | -| `iam:create_user` | Create new IAM users | `iam:CreateUser` | -| `iam:delete_user` | Delete IAM users | `iam:DeleteUser` | -| `iam:rotate_key` | Rotate user secret keys | `iam:RotateAccessKey` | -| `iam:update_policy` | Modify user policies | `iam:PutUserPolicy` | -| `iam:create_key` | Create additional access keys for a user | `iam:CreateAccessKey` | -| `iam:delete_key` | Delete an access key from a user | `iam:DeleteAccessKey` | -| `iam:get_user` | View user details and access keys | `iam:GetUser` | -| `iam:get_policy` | View user policy configuration | `iam:GetPolicy` | -| `iam:disable_user` | Temporarily disable/enable a user account | `iam:DisableUser` | -| `iam:*` | **Admin wildcard** – grants all IAM actions | — | - -#### Wildcards - -| Wildcard | Scope | Description | -| --- | --- | --- | -| `*` (in actions) | All S3 actions | Grants all 19 S3 actions including `list`, `read`, `write`, `delete`, `create_bucket`, `delete_bucket`, `share`, `policy`, `versioning`, `tagging`, `encryption`, `lifecycle`, `cors`, `replication`, `quota`, `object_lock`, `notification`, `logging`, `website` | -| `iam:*` | All IAM actions | Grants all `iam:*` actions for user management | -| `*` (in bucket) | All buckets | Policy applies to every bucket | - -### IAM Policy Structure - -User policies are stored as a JSON array of policy objects. Each object specifies a bucket, the allowed actions, and an optional prefix for object-level scoping: - -```json -[ - { - "bucket": "", - "actions": ["", "", ...], - "prefix": "" - } -] +```text +data/ + / + .myfsio.sys/ + config/ + iam.json + bucket_policies.json + connections.json + gc_history.json + integrity_history.json + metrics_history.json + operation_metrics.json + buckets// + meta/ + versions/ + multipart/ + keys/ ``` -**Fields:** -- `bucket`: The bucket name (case-insensitive) or `*` for all buckets -- `actions`: Array of action strings (simple names or AWS aliases) -- `prefix`: *(optional)* Restrict object-level actions to keys starting with this prefix. Defaults to `*` (all objects). Example: `"uploads/"` restricts to keys under `uploads/` +Important files: -### Example User Policies +- `data/.myfsio.sys/config/iam.json`: IAM users, access keys, and inline policies +- `data/.myfsio.sys/config/bucket_policies.json`: bucket policies +- `data/.myfsio.sys/config/connections.json`: replication connection settings +- `data/.myfsio.sys/config/.secret`: persisted secret key when one has been generated for the install -**Full Administrator (complete system access):** -```json -[{"bucket": "*", "actions": ["list", "read", "write", "delete", "share", "policy", "create_bucket", "delete_bucket", "versioning", "tagging", "encryption", "lifecycle", "cors", "replication", "quota", "object_lock", "notification", "logging", "website", "iam:*"]}] -``` +## 8. Background Services -**Read-Only User (browse and download only):** -```json -[{"bucket": "*", "actions": ["list", "read"]}] -``` +The Rust server can start several workers from the same process. -**Single Bucket Full Access (no access to other buckets):** -```json -[{"bucket": "user-bucket", "actions": ["list", "read", "write", "delete"]}] -``` +### Lifecycle -**Operator (data operations + bucket management, no config):** -```json -[{"bucket": "*", "actions": ["list", "read", "write", "delete", "create_bucket", "delete_bucket"]}] -``` - -**Multiple Bucket Access (different permissions per bucket):** -```json -[ - {"bucket": "public-data", "actions": ["list", "read"]}, - {"bucket": "my-uploads", "actions": ["list", "read", "write", "delete"]}, - {"bucket": "team-shared", "actions": ["list", "read", "write"]} -] -``` - -**Prefix-Scoped Access (restrict to a folder inside a shared bucket):** -```json -[{"bucket": "shared-data", "actions": ["list", "read", "write", "delete"], "prefix": "team-a/"}] -``` - -**IAM Manager (manage users but no data access):** -```json -[{"bucket": "*", "actions": ["iam:list_users", "iam:create_user", "iam:delete_user", "iam:rotate_key", "iam:update_policy", "iam:create_key", "iam:delete_key", "iam:get_user", "iam:get_policy", "iam:disable_user"]}] -``` - -**Replication Operator (manage replication only):** -```json -[{"bucket": "*", "actions": ["list", "read", "replication"]}] -``` - -**Lifecycle Manager (configure object expiration):** -```json -[{"bucket": "*", "actions": ["list", "lifecycle"]}] -``` - -**CORS Administrator (configure cross-origin access):** -```json -[{"bucket": "*", "actions": ["cors"]}] -``` - -**Bucket Administrator (full bucket config, no IAM access):** -```json -[{"bucket": "my-bucket", "actions": ["list", "read", "write", "delete", "create_bucket", "delete_bucket", "share", "policy", "versioning", "tagging", "encryption", "lifecycle", "cors", "replication", "quota", "object_lock", "notification", "logging", "website"]}] -``` - -**Upload-Only User (write but cannot create/delete buckets):** -```json -[{"bucket": "drop-box", "actions": ["write"]}] -``` - -**Backup Operator (read, list, and replicate):** -```json -[{"bucket": "*", "actions": ["list", "read", "replication"]}] -``` - -### Using AWS-Style Action Names - -You can use AWS S3 action names instead of simple names. They are automatically normalized: - -```json -[ - { - "bucket": "my-bucket", - "actions": [ - "s3:ListBucket", - "s3:GetObject", - "s3:PutObject", - "s3:DeleteObject" - ] - } -] -``` - -This is equivalent to: -```json -[{"bucket": "my-bucket", "actions": ["list", "read", "write", "delete"]}] -``` - -### Managing Users via API +Enable with: ```bash -# List all users (requires iam:list_users) -curl http://localhost:5000/iam/users \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Create a new user (requires iam:create_user) -curl -X POST http://localhost:5000/iam/users \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "display_name": "New User", - "policies": [{"bucket": "*", "actions": ["list", "read"]}], - "expires_at": "2026-12-31T23:59:59Z" - }' - -# Rotate user secret (requires iam:rotate_key) -curl -X POST http://localhost:5000/iam/users//rotate \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Update user policies (requires iam:update_policy) -curl -X PUT http://localhost:5000/iam/users//policies \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '[{"bucket": "*", "actions": ["list", "read", "write"]}]' - -# Update credential expiry (requires iam:update_policy) -curl -X POST http://localhost:5000/iam/users//expiry \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d 'expires_at=2026-12-31T23:59:59Z' - -# Remove credential expiry (never expires) -curl -X POST http://localhost:5000/iam/users//expiry \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d 'expires_at=' - -# Delete a user (requires iam:delete_user) -curl -X DELETE http://localhost:5000/iam/users/ \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Get user details (requires iam:get_user) — via Admin API -curl http://localhost:5000/admin/iam/users/ \ - -H "Authorization: AWS4-HMAC-SHA256 ..." - -# Get user policies (requires iam:get_policy) — via Admin API -curl http://localhost:5000/admin/iam/users//policies \ - -H "Authorization: AWS4-HMAC-SHA256 ..." - -# Create additional access key for a user (requires iam:create_key) -curl -X POST http://localhost:5000/admin/iam/users//keys \ - -H "Authorization: AWS4-HMAC-SHA256 ..." - -# Delete an access key (requires iam:delete_key) -curl -X DELETE http://localhost:5000/admin/iam/users//keys/ \ - -H "Authorization: AWS4-HMAC-SHA256 ..." - -# Disable a user account (requires iam:disable_user) -curl -X POST http://localhost:5000/admin/iam/users//disable \ - -H "Authorization: AWS4-HMAC-SHA256 ..." - -# Re-enable a user account (requires iam:disable_user) -curl -X POST http://localhost:5000/admin/iam/users//enable \ - -H "Authorization: AWS4-HMAC-SHA256 ..." +LIFECYCLE_ENABLED=true cargo run -p myfsio-server -- ``` -### Permission Precedence +Current Rust behavior: -When a request is made, permissions are evaluated in this order: +- Runs as a Tokio background task, not a cron job +- Default interval is 3600 seconds +- Evaluates bucket lifecycle configuration and applies expiration and multipart abort rules -1. **Authentication** – Verify the access key and secret key are valid -2. **Lockout Check** – Ensure the account is not locked due to failed attempts -3. **Expiry Check** – Reject requests if the user's credentials have expired (`expires_at`) -4. **IAM Policy Check** – Verify the user has the required action for the target bucket -5. **Bucket Policy Check** – If a bucket policy exists, verify it allows the action +At the moment, the interval is hardcoded through `LifecycleConfig::default()` rather than exposed as an environment variable. -A request is allowed only if: -- The IAM policy grants the action, AND -- The bucket policy allows the action (or no bucket policy exists) +### Garbage collection -### Common Permission Scenarios - -| Scenario | Required Actions | -| --- | --- | -| Browse bucket contents | `list` | -| Download a file | `read` | -| Upload a file | `write` | -| Delete a file | `delete` | -| Generate presigned URL (GET) | `read` | -| Generate presigned URL (PUT) | `write` | -| Generate presigned URL (DELETE) | `delete` | -| Enable versioning | `write` (includes `s3:PutBucketVersioning`) | -| View bucket policy | `policy` | -| Modify bucket policy | `policy` | -| Configure lifecycle rules | `lifecycle` | -| View lifecycle rules | `lifecycle` | -| Configure CORS | `cors` | -| View CORS rules | `cors` | -| Configure replication | `replication` (admin-only for creation) | -| Pause/resume replication | `replication` | -| Manage other users | `iam:*` or specific `iam:` actions | -| Set bucket quotas | `iam:*` or `iam:list_users` (admin feature) | - -### Security Best Practices - -1. **Principle of Least Privilege** – Grant only the permissions users need -2. **Avoid Wildcards** – Use specific bucket names instead of `*` when possible -3. **Rotate Secrets Regularly** – Use the rotate key feature periodically -4. **Separate Admin Accounts** – Don't use admin accounts for daily operations -5. **Monitor Failed Logins** – Check logs for repeated authentication failures -6. **Use Bucket Policies for Fine-Grained Control** – Combine with IAM for defense in depth - -## 5. Bucket Policies & Presets - -- **Storage**: Policies are persisted in `data/.myfsio.sys/config/bucket_policies.json` under `{"policies": {"bucket": {...}}}`. -- **Hot reload**: Both API and UI call `maybe_reload()` before evaluating policies. Editing the JSON on disk is immediately reflected—no restarts required. -- **UI editor**: Each bucket detail page includes: - - A preset selector: **Private** detaches the policy (delete mode), **Public** injects an allow policy granting anonymous `s3:ListBucket` + `s3:GetObject`, and **Custom** restores your draft. - - A read-only preview of the attached policy. - - Autosave behavior for custom drafts while you type. - -### Editing via CLI +Enable with: ```bash -curl -X PUT "http://127.0.0.1:5000/test?policy" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": ["s3:ListBucket"], - "Resource": ["arn:aws:s3:::test"] - } - ] - }' +GC_ENABLED=true cargo run -p myfsio-server -- ``` -The UI will reflect this change as soon as the request completes thanks to the hot reload. +Current Rust defaults from `GcConfig::default()`: -### UI Object Browser +- Run every 6 hours +- Temp files older than 24 hours are eligible for cleanup +- Multipart uploads older than 7 days are eligible for cleanup +- Lock files older than 1 hour are eligible for cleanup -The bucket detail page includes a powerful object browser with the following features: +Those GC timings are currently hardcoded defaults, not environment-driven configuration. -#### Folder Navigation +### Integrity scanning -Objects with forward slashes (`/`) in their keys are displayed as a folder hierarchy. Click a folder row to navigate into it. A breadcrumb navigation bar shows your current path and allows quick navigation back to parent folders or the root. - -#### Pagination & Infinite Scroll - -- Objects load in configurable batches (50, 100, 150, 200, or 250 per page) -- Scroll to the bottom to automatically load more objects (infinite scroll) -- A **Load more** button is available as a fallback for touch devices or when infinite scroll doesn't trigger -- The footer shows the current load status (e.g., "Showing 100 of 500 objects") - -#### Bulk Operations - -- Select multiple objects using checkboxes -- **Bulk Delete**: Delete multiple objects at once -- **Bulk Download**: Download selected objects as a single ZIP archive (up to `BULK_DOWNLOAD_MAX_BYTES`, default 1 GiB) - -#### Search & Filter - -Use the search box to filter objects by name in real-time. The filter applies to the currently loaded objects. - -#### Error Handling - -If object loading fails (e.g., network error), a friendly error message is displayed with a **Retry** button to attempt loading again. - -#### Object Preview - -Click any object row to view its details in the preview sidebar: -- File size and last modified date -- ETag (content hash) -- Custom metadata (if present) -- Download and presign (share link) buttons -- Version history (when versioning is enabled) - -#### Drag & Drop Upload - -Drag files directly onto the objects table to upload them to the current bucket and folder path. - -## 6. Presigned URLs - -- Trigger from the UI using the **Presign** button after selecting an object. -- Supported methods: `GET`, `PUT`, `DELETE`; expiration must be `1..604800` seconds. -- The service signs requests using the caller's IAM credentials and enforces bucket policies both when issuing and when the presigned URL is used. -- Legacy share links have been removed; presigned URLs now handle both private and public workflows. - -### Multipart Upload Example - -```python -import boto3 - -s3 = boto3.client('s3', endpoint_url='http://localhost:5000') - -# Initiate -response = s3.create_multipart_upload(Bucket='mybucket', Key='large.bin') -upload_id = response['UploadId'] - -# Upload parts -parts = [] -chunks = [b'chunk1', b'chunk2'] # Example data chunks -for part_number, chunk in enumerate(chunks, start=1): - response = s3.upload_part( - Bucket='mybucket', - Key='large.bin', - PartNumber=part_number, - UploadId=upload_id, - Body=chunk - ) - parts.append({'PartNumber': part_number, 'ETag': response['ETag']}) - -# Complete -s3.complete_multipart_upload( - Bucket='mybucket', - Key='large.bin', - UploadId=upload_id, - MultipartUpload={'Parts': parts} -) -``` - -## 7. Encryption - -MyFSIO supports **server-side encryption at rest** to protect your data. When enabled, objects are encrypted using AES-256-GCM before being written to disk. - -### Encryption Types - -| Type | Description | -|------|-------------| -| **AES-256 (SSE-S3)** | Server-managed encryption using a local master key | -| **KMS (SSE-KMS)** | Encryption using customer-managed keys via the built-in KMS | -| **SSE-C** | Server-side encryption with customer-provided keys (per-request) | - -### Enabling Encryption - -#### 1. Set Environment Variables - -```powershell -# PowerShell -$env:ENCRYPTION_ENABLED = "true" -$env:KMS_ENABLED = "true" # Optional, for KMS key management -python run.py -``` +Enable with: ```bash -# Bash -export ENCRYPTION_ENABLED=true -export KMS_ENABLED=true -python run.py +INTEGRITY_ENABLED=true cargo run -p myfsio-server -- ``` -#### 2. Configure Bucket Default Encryption (UI) +Current Rust defaults from `IntegrityConfig::default()`: -1. Navigate to your bucket in the UI -2. Click the **Properties** tab -3. Find the **Default Encryption** card -4. Click **Enable Encryption** -5. Choose algorithm: - - **AES-256**: Uses the server's master key - - **aws:kms**: Uses a KMS-managed key (select from dropdown) -6. Save changes +- Run every 24 hours +- Batch size 1000 +- Auto-heal disabled -Once enabled, all **new objects** uploaded to the bucket will be automatically encrypted. +### Metrics history -### KMS Key Management - -When `KMS_ENABLED=true`, you can manage encryption keys via the KMS API: +Enable with: ```bash -# Create a new KMS key -curl -X POST http://localhost:5000/kms/keys \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{"alias": "my-key", "description": "Production encryption key"}' - -# List all keys -curl http://localhost:5000/kms/keys \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Get key details -curl http://localhost:5000/kms/keys/{key-id} \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Rotate a key (creates new key material) -curl -X POST http://localhost:5000/kms/keys/{key-id}/rotate \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Disable/Enable a key -curl -X POST http://localhost:5000/kms/keys/{key-id}/disable \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -curl -X POST http://localhost:5000/kms/keys/{key-id}/enable \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Schedule key deletion (30-day waiting period) -curl -X DELETE http://localhost:5000/kms/keys/{key-id}?waiting_period_days=30 \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." +METRICS_HISTORY_ENABLED=true cargo run -p myfsio-server -- ``` -### How It Works - -1. **Envelope Encryption**: Each object is encrypted with a unique Data Encryption Key (DEK) -2. **Key Wrapping**: The DEK is encrypted (wrapped) by the master key or KMS key -3. **Storage**: The encrypted DEK is stored alongside the encrypted object -4. **Decryption**: On read, the DEK is unwrapped and used to decrypt the object - -### Client-Side Encryption - -For additional security, you can use client-side encryption. The `ClientEncryptionHelper` class provides utilities: - -```python -from app.encryption import ClientEncryptionHelper - -# Generate a client-side key -key = ClientEncryptionHelper.generate_key() -key_b64 = ClientEncryptionHelper.key_to_base64(key) - -# Encrypt before upload -plaintext = b"sensitive data" -encrypted, metadata = ClientEncryptionHelper.encrypt_for_upload(plaintext, key) - -# Upload with metadata headers -# x-amz-meta-x-amz-key: -# x-amz-meta-x-amz-iv: -# x-amz-meta-x-amz-matdesc: - -# Decrypt after download -decrypted = ClientEncryptionHelper.decrypt_from_download(encrypted, metadata, key) -``` - -### SSE-C (Customer-Provided Keys) - -With SSE-C, you provide your own 256-bit AES encryption key with each request. The server encrypts/decrypts using your key but never stores it. You must supply the same key for both upload and download. - -**Required headers:** - -| Header | Value | -|--------|-------| -| `x-amz-server-side-encryption-customer-algorithm` | `AES256` | -| `x-amz-server-side-encryption-customer-key` | Base64-encoded 256-bit key | -| `x-amz-server-side-encryption-customer-key-MD5` | Base64-encoded MD5 of the key | +Tune it with: ```bash -# Generate a 256-bit key -KEY=$(openssl rand -base64 32) -KEY_MD5=$(echo -n "$KEY" | base64 -d | openssl dgst -md5 -binary | base64) - -# Upload with SSE-C -curl -X PUT "http://localhost:5000/my-bucket/secret.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-server-side-encryption-customer-algorithm: AES256" \ - -H "x-amz-server-side-encryption-customer-key: $KEY" \ - -H "x-amz-server-side-encryption-customer-key-MD5: $KEY_MD5" \ - --data-binary @secret.txt - -# Download with SSE-C (same key required) -curl "http://localhost:5000/my-bucket/secret.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-server-side-encryption-customer-algorithm: AES256" \ - -H "x-amz-server-side-encryption-customer-key: $KEY" \ - -H "x-amz-server-side-encryption-customer-key-MD5: $KEY_MD5" +METRICS_HISTORY_INTERVAL_MINUTES=10 +METRICS_HISTORY_RETENTION_HOURS=72 ``` -**Key points:** -- SSE-C does not require `ENCRYPTION_ENABLED` or `KMS_ENABLED` — the key is provided per-request -- If you lose your key, the data is irrecoverable -- The MD5 header is optional but recommended for integrity verification +Snapshots are stored in `data/.myfsio.sys/config/metrics_history.json`. -### Important Notes +### Operation metrics -- **Existing objects are NOT encrypted** - Only new uploads after enabling encryption are encrypted -- **Master key security** - The master key file (`master.key`) should be backed up securely and protected -- **Key rotation** - Rotating a KMS key creates new key material; existing objects remain encrypted with the old material -- **Disabled keys** - Objects encrypted with a disabled key cannot be decrypted until the key is re-enabled -- **Deleted keys** - Once a key is deleted (after the waiting period), objects encrypted with it are permanently inaccessible - -### Verifying Encryption - -To verify an object is encrypted: -1. Check the raw file in `data//` - it should be unreadable binary -2. Look for `.meta` files containing encryption metadata -3. Download via the API/UI - the object should be automatically decrypted - -## 8. Bucket Quotas - -MyFSIO supports **storage quotas** to limit how much data a bucket can hold. Quotas are enforced on uploads and multipart completions. - -### Quota Types - -| Limit | Description | -|-------|-------------| -| **Max Size (MB)** | Maximum total storage in megabytes (includes current objects + archived versions) | -| **Max Objects** | Maximum number of objects (includes current objects + archived versions) | - -### Managing Quotas (Admin Only) - -Quota management is restricted to administrators (users with `iam:*` or `iam:list_users` permissions). - -#### Via UI - -1. Navigate to your bucket in the UI -2. Click the **Properties** tab -3. Find the **Storage Quota** card -4. Enter limits: - - **Max Size (MB)**: Leave empty for unlimited - - **Max Objects**: Leave empty for unlimited -5. Click **Update Quota** - -To remove a quota, click **Remove Quota**. - -#### Via API +Enable with: ```bash -# Set quota (max 100MB, max 1000 objects) -curl -X PUT "http://localhost:5000/bucket/?quota" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{"max_bytes": 104857600, "max_objects": 1000}' - -# Get current quota -curl "http://localhost:5000/bucket/?quota" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Remove quota -curl -X PUT "http://localhost:5000/bucket/?quota" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{"max_bytes": null, "max_objects": null}' +OPERATION_METRICS_ENABLED=true cargo run -p myfsio-server -- ``` -### Quota Behavior - -- **Version Counting**: When versioning is enabled, archived versions count toward the quota -- **Enforcement Points**: Quotas are checked during `PUT` object and `CompleteMultipartUpload` operations -- **Error Response**: When quota is exceeded, the API returns `HTTP 400` with error code `QuotaExceeded` -- **Visibility**: All users can view quota usage in the bucket detail page, but only admins can modify quotas - -### Example Error - -```xml - - QuotaExceeded - Bucket quota exceeded: storage limit reached - my-bucket - -``` - -## 9. Operation Metrics - -Operation metrics provide real-time visibility into API request statistics, including request counts, latency, error rates, and bandwidth usage. - -### Enabling Operation Metrics - -By default, operation metrics are disabled. Enable by setting the environment variable: +Tune it with: ```bash -OPERATION_METRICS_ENABLED=true python run.py -``` - -Or in your `myfsio.env` file: -``` -OPERATION_METRICS_ENABLED=true OPERATION_METRICS_INTERVAL_MINUTES=5 OPERATION_METRICS_RETENTION_HOURS=24 ``` -### Configuration Options +Snapshots are stored in `data/.myfsio.sys/config/operation_metrics.json`. -| Variable | Default | Description | -|----------|---------|-------------| -| `OPERATION_METRICS_ENABLED` | `false` | Enable/disable operation metrics | -| `OPERATION_METRICS_INTERVAL_MINUTES` | `5` | Snapshot interval (minutes) | -| `OPERATION_METRICS_RETENTION_HOURS` | `24` | History retention period (hours) | +## 9. Encryption and KMS -### What's Tracked - -**Request Statistics:** -- Request counts by HTTP method (GET, PUT, POST, DELETE, HEAD, OPTIONS) -- Response status codes grouped by class (2xx, 3xx, 4xx, 5xx) -- Latency statistics (min, max, average) -- Bytes transferred in/out - -**Endpoint Breakdown:** -- `object` - Object operations (GET/PUT/DELETE objects) -- `bucket` - Bucket operations (list, create, delete buckets) -- `ui` - Web UI requests -- `service` - Health checks, internal endpoints -- `kms` - KMS API operations - -**S3 Error Codes:** -Tracks API-specific error codes like `NoSuchKey`, `AccessDenied`, `BucketNotFound`. Note: These are separate from HTTP status codes - a 404 from the UI won't appear here, only S3 API errors. - -### API Endpoints +Object encryption and built-in KMS are both optional. ```bash -# Get current operation metrics -curl http://localhost:5100/ui/metrics/operations \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Get operation metrics history -curl http://localhost:5100/ui/metrics/operations/history \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Filter history by time range -curl "http://localhost:5100/ui/metrics/operations/history?hours=6" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." +ENCRYPTION_ENABLED=true KMS_ENABLED=true cargo run -p myfsio-server -- ``` -### Storage Location +Notes: -Operation metrics data is stored at: -``` -data/.myfsio.sys/config/operation_metrics.json -``` +- If `ENCRYPTION_ENABLED=true` and `SECRET_KEY` is not configured, the server still starts, but `--check-config` warns that secure-at-rest config encryption is unavailable. +- KMS and the object encryption master key live under `data/.myfsio.sys/keys/`. -### UI Dashboard +## 10. Docker -When enabled, the Metrics page (`/ui/metrics`) shows an "API Operations" section with: -- Summary cards: Requests, Success Rate, Errors, Latency, Bytes In, Bytes Out -- Charts: Requests by Method (doughnut), Requests by Status (bar), Requests by Endpoint (horizontal bar) -- S3 Error Codes table with distribution - -Data refreshes every 5 seconds. - -## 10. Site Replication - -### Permission Model - -Replication uses a two-tier permission system: - -| Role | Capabilities | -|------|--------------| -| **Admin** (users with `iam:*` permissions) | Create/delete replication rules, configure connections and target buckets | -| **Users** (with `replication` permission) | Enable/disable (pause/resume) existing replication rules | - -> **Note:** The Replication tab is hidden for users without the `replication` permission on the bucket. - -This separation allows administrators to pre-configure where data should replicate, while allowing authorized users to toggle replication on/off without accessing connection credentials. - -### Replication Modes - -| Mode | Behavior | -|------|----------| -| `new_only` | Only replicate new/modified objects (default) | -| `all` | Sync all existing objects when rule is enabled | -| `bidirectional` | Two-way sync with Last-Write-Wins conflict resolution | - -### Architecture - -- **Source Instance**: The MyFSIO instance where you upload files. It runs the replication worker. -- **Target Instance**: Another MyFSIO instance (or any S3-compatible service like AWS S3, MinIO) that receives the copies. - -For `new_only` and `all` modes, replication is **asynchronous** (happens in the background) and **one-way** (Source -> Target). - -For `bidirectional` mode, replication is **two-way** with automatic conflict resolution. - -### Setup Guide - -#### 1. Prepare the Target Instance - -If your target is another MyFSIO server (e.g., running on a different machine or port), you need to create a destination bucket and a user with write permissions. - -**Option A: Using the UI (Easiest)** -If you have access to the UI of the target instance: -1. Log in to the Target UI. -2. Create a new bucket (e.g., `backup-bucket`). -3. Go to **IAM**, create a new user (e.g., `replication-user`), and copy the Access/Secret keys. - -**Option B: Headless Setup (API Only)** -If the target server is only running the API (`run_api.py`) and has no UI access, you can bootstrap the credentials and bucket by running a Python script on the server itself. - -Run this script on the **Target Server**: - -```python -# setup_target.py -from pathlib import Path -from app.iam import IamService -from app.storage import ObjectStorage - -# Initialize services (paths match default config) -data_dir = Path("data") -iam = IamService(data_dir / ".myfsio.sys" / "config" / "iam.json") -storage = ObjectStorage(data_dir) - -# 1. Create the bucket -bucket_name = "backup-bucket" -try: - storage.create_bucket(bucket_name) - print(f"Bucket '{bucket_name}' created.") -except Exception as e: - print(f"Bucket creation skipped: {e}") - -# 2. Create the user -try: - # Create user with full access (or restrict policy as needed) - creds = iam.create_user( - display_name="Replication User", - policies=[{"bucket": bucket_name, "actions": ["write", "read", "list"]}] - ) - print("\n--- CREDENTIALS GENERATED ---") - print(f"Access Key: {creds['access_key']}") - print(f"Secret Key: {creds['secret_key']}") - print("-----------------------------") -except Exception as e: - print(f"User creation failed: {e}") -``` - -Save and run: `python setup_target.py` - -#### 2. Configure the Source Instance - -Now, configure the primary instance to replicate to the target. - -1. **Access the Console**: - Log in to the UI of your Source Instance. - -2. **Add a Connection**: - - Navigate to **Connections** in the top menu. - - Click **Add Connection**. - - **Name**: `Secondary Site`. - - **Endpoint URL**: The URL of your Target Instance's API (e.g., `http://target-server:5002`). - - **Access Key**: The key you generated on the Target. - - **Secret Key**: The secret you generated on the Target. - - Click **Add Connection**. - -3. **Enable Replication** (Admin): - - Navigate to **Buckets** and select the source bucket. - - Switch to the **Replication** tab. - - Select the `Secondary Site` connection. - - Enter the target bucket name (`backup-bucket`). - - Click **Enable Replication**. - - Once configured, users with `replication` permission on this bucket can pause/resume replication without needing access to connection details. - -### Verification - -1. Upload a file to the source bucket. -2. Check the target bucket (via UI, CLI, or API). The file should appear shortly. +Build the Rust image from the `rust/` directory: ```bash -# Verify on target using AWS CLI -aws --endpoint-url http://target-server:5002 s3 ls s3://backup-bucket +docker build -t myfsio ./rust +docker run --rm \ + -p 5000:5000 \ + -p 5100:5100 \ + -v "$PWD/data:/app/data" \ + myfsio ``` -### Pausing and Resuming Replication +The container entrypoint runs `/usr/local/bin/myfsio-server`. -Users with the `replication` permission (but not admin rights) can pause and resume existing replication rules: +Inside the image: -1. Navigate to the bucket's **Replication** tab. -2. If replication is **Active**, click **Pause Replication** to temporarily stop syncing. -3. If replication is **Paused**, click **Resume Replication** to continue syncing. +- `HOST=0.0.0.0` +- `PORT=5000` +- `STORAGE_ROOT=/app/data` -When paused, new objects uploaded to the source will not replicate until replication is resumed. Objects uploaded while paused will be replicated once resumed. +If you want generated links and presigned URLs to use an external hostname, set `API_BASE_URL`. -> **Note:** Only admins can create new replication rules, change the target connection/bucket, or delete rules entirely. +## 11. Linux Installer -### Bidirectional Site Replication - -For true two-way synchronization with automatic conflict resolution, use the `bidirectional` replication mode. This enables a background sync worker that periodically pulls changes from the remote site. - -> **Important:** Both sites must be configured to sync with each other. Each site pushes its changes and pulls from the other. You must set up connections and replication rules on both ends. - -#### Step 1: Enable Site Sync on Both Sites - -Set these environment variables on **both** Site A and Site B: +The repository includes `scripts/install.sh`. For the Rust server, build the binary first and pass the path explicitly: ```bash -SITE_SYNC_ENABLED=true -SITE_SYNC_INTERVAL_SECONDS=60 # How often to pull changes (default: 60) -SITE_SYNC_BATCH_SIZE=100 # Max objects per sync cycle (default: 100) +cd rust/myfsio-engine +cargo build --release -p myfsio-server + +cd ../.. +sudo ./scripts/install.sh --binary ./rust/myfsio-engine/target/release/myfsio-server ``` -#### Step 2: Create IAM Users for Cross-Site Access +The installer copies that binary to `/opt/myfsio/myfsio`, creates `/opt/myfsio/myfsio.env`, and can register a `myfsio.service` systemd unit. -On each site, create an IAM user that the other site will use to connect: +## 12. Updating and Rollback -| Site | Create User For | Required Permissions | -|------|-----------------|---------------------| -| Site A | Site B to connect | `read`, `write`, `list`, `delete` on target bucket | -| Site B | Site A to connect | `read`, `write`, `list`, `delete` on target bucket | +Recommended update flow: -Example policy for the replication user: -```json -[{"bucket": "my-bucket", "actions": ["read", "write", "list", "delete"]}] +1. Stop the running service. +2. Back up `data/.myfsio.sys/config/`. +3. Build or download the new Rust binary. +4. Run `myfsio-server --check-config` against the target environment. +5. Start the service and verify `/myfsio/health`. + +Example backup: + +```bash +cp -r data/.myfsio.sys/config config-backup ``` -#### Step 3: Create Connections +Health check: -On each site, add a connection pointing to the other: +```bash +curl http://127.0.0.1:5000/myfsio/health +``` -**On Site A:** -- Go to **Connections** and add a connection to Site B -- Endpoint: `https://site-b.example.com` -- Credentials: Site B's IAM user (created in Step 2) - -**On Site B:** -- Go to **Connections** and add a connection to Site A -- Endpoint: `https://site-a.example.com` -- Credentials: Site A's IAM user (created in Step 2) - -#### Step 4: Enable Bidirectional Replication - -On each site, go to the bucket's **Replication** tab and enable with mode `bidirectional`: - -**On Site A:** -- Source bucket: `my-bucket` -- Target connection: Site B connection -- Target bucket: `my-bucket` -- Mode: **Bidirectional sync** - -**On Site B:** -- Source bucket: `my-bucket` -- Target connection: Site A connection -- Target bucket: `my-bucket` -- Mode: **Bidirectional sync** - -#### How It Works - -- **PUSH**: Local changes replicate to remote immediately on write/delete -- **PULL**: Background worker fetches remote changes every `SITE_SYNC_INTERVAL_SECONDS` -- **Loop Prevention**: `S3ReplicationAgent` and `SiteSyncAgent` User-Agents prevent infinite sync loops - -#### Conflict Resolution (Last-Write-Wins) - -When the same object exists on both sites, the system uses Last-Write-Wins (LWW) based on `last_modified` timestamps: - -- **Remote newer**: Pull the remote version -- **Local newer**: Keep the local version -- **Same timestamp**: Use ETag as tiebreaker (higher ETag wins) - -A 1-second clock skew tolerance prevents false conflicts from minor time differences. - -#### Deletion Synchronization - -When `sync_deletions=true` (default), remote deletions propagate locally only if: -1. The object was previously synced FROM remote (tracked in sync state) -2. The local version hasn't been modified since last sync - -This prevents accidental deletion of local-only objects. - -#### Sync State Storage - -Sync state is stored at: `data/.myfsio.sys/buckets//site_sync_state.json` +The response includes the active Rust crate version: ```json { - "synced_objects": { - "path/to/file.txt": { - "last_synced_at": 1706100000.0, - "remote_etag": "abc123", - "source": "remote" - } - }, - "last_full_sync": 1706100000.0 + "status": "ok", + "version": "0.5.0" } ``` -### Legacy Bidirectional Setup (Manual) +## 13. Credential Reset -For simpler use cases without the site sync worker, you can manually configure two one-way rules: - -1. Follow the steps above to replicate **A → B**. -2. Repeat the process on Server B to replicate **B → A**: - - Create a connection on Server B pointing to Server A. - - Enable replication on the target bucket on Server B. - -**Loop Prevention**: The system automatically detects replication traffic using custom User-Agents (`S3ReplicationAgent` and `SiteSyncAgent`). This prevents infinite loops where an object replicated from A to B is immediately replicated back to A. - -**Deletes**: Deleting an object on one server will propagate the deletion to the other server. - -**Note**: Deleting a bucket will automatically remove its associated replication configuration. - -## 12. Running Tests +To rotate the bootstrap admin credentials: ```bash -pytest -q +cargo run -p myfsio-server -- --reset-cred ``` -The suite now includes a boto3 integration test that spins up a live HTTP server and drives the API through the official AWS SDK. If you want to skip it (for faster unit-only loops), run `pytest -m "not integration"`. +The command: -The suite covers bucket CRUD, presigned downloads, bucket policy enforcement, and regression tests for anonymous reads when a Public policy is attached. +- backs up the existing IAM file with a timestamped `.bak-...` suffix +- writes a fresh admin config +- respects `ADMIN_ACCESS_KEY` and `ADMIN_SECRET_KEY` if you set them -## 13. Troubleshooting +## 14. Testing -| Symptom | Likely Cause | Fix | -| --- | --- | --- | -| 403 from API despite Public preset | Policy didn’t save or bucket key path mismatch | Reapply Public preset, confirm bucket name in `Resource` matches `arn:aws:s3:::bucket/*`. | -| UI still shows old policy text | Browser cached view before hot reload | Refresh; JSON is already reloaded on server. | -| Presign modal errors with 403 | IAM user lacks `read/write/delete` for target bucket or bucket policy denies | Update IAM inline policies or remove conflicting deny statements. | -| Large upload rejected immediately | File exceeds `MAX_UPLOAD_SIZE` | Increase env var or shrink object. | - -## 14. API Matrix - -``` -# Service Endpoints -GET /myfsio/health # Health check - -# Bucket Operations -GET / # List buckets -PUT / # Create bucket -DELETE / # Remove bucket -GET / # List objects (supports ?list-type=2) -HEAD / # Check bucket exists -POST / # POST object upload (HTML form) -POST /?delete # Bulk delete objects - -# Bucket Configuration -GET /?policy # Fetch bucket policy -PUT /?policy # Upsert bucket policy -DELETE /?policy # Delete bucket policy -GET /?quota # Get bucket quota -PUT /?quota # Set bucket quota (admin only) -GET /?versioning # Get versioning status -PUT /?versioning # Enable/disable versioning -GET /?lifecycle # Get lifecycle rules -PUT /?lifecycle # Set lifecycle rules -DELETE /?lifecycle # Delete lifecycle rules -GET /?cors # Get CORS configuration -PUT /?cors # Set CORS configuration -DELETE /?cors # Delete CORS configuration -GET /?encryption # Get encryption configuration -PUT /?encryption # Set default encryption -DELETE /?encryption # Delete encryption configuration -GET /?acl # Get bucket ACL -PUT /?acl # Set bucket ACL -GET /?tagging # Get bucket tags -PUT /?tagging # Set bucket tags -DELETE /?tagging # Delete bucket tags -GET /?replication # Get replication configuration -PUT /?replication # Set replication rules -DELETE /?replication # Delete replication configuration -GET /?logging # Get access logging configuration -PUT /?logging # Set access logging -GET /?notification # Get event notifications -PUT /?notification # Set event notifications (webhooks) -GET /?object-lock # Get object lock configuration -PUT /?object-lock # Set object lock configuration -GET /?website # Get website configuration -PUT /?website # Set website configuration -DELETE /?website # Delete website configuration -GET /?uploads # List active multipart uploads -GET /?versions # List object versions -GET /?location # Get bucket location/region - -# Object Operations -PUT // # Upload object -GET // # Download object (supports Range header) -DELETE // # Delete object -HEAD // # Get object metadata -POST // # POST upload with policy -POST //?select # SelectObjectContent (SQL query) - -# Object Configuration -GET //?tagging # Get object tags -PUT //?tagging # Set object tags -DELETE //?tagging # Delete object tags -GET //?acl # Get object ACL -PUT //?acl # Set object ACL -PUT //?retention # Set object retention -GET //?retention # Get object retention -PUT //?legal-hold # Set legal hold -GET //?legal-hold # Get legal hold status - -# Multipart Upload -POST //?uploads # Initiate multipart upload -PUT //?uploadId=X&partNumber=N # Upload part -PUT //?uploadId=X&partNumber=N (with x-amz-copy-source) # UploadPartCopy -POST //?uploadId=X # Complete multipart upload -DELETE //?uploadId=X # Abort multipart upload -GET //?uploadId=X # List parts - -# Copy Operations -PUT // (with x-amz-copy-source header) # CopyObject - -# Admin API -GET /admin/site # Get local site info -PUT /admin/site # Update local site -GET /admin/sites # List peer sites -POST /admin/sites # Register peer site -GET /admin/sites/ # Get peer site -PUT /admin/sites/ # Update peer site -DELETE /admin/sites/ # Unregister peer site -GET /admin/sites//health # Check peer health -GET /admin/topology # Get cluster topology -GET /admin/website-domains # List domain mappings -POST /admin/website-domains # Create domain mapping -GET /admin/website-domains/ # Get domain mapping -PUT /admin/website-domains/ # Update domain mapping -DELETE /admin/website-domains/ # Delete domain mapping - -# KMS API -GET /kms/keys # List KMS keys -POST /kms/keys # Create KMS key -GET /kms/keys/ # Get key details -DELETE /kms/keys/ # Schedule key deletion -POST /kms/keys//enable # Enable key -POST /kms/keys//disable # Disable key -POST /kms/keys//rotate # Rotate key material -POST /kms/encrypt # Encrypt data -POST /kms/decrypt # Decrypt data -POST /kms/generate-data-key # Generate data key -POST /kms/generate-random # Generate random bytes -``` - -## 15. Health Check Endpoint - -The API exposes a simple health check endpoint for monitoring and load balancer integration: +Run the Rust test suite: ```bash -# Check API health -curl http://localhost:5000/myfsio/health - -# Response -{"status": "ok", "version": "0.1.7"} +cd rust/myfsio-engine +cargo test ``` -The response includes: -- `status`: Always `"ok"` when the server is running -- `version`: Current application version from `app/version.py` +If you are validating documentation changes for the UI, the most relevant coverage lives under: -Use this endpoint for: -- Load balancer health checks -- Kubernetes liveness/readiness probes -- Monitoring system integration (Prometheus, Datadog, etc.) +- `rust/myfsio-engine/crates/myfsio-server/tests` +- `rust/myfsio-engine/crates/myfsio-storage/src` -## 16. Object Lock & Retention +## 15. API Notes -Object Lock prevents objects from being deleted or overwritten for a specified retention period. MyFSIO supports both GOVERNANCE and COMPLIANCE modes. +The Rust server exposes: -### Retention Modes +- `GET /myfsio/health` +- S3 bucket and object operations on `/` and `//` +- UI routes under `/ui/...` +- admin routes under `/admin/...` +- KMS routes under `/kms/...` -| Mode | Description | -|------|-------------| -| **GOVERNANCE** | Objects can't be deleted by normal users, but users with `s3:BypassGovernanceRetention` permission can override | -| **COMPLIANCE** | Objects can't be deleted or overwritten by anyone, including root, until the retention period expires | +For a route-level view, inspect: -### Enabling Object Lock - -Object Lock must be enabled when creating a bucket: - -```bash -# Create bucket with Object Lock enabled -curl -X PUT "http://localhost:5000/my-bucket" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-bucket-object-lock-enabled: true" - -# Set default retention configuration -curl -X PUT "http://localhost:5000/my-bucket?object-lock" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "ObjectLockEnabled": "Enabled", - "Rule": { - "DefaultRetention": { - "Mode": "GOVERNANCE", - "Days": 30 - } - } - }' -``` - -### Per-Object Retention - -Set retention on individual objects: - -```bash -# Set object retention -curl -X PUT "http://localhost:5000/my-bucket/important.pdf?retention" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "Mode": "COMPLIANCE", - "RetainUntilDate": "2025-12-31T23:59:59Z" - }' - -# Get object retention -curl "http://localhost:5000/my-bucket/important.pdf?retention" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### Legal Hold - -Legal hold provides indefinite protection independent of retention settings: - -```bash -# Enable legal hold -curl -X PUT "http://localhost:5000/my-bucket/document.pdf?legal-hold" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{"Status": "ON"}' - -# Disable legal hold -curl -X PUT "http://localhost:5000/my-bucket/document.pdf?legal-hold" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{"Status": "OFF"}' - -# Check legal hold status -curl "http://localhost:5000/my-bucket/document.pdf?legal-hold" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -## 17. Access Logging - -Enable S3-style access logging to track all requests to your buckets. - -### Configuration - -```bash -# Enable access logging -curl -X PUT "http://localhost:5000/my-bucket?logging" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "LoggingEnabled": { - "TargetBucket": "log-bucket", - "TargetPrefix": "logs/my-bucket/" - } - }' - -# Get logging configuration -curl "http://localhost:5000/my-bucket?logging" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Disable logging (empty configuration) -curl -X PUT "http://localhost:5000/my-bucket?logging" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{}' -``` - -### Log Format - -Access logs are written in S3-compatible format with fields including: -- Timestamp, bucket, key -- Operation (REST.GET.OBJECT, REST.PUT.OBJECT, etc.) -- Request ID, requester, source IP -- HTTP status, error code, bytes sent -- Total time, turn-around time -- Referrer, User-Agent - -## 18. Bucket Notifications & Webhooks - -Configure event notifications to trigger webhooks when objects are created or deleted. - -### Supported Events - -| Event Type | Description | -|-----------|-------------| -| `s3:ObjectCreated:*` | Any object creation (PUT, POST, COPY, multipart) | -| `s3:ObjectCreated:Put` | Object created via PUT | -| `s3:ObjectCreated:Post` | Object created via POST | -| `s3:ObjectCreated:Copy` | Object created via COPY | -| `s3:ObjectCreated:CompleteMultipartUpload` | Multipart upload completed | -| `s3:ObjectRemoved:*` | Any object deletion | -| `s3:ObjectRemoved:Delete` | Object deleted | -| `s3:ObjectRemoved:DeleteMarkerCreated` | Delete marker created (versioned bucket) | - -### Configuration - -```bash -# Set notification configuration -curl -X PUT "http://localhost:5000/my-bucket?notification" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "TopicConfigurations": [ - { - "Id": "upload-notify", - "TopicArn": "https://webhook.example.com/s3-events", - "Events": ["s3:ObjectCreated:*"], - "Filter": { - "Key": { - "FilterRules": [ - {"Name": "prefix", "Value": "uploads/"}, - {"Name": "suffix", "Value": ".jpg"} - ] - } - } - } - ] - }' - -# Get notification configuration -curl "http://localhost:5000/my-bucket?notification" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### Webhook Payload - -The webhook receives a JSON payload similar to AWS S3 event notifications: - -```json -{ - "Records": [ - { - "eventVersion": "2.1", - "eventSource": "myfsio:s3", - "eventTime": "2024-01-15T10:30:00.000Z", - "eventName": "ObjectCreated:Put", - "s3": { - "bucket": {"name": "my-bucket"}, - "object": { - "key": "uploads/photo.jpg", - "size": 102400, - "eTag": "abc123..." - } - } - } - ] -} -``` - -### Security Notes - -- Webhook URLs are validated to prevent SSRF attacks -- Internal/private IP ranges are blocked by default -- Use HTTPS endpoints in production - -## 19. SelectObjectContent (SQL Queries) - -Query CSV, JSON, or Parquet files directly using SQL without downloading the entire object. Requires DuckDB to be installed. - -### Prerequisites - -```bash -pip install duckdb -``` - -### Usage - -```bash -# Query a CSV file -curl -X POST "http://localhost:5000/my-bucket/data.csv?select" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "Expression": "SELECT name, age FROM s3object WHERE age > 25", - "ExpressionType": "SQL", - "InputSerialization": { - "CSV": { - "FileHeaderInfo": "USE", - "FieldDelimiter": "," - } - }, - "OutputSerialization": { - "JSON": {} - } - }' - -# Query a JSON file -curl -X POST "http://localhost:5000/my-bucket/data.json?select" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "Expression": "SELECT * FROM s3object s WHERE s.status = '\"active'\"", - "ExpressionType": "SQL", - "InputSerialization": {"JSON": {"Type": "LINES"}}, - "OutputSerialization": {"JSON": {}} - }' -``` - -### Supported Input Formats - -| Format | Options | -|--------|---------| -| **CSV** | `FileHeaderInfo` (USE, IGNORE, NONE), `FieldDelimiter`, `QuoteCharacter`, `RecordDelimiter` | -| **JSON** | `Type` (DOCUMENT, LINES) | -| **Parquet** | Automatic schema detection | - -### Output Formats - -- **JSON**: Returns results as JSON records -- **CSV**: Returns results as CSV - -## 20. PostObject (HTML Form Upload) - -Upload objects using HTML forms with policy-based authorization. Useful for browser-based direct uploads. - -### Form Fields - -| Field | Required | Description | -|-------|----------|-------------| -| `key` | Yes | Object key (can include `${filename}` placeholder) | -| `file` | Yes | The file to upload | -| `policy` | No | Base64-encoded policy document | -| `x-amz-signature` | No | Policy signature | -| `x-amz-credential` | No | Credential scope | -| `x-amz-algorithm` | No | Signing algorithm (AWS4-HMAC-SHA256) | -| `x-amz-date` | No | Request timestamp | -| `Content-Type` | No | MIME type of the file | -| `x-amz-meta-*` | No | Custom metadata | - -### Example HTML Form - -```html -
- - - - - -
-``` - -### With Policy (Signed Upload) - -For authenticated uploads, include a policy document: - -```bash -# Generate policy and signature using boto3 or similar -# Then include in form: -# - policy: base64(policy_document) -# - x-amz-signature: HMAC-SHA256(policy, signing_key) -# - x-amz-credential: access_key/date/region/s3/aws4_request -# - x-amz-algorithm: AWS4-HMAC-SHA256 -# - x-amz-date: YYYYMMDDTHHMMSSZ -``` - -## 21. Advanced S3 Operations - -### CopyObject - -Copy objects within or between buckets: - -```bash -# Copy within same bucket -curl -X PUT "http://localhost:5000/my-bucket/copy-of-file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-copy-source: /my-bucket/original-file.txt" - -# Copy to different bucket -curl -X PUT "http://localhost:5000/other-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-copy-source: /my-bucket/original-file.txt" - -# Copy with metadata replacement -curl -X PUT "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-copy-source: /my-bucket/file.txt" \ - -H "x-amz-metadata-directive: REPLACE" \ - -H "x-amz-meta-newkey: newvalue" -``` - -### MoveObject (UI) - -Move an object to a different key or bucket. This is a UI-only convenience operation that performs a copy followed by a delete of the source. Requires `read` and `delete` on the source, and `write` on the destination. - -```bash -# Move via UI API -curl -X POST "http://localhost:5100/ui/buckets/my-bucket/objects/old-path/file.txt/move" \ - -H "Content-Type: application/json" \ - --cookie "session=..." \ - -d '{"dest_bucket": "other-bucket", "dest_key": "new-path/file.txt"}' -``` - -The move is atomic from the caller's perspective: if the copy succeeds but the delete fails, the object exists in both locations (no data loss). - -### UploadPartCopy - -Copy data from an existing object into a multipart upload part: - -```bash -# Initiate multipart upload -UPLOAD_ID=$(curl -X POST "http://localhost:5000/my-bucket/large-file.bin?uploads" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." | jq -r '.UploadId') - -# Copy bytes 0-10485759 from source as part 1 -curl -X PUT "http://localhost:5000/my-bucket/large-file.bin?uploadId=$UPLOAD_ID&partNumber=1" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-copy-source: /source-bucket/source-file.bin" \ - -H "x-amz-copy-source-range: bytes=0-10485759" - -# Copy bytes 10485760-20971519 as part 2 -curl -X PUT "http://localhost:5000/my-bucket/large-file.bin?uploadId=$UPLOAD_ID&partNumber=2" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-copy-source: /source-bucket/source-file.bin" \ - -H "x-amz-copy-source-range: bytes=10485760-20971519" -``` - -### Range Requests - -Download partial content using the Range header: - -```bash -# Get first 1000 bytes -curl "http://localhost:5000/my-bucket/large-file.bin" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "Range: bytes=0-999" - -# Get bytes 1000-1999 -curl "http://localhost:5000/my-bucket/large-file.bin" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "Range: bytes=1000-1999" - -# Get last 500 bytes -curl "http://localhost:5000/my-bucket/large-file.bin" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "Range: bytes=-500" - -# Get from byte 5000 to end -curl "http://localhost:5000/my-bucket/large-file.bin" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "Range: bytes=5000-" -``` - -Range responses include: -- HTTP 206 Partial Content status -- `Content-Range` header showing the byte range -- `Accept-Ranges: bytes` header - -### Conditional Requests - -Use conditional headers for cache validation: - -```bash -# Only download if modified since -curl "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "If-Modified-Since: Wed, 15 Jan 2025 10:00:00 GMT" - -# Only download if ETag doesn't match (changed) -curl "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "If-None-Match: \"abc123...\"" - -# Only download if ETag matches -curl "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "If-Match: \"abc123...\"" -``` - -## 22. Access Control Lists (ACLs) - -ACLs provide legacy-style permission management for buckets and objects. - -### Canned ACLs - -| ACL | Description | -|-----|-------------| -| `private` | Owner gets FULL_CONTROL (default) | -| `public-read` | Owner FULL_CONTROL, public READ | -| `public-read-write` | Owner FULL_CONTROL, public READ and WRITE | -| `authenticated-read` | Owner FULL_CONTROL, authenticated users READ | - -### Setting ACLs - -```bash -# Set bucket ACL using canned ACL -curl -X PUT "http://localhost:5000/my-bucket?acl" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-acl: public-read" - -# Set object ACL -curl -X PUT "http://localhost:5000/my-bucket/file.txt?acl" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-acl: private" - -# Set ACL during upload -curl -X PUT "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-acl: public-read" \ - --data-binary @file.txt - -# Get bucket ACL -curl "http://localhost:5000/my-bucket?acl" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Get object ACL -curl "http://localhost:5000/my-bucket/file.txt?acl" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### ACL vs Bucket Policies - -- **ACLs**: Simple, limited options, legacy approach -- **Bucket Policies**: Powerful, flexible, recommended for new deployments - -For most use cases, prefer bucket policies over ACLs. - -## 23. Object & Bucket Tagging - -Add metadata tags to buckets and objects for organization, cost allocation, or lifecycle rule filtering. - -### Bucket Tagging - -```bash -# Set bucket tags -curl -X PUT "http://localhost:5000/my-bucket?tagging" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "TagSet": [ - {"Key": "Environment", "Value": "Production"}, - {"Key": "Team", "Value": "Engineering"} - ] - }' - -# Get bucket tags -curl "http://localhost:5000/my-bucket?tagging" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Delete bucket tags -curl -X DELETE "http://localhost:5000/my-bucket?tagging" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### Object Tagging - -```bash -# Set object tags -curl -X PUT "http://localhost:5000/my-bucket/file.txt?tagging" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "TagSet": [ - {"Key": "Classification", "Value": "Confidential"}, - {"Key": "Owner", "Value": "john@example.com"} - ] - }' - -# Get object tags -curl "http://localhost:5000/my-bucket/file.txt?tagging" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Delete object tags -curl -X DELETE "http://localhost:5000/my-bucket/file.txt?tagging" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Set tags during upload -curl -X PUT "http://localhost:5000/my-bucket/file.txt" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -H "x-amz-tagging: Environment=Staging&Team=QA" \ - --data-binary @file.txt -``` - -### Tagging Limits - -- Maximum 50 tags per object (configurable via `OBJECT_TAG_LIMIT`) -- Tag key: 1-128 Unicode characters -- Tag value: 0-256 Unicode characters - -### Use Cases - -- **Lifecycle Rules**: Filter objects for expiration by tag -- **Access Control**: Use tag conditions in bucket policies -- **Cost Tracking**: Group objects by project or department -- **Automation**: Trigger actions based on object tags - -## 24. CORS Configuration - -Configure Cross-Origin Resource Sharing for browser-based applications. - -### Setting CORS Rules - -```bash -# Set CORS configuration -curl -X PUT "http://localhost:5000/my-bucket?cors" \ - -H "Content-Type: application/json" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." \ - -d '{ - "CORSRules": [ - { - "AllowedOrigins": ["https://example.com", "https://app.example.com"], - "AllowedMethods": ["GET", "PUT", "POST", "DELETE"], - "AllowedHeaders": ["*"], - "ExposeHeaders": ["ETag", "x-amz-meta-*"], - "MaxAgeSeconds": 3600 - } - ] - }' - -# Get CORS configuration -curl "http://localhost:5000/my-bucket?cors" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Delete CORS configuration -curl -X DELETE "http://localhost:5000/my-bucket?cors" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### CORS Rule Fields - -| Field | Description | -|-------|-------------| -| `AllowedOrigins` | Origins allowed to access the bucket (required) | -| `AllowedMethods` | HTTP methods allowed (GET, PUT, POST, DELETE, HEAD) | -| `AllowedHeaders` | Request headers allowed in preflight | -| `ExposeHeaders` | Response headers visible to browser | -| `MaxAgeSeconds` | How long browser can cache preflight response | - -## 25. List Objects API v2 - -MyFSIO supports both ListBucketResult v1 and v2 APIs. - -### Using v2 API - -```bash -# List with v2 (supports continuation tokens) -curl "http://localhost:5000/my-bucket?list-type=2" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# With prefix and delimiter (folder-like listing) -curl "http://localhost:5000/my-bucket?list-type=2&prefix=photos/&delimiter=/" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Pagination with continuation token -curl "http://localhost:5000/my-bucket?list-type=2&max-keys=100&continuation-token=TOKEN" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." - -# Start after specific key -curl "http://localhost:5000/my-bucket?list-type=2&start-after=photos/2024/" \ - -H "X-Access-Key: ..." -H "X-Secret-Key: ..." -``` - -### v1 vs v2 Differences - -| Feature | v1 | v2 | -|---------|----|----| -| Pagination | `marker` | `continuation-token` | -| Start position | `marker` | `start-after` | -| Fetch owner info | Always included | Use `fetch-owner=true` | -| Max keys | 1000 | 1000 | - -### Query Parameters - -| Parameter | Description | -|-----------|-------------| -| `list-type` | Set to `2` for v2 API | -| `prefix` | Filter objects by key prefix | -| `delimiter` | Group objects (typically `/`) | -| `max-keys` | Maximum results (1-1000, default 1000) | -| `continuation-token` | Token from previous response | -| `start-after` | Start listing after this key | -| `fetch-owner` | Include owner info in response | -| `encoding-type` | Set to `url` for URL-encoded keys - -## 26. Static Website Hosting - -MyFSIO can serve S3 buckets as static websites via custom domain mappings. When a request arrives with a `Host` header matching a mapped domain, MyFSIO resolves the bucket and serves objects directly. - -### Enabling - -Set the environment variable: - -```bash -WEBSITE_HOSTING_ENABLED=true -``` - -When disabled, all website hosting endpoints return 400 and domain-based serving is skipped. - -### Configuration - -| Variable | Default | Description | -|----------|---------|-------------| -| `WEBSITE_HOSTING_ENABLED` | `false` | Master switch for website hosting | - -### Setting Up a Website - -**Step 1: Configure the bucket website settings** - -```bash -curl -X PUT "http://localhost:5000/my-site?website" \ - -H "Authorization: ..." \ - -d ' - - index.html - 404.html -' -``` - -- `IndexDocument` with `Suffix` is required (must not contain `/`) -- `ErrorDocument` is optional - -**Step 2: Map a domain to the bucket** - -```bash -curl -X POST "http://localhost:5000/admin/website-domains" \ - -H "Authorization: ..." \ - -H "Content-Type: application/json" \ - -d '{"domain": "example.com", "bucket": "my-site"}' -``` - -**Step 3: Point your domain to MyFSIO** - -For HTTP-only (direct access), point DNS to the MyFSIO host on port 5000. - -For HTTPS (recommended), use a reverse proxy. The critical requirement is passing the original `Host` header so MyFSIO can match the domain to a bucket. - -**nginx example:** - -```nginx -server { - server_name example.com; - listen 443 ssl; - - ssl_certificate /etc/ssl/certs/example.com.pem; - ssl_certificate_key /etc/ssl/private/example.com.key; - - location / { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -`proxy_set_header Host $host;` is required — without it, MyFSIO cannot match the incoming domain to a bucket. You do not need any path-based routing rules; MyFSIO handles all object resolution internally. - -### How Domain Routing Works - -1. A request arrives with `Host: example.com` -2. MyFSIO's `before_request` hook strips the port and looks up the domain in the `WebsiteDomainStore` -3. If a match is found, it loads the bucket's website config (index/error documents) -4. Object key resolution: - - `/` or trailing `/` → append `index_document` (e.g., `index.html`) - - `/path` → try exact match, then try `path/index_document` - - Not found → serve `error_document` with 404 status -5. If no domain match is found, the request falls through to normal S3 API / UI routing - -### Domain Mapping Admin API - -All endpoints require admin (`iam:*`) permissions. - -| Method | Route | Body | Description | -|--------|-------|------|-------------| -| `GET` | `/admin/website-domains` | — | List all mappings | -| `POST` | `/admin/website-domains` | `{"domain": "...", "bucket": "..."}` | Create mapping | -| `GET` | `/admin/website-domains/` | — | Get single mapping | -| `PUT` | `/admin/website-domains/` | `{"bucket": "..."}` | Update mapping | -| `DELETE` | `/admin/website-domains/` | — | Delete mapping | - -### Bucket Website API - -| Method | Route | Description | -|--------|-------|-------------| -| `PUT` | `/?website` | Set website config (XML body) | -| `GET` | `/?website` | Get website config (XML response) | -| `DELETE` | `/?website` | Remove website config | - -### Web UI - -- **Per-bucket config:** Bucket Details → Properties tab → "Static Website Hosting" card -- **Domain management:** Sidebar → "Domains" (visible when hosting is enabled and user is admin) +- `rust/myfsio-engine/crates/myfsio-server/src/lib.rs` +- `rust/myfsio-engine/crates/myfsio-server/src/handlers/` diff --git a/python/app/ui.py b/python/app/ui.py index 29ffd05..f43ddb2 100644 --- a/python/app/ui.py +++ b/python/app/ui.py @@ -225,10 +225,10 @@ def _policy_allows_public_read(policy: dict[str, Any]) -> bool: def _bucket_access_descriptor(policy: dict[str, Any] | None) -> tuple[str, str]: if not policy: - return ("IAM only", "text-bg-secondary") + return ("IAM only", "bg-secondary-subtle text-secondary-emphasis") if _policy_allows_public_read(policy): - return ("Public read", "text-bg-warning") - return ("Custom policy", "text-bg-info") + return ("Public read", "bg-warning-subtle text-warning-emphasis") + return ("Custom policy", "bg-info-subtle text-info-emphasis") def _current_principal(): diff --git a/python/templates/bucket_detail.html b/python/templates/bucket_detail.html index f913880..c3a168f 100644 --- a/python/templates/bucket_detail.html +++ b/python/templates/bucket_detail.html @@ -921,14 +921,14 @@
- Storage quota enabled + Storage quota active

{% if max_bytes is not none and max_objects is not none %} - Limited to {{ max_bytes | filesizeformat }} and {{ max_objects }} objects. + This bucket is limited to {{ max_bytes | filesizeformat }} storage and {{ max_objects }} objects. {% elif max_bytes is not none %} - Limited to {{ max_bytes | filesizeformat }} storage. + This bucket is limited to {{ max_bytes | filesizeformat }} storage. {% else %} - Limited to {{ max_objects }} objects. + This bucket is limited to {{ max_objects }} objects. {% endif %}

diff --git a/rust/myfsio-engine/Cargo.lock b/rust/myfsio-engine/Cargo.lock index c470a57..dd29eae 100644 --- a/rust/myfsio-engine/Cargo.lock +++ b/rust/myfsio-engine/Cargo.lock @@ -326,6 +326,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -1165,6 +1177,23 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "const-oid" version = "0.9.6" @@ -1442,6 +1471,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "duckdb" version = "1.10501.0" @@ -2137,7 +2172,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -2595,7 +2630,7 @@ dependencies = [ [[package]] name = "myfsio-auth" -version = "0.1.0" +version = "0.5.0" dependencies = [ "aes", "base64", @@ -2608,6 +2643,7 @@ dependencies = [ "parking_lot", "pbkdf2", "percent-encoding", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.9", @@ -2619,7 +2655,7 @@ dependencies = [ [[package]] name = "myfsio-common" -version = "0.1.0" +version = "0.5.0" dependencies = [ "chrono", "serde", @@ -2630,7 +2666,7 @@ dependencies = [ [[package]] name = "myfsio-crypto" -version = "0.1.0" +version = "0.5.0" dependencies = [ "aes-gcm", "base64", @@ -2651,8 +2687,9 @@ dependencies = [ [[package]] name = "myfsio-server" -version = "0.1.0" +version = "0.5.0" dependencies = [ + "aes-gcm", "async-trait", "aws-config", "aws-credential-types", @@ -2665,6 +2702,7 @@ dependencies = [ "clap", "cookie", "crc32fast", + "dotenvy", "duckdb", "futures", "http-body-util", @@ -2686,6 +2724,7 @@ dependencies = [ "serde", "serde_json", "subtle", + "sysinfo", "tempfile", "tera", "tokio", @@ -2699,7 +2738,7 @@ dependencies = [ [[package]] name = "myfsio-storage" -version = "0.1.0" +version = "0.5.0" dependencies = [ "chrono", "dashmap", @@ -2722,7 +2761,7 @@ dependencies = [ [[package]] name = "myfsio-xml" -version = "0.1.0" +version = "0.5.0" dependencies = [ "chrono", "myfsio-common", @@ -2730,6 +2769,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3222,6 +3270,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3905,6 +3973,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + [[package]] name = "tap" version = "1.0.1" @@ -4164,6 +4246,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags", "bytes", "futures-core", @@ -4569,6 +4652,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4578,19 +4677,58 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link", - "windows-result", + "windows-result 0.4.1", "windows-strings", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4602,6 +4740,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -4619,6 +4768,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/rust/myfsio-engine/Cargo.toml b/rust/myfsio-engine/Cargo.toml index 5e049b7..6f3bde5 100644 --- a/rust/myfsio-engine/Cargo.toml +++ b/rust/myfsio-engine/Cargo.toml @@ -9,11 +9,15 @@ members = [ "crates/myfsio-server", ] +[workspace.package] +version = "0.4.3" +edition = "2021" + [workspace.dependencies] tokio = { version = "1", features = ["full"] } axum = { version = "0.8" } tower = { version = "0.5" } -tower-http = { version = "0.6", features = ["cors", "trace", "fs"] } +tower-http = { version = "0.6", features = ["cors", "trace", "fs", "compression-gzip"] } hyper = { version = "1" } bytes = "1" serde = { version = "1", features = ["derive"] } @@ -54,3 +58,4 @@ tera = "1" cookie = "0.18" subtle = "2" clap = { version = "4", features = ["derive"] } +dotenvy = "0.15" diff --git a/rust/myfsio-engine/crates/myfsio-auth/Cargo.toml b/rust/myfsio-engine/crates/myfsio-auth/Cargo.toml index 384427a..7622bf9 100644 --- a/rust/myfsio-engine/crates/myfsio-auth/Cargo.toml +++ b/rust/myfsio-engine/crates/myfsio-auth/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "myfsio-auth" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true [dependencies] myfsio-common = { path = "../myfsio-common" } @@ -12,6 +12,7 @@ aes = { workspace = true } cbc = { workspace = true } base64 = { workspace = true } pbkdf2 = "0.12" +rand = "0.8" lru = { workspace = true } parking_lot = { workspace = true } percent-encoding = { workspace = true } diff --git a/rust/myfsio-engine/crates/myfsio-auth/src/fernet.rs b/rust/myfsio-engine/crates/myfsio-auth/src/fernet.rs index ba7fb64..1b2e129 100644 --- a/rust/myfsio-engine/crates/myfsio-auth/src/fernet.rs +++ b/rust/myfsio-engine/crates/myfsio-auth/src/fernet.rs @@ -1,9 +1,11 @@ -use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; +use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use base64::{engine::general_purpose::URL_SAFE, Engine}; use hmac::{Hmac, Mac}; +use rand::RngCore; use sha2::Sha256; type Aes128CbcDec = cbc::Decryptor; +type Aes128CbcEnc = cbc::Encryptor; type HmacSha256 = Hmac; pub fn derive_fernet_key(secret: &str) -> String { @@ -44,8 +46,7 @@ pub fn decrypt(key_b64: &str, token: &str) -> Result, &'static str> { let payload = &token_bytes[..hmac_offset]; let expected_hmac = &token_bytes[hmac_offset..]; - let mut mac = - HmacSha256::new_from_slice(signing_key).map_err(|_| "hmac key error")?; + let mut mac = HmacSha256::new_from_slice(signing_key).map_err(|_| "hmac key error")?; mac.update(payload); mac.verify_slice(expected_hmac) .map_err(|_| "HMAC verification failed")?; @@ -60,6 +61,43 @@ pub fn decrypt(key_b64: &str, token: &str) -> Result, &'static str> { Ok(plaintext) } +pub fn encrypt(key_b64: &str, plaintext: &[u8]) -> Result { + let key_bytes = URL_SAFE + .decode(key_b64) + .map_err(|_| "invalid fernet key base64")?; + if key_bytes.len() != 32 { + return Err("fernet key must be 32 bytes"); + } + + let signing_key = &key_bytes[..16]; + let encryption_key = &key_bytes[16..]; + + let mut iv = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut iv); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| "system time error")? + .as_secs(); + + let ciphertext = Aes128CbcEnc::new(encryption_key.into(), (&iv).into()) + .encrypt_padded_vec_mut::(plaintext); + + let mut payload = Vec::with_capacity(1 + 8 + 16 + ciphertext.len()); + payload.push(0x80); + payload.extend_from_slice(×tamp.to_be_bytes()); + payload.extend_from_slice(&iv); + payload.extend_from_slice(&ciphertext); + + let mut mac = HmacSha256::new_from_slice(signing_key).map_err(|_| "hmac key error")?; + mac.update(&payload); + let tag = mac.finalize().into_bytes(); + + let mut token_bytes = payload; + token_bytes.extend_from_slice(&tag); + Ok(URL_SAFE.encode(&token_bytes)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/myfsio-engine/crates/myfsio-auth/src/iam.rs b/rust/myfsio-engine/crates/myfsio-auth/src/iam.rs index 34f41bd..a7f45b9 100644 --- a/rust/myfsio-engine/crates/myfsio-auth/src/iam.rs +++ b/rust/myfsio-engine/crates/myfsio-auth/src/iam.rs @@ -69,7 +69,10 @@ impl RawIamUser { } } let display_name = self.display_name.unwrap_or_else(|| { - access_keys.first().map(|k| k.access_key.clone()).unwrap_or_else(|| "unknown".to_string()) + access_keys + .first() + .map(|k| k.access_key.clone()) + .unwrap_or_else(|| "unknown".to_string()) }); let user_id = self.user_id.unwrap_or_else(|| { format!("u-{}", display_name.to_ascii_lowercase().replace(' ', "-")) @@ -173,7 +176,7 @@ impl IamService { (None, Some(_)) => true, (Some(old), Some(new)) => old != new, (Some(_), None) => true, - (None, None) => state.key_secrets.is_empty(), + (None, None) => false, } }; @@ -188,7 +191,11 @@ impl IamService { let content = match std::fs::read_to_string(&self.config_path) { Ok(c) => c, Err(e) => { - tracing::warn!("Failed to read IAM config {}: {}", self.config_path.display(), e); + tracing::warn!( + "Failed to read IAM config {}: {}", + self.config_path.display(), + e + ); return; } }; @@ -205,7 +212,10 @@ impl IamService { } }, Err(e) => { - tracing::error!("Failed to decrypt IAM config: {}. SECRET_KEY may have changed.", e); + tracing::error!( + "Failed to decrypt IAM config: {}. SECRET_KEY may have changed.", + e + ); return; } }, @@ -226,7 +236,11 @@ impl IamService { } }; - let users: Vec = raw_config.users.into_iter().map(|u| u.normalize()).collect(); + let users: Vec = raw_config + .users + .into_iter() + .map(|u| u.normalize()) + .collect(); let mut key_secrets = HashMap::new(); let mut key_index = HashMap::new(); @@ -254,9 +268,11 @@ impl IamService { state.file_mtime = file_mtime; state.last_check = Instant::now(); - tracing::info!("IAM config reloaded: {} users, {} keys", + tracing::info!( + "IAM config reloaded: {} users, {} keys", users.len(), - state.key_secrets.len()); + state.key_secrets.len() + ); } pub fn get_secret_key(&self, access_key: &str) -> Option { @@ -308,9 +324,10 @@ impl IamService { } } - let is_admin = user.policies.iter().any(|p| { - p.bucket == "*" && p.actions.iter().any(|a| a == "*") - }); + let is_admin = user + .policies + .iter() + .any(|p| p.bucket == "*" && p.actions.iter().any(|a| a == "*")); Some(Principal::new( access_key.to_string(), @@ -341,10 +358,7 @@ impl IamService { return true; } - let normalized_bucket = bucket_name - .unwrap_or("*") - .trim() - .to_ascii_lowercase(); + let normalized_bucket = bucket_name.unwrap_or("*").trim().to_ascii_lowercase(); let normalized_action = action.trim().to_ascii_lowercase(); let state = self.state.read(); @@ -383,6 +397,46 @@ impl IamService { false } + pub fn export_config(&self, mask_secrets: bool) -> serde_json::Value { + self.reload_if_needed(); + let state = self.state.read(); + let users: Vec = state + .user_records + .values() + .map(|u| { + let access_keys: Vec = u + .access_keys + .iter() + .map(|k| { + let secret = if mask_secrets { + "***".to_string() + } else { + k.secret_key.clone() + }; + serde_json::json!({ + "access_key": k.access_key, + "secret_key": secret, + "status": k.status, + "created_at": k.created_at, + }) + }) + .collect(); + serde_json::json!({ + "user_id": u.user_id, + "display_name": u.display_name, + "enabled": u.enabled, + "expires_at": u.expires_at, + "access_keys": access_keys, + "policies": u.policies, + }) + }) + .collect(); + serde_json::json!({ + "version": 2, + "users": users, + }) + } + pub async fn list_users(&self) -> Vec { self.reload_if_needed(); let state = self.state.read(); @@ -411,12 +465,12 @@ impl IamService { self.reload_if_needed(); let state = self.state.read(); - let user = state - .user_records - .get(identifier) - .or_else(|| { - state.key_index.get(identifier).and_then(|uid| state.user_records.get(uid)) - })?; + let user = state.user_records.get(identifier).or_else(|| { + state + .key_index + .get(identifier) + .and_then(|uid| state.user_records.get(uid)) + })?; Some(serde_json::json!({ "user_id": user.user_id, @@ -449,8 +503,7 @@ impl IamService { .users .iter_mut() .find(|u| { - u.user_id == identifier - || u.access_keys.iter().any(|k| k.access_key == identifier) + u.user_id == identifier || u.access_keys.iter().any(|k| k.access_key == identifier) }) .ok_or_else(|| "User not found".to_string())?; @@ -468,12 +521,12 @@ impl IamService { pub fn get_user_policies(&self, identifier: &str) -> Option> { self.reload_if_needed(); let state = self.state.read(); - let user = state - .user_records - .get(identifier) - .or_else(|| { - state.key_index.get(identifier).and_then(|uid| state.user_records.get(uid)) - })?; + let user = state.user_records.get(identifier).or_else(|| { + state + .key_index + .get(identifier) + .and_then(|uid| state.user_records.get(uid)) + })?; Some( user.policies .iter() @@ -496,8 +549,7 @@ impl IamService { .users .iter_mut() .find(|u| { - u.user_id == identifier - || u.access_keys.iter().any(|k| k.access_key == identifier) + u.user_id == identifier || u.access_keys.iter().any(|k| k.access_key == identifier) }) .ok_or_else(|| format!("User '{}' not found", identifier))?; @@ -557,6 +609,178 @@ impl IamService { self.reload(); Ok(()) } + + fn load_config(&self) -> Result { + let content = std::fs::read_to_string(&self.config_path) + .map_err(|e| format!("Failed to read IAM config: {}", e))?; + let raw_text = if content.starts_with("MYFSIO_IAM_ENC:") { + let encrypted_token = &content["MYFSIO_IAM_ENC:".len()..]; + let key = self.fernet_key.as_ref().ok_or_else(|| { + "IAM config is encrypted but no SECRET_KEY configured".to_string() + })?; + let plaintext = crate::fernet::decrypt(key, encrypted_token.trim()) + .map_err(|e| format!("Failed to decrypt IAM config: {}", e))?; + String::from_utf8(plaintext) + .map_err(|e| format!("Decrypted IAM config not UTF-8: {}", e))? + } else { + content + }; + let raw: RawIamConfig = serde_json::from_str(&raw_text) + .map_err(|e| format!("Failed to parse IAM config: {}", e))?; + Ok(IamConfig { + version: 2, + users: raw.users.into_iter().map(|u| u.normalize()).collect(), + }) + } + + fn save_config(&self, config: &IamConfig) -> Result<(), String> { + let json = serde_json::to_string_pretty(config) + .map_err(|e| format!("Failed to serialize IAM config: {}", e))?; + let payload = if let Some(key) = &self.fernet_key { + let token = crate::fernet::encrypt(key, json.as_bytes()) + .map_err(|e| format!("Failed to encrypt IAM config: {}", e))?; + format!("MYFSIO_IAM_ENC:{}", token) + } else { + json + }; + std::fs::write(&self.config_path, payload) + .map_err(|e| format!("Failed to write IAM config: {}", e))?; + self.reload(); + Ok(()) + } + + pub fn create_user( + &self, + display_name: &str, + policies: Option>, + access_key: Option, + secret_key: Option, + expires_at: Option, + ) -> Result { + let mut config = self.load_config()?; + + let new_ak = access_key + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| format!("AK{}", uuid::Uuid::new_v4().simple())); + let new_sk = secret_key + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| format!("SK{}", uuid::Uuid::new_v4().simple())); + + if config + .users + .iter() + .any(|u| u.access_keys.iter().any(|k| k.access_key == new_ak)) + { + return Err(format!("Access key '{}' already exists", new_ak)); + } + + let user_id = format!("u-{}", uuid::Uuid::new_v4().simple()); + let resolved_policies = policies.unwrap_or_else(|| { + vec![IamPolicy { + bucket: "*".to_string(), + actions: vec!["*".to_string()], + prefix: "*".to_string(), + }] + }); + + let user = IamUser { + user_id: user_id.clone(), + display_name: display_name.to_string(), + enabled: true, + expires_at, + access_keys: vec![AccessKey { + access_key: new_ak.clone(), + secret_key: new_sk.clone(), + status: "active".to_string(), + created_at: Some(chrono::Utc::now().to_rfc3339()), + }], + policies: resolved_policies, + }; + config.users.push(user); + + self.save_config(&config)?; + Ok(serde_json::json!({ + "user_id": user_id, + "access_key": new_ak, + "secret_key": new_sk, + "display_name": display_name, + })) + } + + pub fn delete_user(&self, identifier: &str) -> Result<(), String> { + let mut config = self.load_config()?; + let before = config.users.len(); + config.users.retain(|u| { + u.user_id != identifier && !u.access_keys.iter().any(|k| k.access_key == identifier) + }); + if config.users.len() == before { + return Err(format!("User '{}' not found", identifier)); + } + self.save_config(&config) + } + + pub fn update_user( + &self, + identifier: &str, + display_name: Option, + expires_at: Option>, + ) -> Result<(), String> { + let mut config = self.load_config()?; + let user = config + .users + .iter_mut() + .find(|u| { + u.user_id == identifier || u.access_keys.iter().any(|k| k.access_key == identifier) + }) + .ok_or_else(|| format!("User '{}' not found", identifier))?; + if let Some(name) = display_name { + user.display_name = name; + } + if let Some(exp) = expires_at { + user.expires_at = exp; + } + self.save_config(&config) + } + + pub fn update_user_policies( + &self, + identifier: &str, + policies: Vec, + ) -> Result<(), String> { + let mut config = self.load_config()?; + let user = config + .users + .iter_mut() + .find(|u| { + u.user_id == identifier || u.access_keys.iter().any(|k| k.access_key == identifier) + }) + .ok_or_else(|| format!("User '{}' not found", identifier))?; + user.policies = policies; + self.save_config(&config) + } + + pub fn rotate_secret(&self, identifier: &str) -> Result { + let mut config = self.load_config()?; + let user = config + .users + .iter_mut() + .find(|u| { + u.user_id == identifier || u.access_keys.iter().any(|k| k.access_key == identifier) + }) + .ok_or_else(|| format!("User '{}' not found", identifier))?; + let key = user + .access_keys + .first_mut() + .ok_or_else(|| "User has no access keys".to_string())?; + let new_sk = format!("SK{}", uuid::Uuid::new_v4().simple()); + key.secret_key = new_sk.clone(); + let ak = key.access_key.clone(); + self.save_config(&config)?; + Ok(serde_json::json!({ + "access_key": ak, + "secret_key": new_sk, + })) + } } fn bucket_matches(policy_bucket: &str, bucket: &str) -> bool { @@ -622,10 +846,7 @@ mod tests { let svc = IamService::new(tmp.path().to_path_buf()); let secret = svc.get_secret_key("AKIAIOSFODNN7EXAMPLE"); - assert_eq!( - secret.unwrap(), - "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - ); + assert_eq!(secret.unwrap(), "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); } #[test] @@ -664,7 +885,9 @@ mod tests { tmp.flush().unwrap(); let svc = IamService::new(tmp.path().to_path_buf()); - assert!(svc.authenticate("AKIAIOSFODNN7EXAMPLE", "wrongsecret").is_none()); + assert!(svc + .authenticate("AKIAIOSFODNN7EXAMPLE", "wrongsecret") + .is_none()); } #[test] @@ -784,29 +1007,9 @@ mod tests { let svc = IamService::new(tmp.path().to_path_buf()); let principal = svc.get_principal("READER_KEY").unwrap(); - assert!(svc.authorize( - &principal, - Some("docs"), - "read", - Some("reports/2026.csv"), - )); - assert!(!svc.authorize( - &principal, - Some("docs"), - "write", - Some("reports/2026.csv"), - )); - assert!(!svc.authorize( - &principal, - Some("docs"), - "read", - Some("private/2026.csv"), - )); - assert!(!svc.authorize( - &principal, - Some("other"), - "read", - Some("reports/2026.csv"), - )); + assert!(svc.authorize(&principal, Some("docs"), "read", Some("reports/2026.csv"),)); + assert!(!svc.authorize(&principal, Some("docs"), "write", Some("reports/2026.csv"),)); + assert!(!svc.authorize(&principal, Some("docs"), "read", Some("private/2026.csv"),)); + assert!(!svc.authorize(&principal, Some("other"), "read", Some("reports/2026.csv"),)); } } diff --git a/rust/myfsio-engine/crates/myfsio-auth/src/lib.rs b/rust/myfsio-engine/crates/myfsio-auth/src/lib.rs index 083404f..0d15c37 100644 --- a/rust/myfsio-engine/crates/myfsio-auth/src/lib.rs +++ b/rust/myfsio-engine/crates/myfsio-auth/src/lib.rs @@ -1,4 +1,4 @@ -pub mod sigv4; -pub mod principal; -pub mod iam; mod fernet; +pub mod iam; +pub mod principal; +pub mod sigv4; diff --git a/rust/myfsio-engine/crates/myfsio-auth/src/sigv4.rs b/rust/myfsio-engine/crates/myfsio-auth/src/sigv4.rs index b515ee0..5ef6997 100644 --- a/rust/myfsio-engine/crates/myfsio-auth/src/sigv4.rs +++ b/rust/myfsio-engine/crates/myfsio-auth/src/sigv4.rs @@ -64,7 +64,10 @@ pub fn derive_signing_key_cached( } } - let k_date = hmac_sha256(format!("AWS4{}", secret_key).as_bytes(), date_stamp.as_bytes()); + let k_date = hmac_sha256( + format!("AWS4{}", secret_key).as_bytes(), + date_stamp.as_bytes(), + ); let k_region = hmac_sha256(&k_date, region.as_bytes()); let k_service = hmac_sha256(&k_region, service.as_bytes()); let k_signing = hmac_sha256(&k_service, b"aws4_request"); @@ -134,7 +137,11 @@ pub fn verify_sigv4_signature( let canonical_request = format!( "{}\n{}\n{}\n{}\n{}\n{}", - method, canonical_uri, canonical_query_string, canonical_headers, signed_headers_str, + method, + canonical_uri, + canonical_query_string, + canonical_headers, + signed_headers_str, payload_hash ); @@ -197,7 +204,12 @@ mod tests { #[test] fn test_derive_signing_key() { - let key = derive_signing_key("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "20130524", "us-east-1", "s3"); + let key = derive_signing_key( + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "20130524", + "us-east-1", + "s3", + ); assert_eq!(key.len(), 32); } @@ -217,7 +229,11 @@ mod tests { #[test] fn test_build_string_to_sign() { - let result = build_string_to_sign("20130524T000000Z", "20130524/us-east-1/s3/aws4_request", "GET\n/\n\nhost:example.com\n\nhost\nUNSIGNED-PAYLOAD"); + let result = build_string_to_sign( + "20130524T000000Z", + "20130524/us-east-1/s3/aws4_request", + "GET\n/\n\nhost:example.com\n\nhost\nUNSIGNED-PAYLOAD", + ); assert!(result.starts_with("AWS4-HMAC-SHA256\n")); assert!(result.contains("20130524T000000Z")); } @@ -239,8 +255,13 @@ mod tests { let signing_key = derive_signing_key(secret, date_stamp, region, service); - let canonical_request = "GET\n/\n\nhost:examplebucket.s3.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD"; - let string_to_sign = build_string_to_sign(amz_date, &format!("{}/{}/{}/aws4_request", date_stamp, region, service), canonical_request); + let canonical_request = + "GET\n/\n\nhost:examplebucket.s3.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD"; + let string_to_sign = build_string_to_sign( + amz_date, + &format!("{}/{}/{}/aws4_request", date_stamp, region, service), + canonical_request, + ); let signature = compute_signature(&signing_key, &string_to_sign); @@ -249,7 +270,10 @@ mod tests { "/", &[], "host", - &[("host".to_string(), "examplebucket.s3.amazonaws.com".to_string())], + &[( + "host".to_string(), + "examplebucket.s3.amazonaws.com".to_string(), + )], "UNSIGNED-PAYLOAD", amz_date, date_stamp, diff --git a/rust/myfsio-engine/crates/myfsio-common/Cargo.toml b/rust/myfsio-engine/crates/myfsio-common/Cargo.toml index e140021..29774bc 100644 --- a/rust/myfsio-engine/crates/myfsio-common/Cargo.toml +++ b/rust/myfsio-engine/crates/myfsio-common/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "myfsio-common" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true [dependencies] thiserror = { workspace = true } diff --git a/rust/myfsio-engine/crates/myfsio-crypto/Cargo.toml b/rust/myfsio-engine/crates/myfsio-crypto/Cargo.toml index 45cecc7..09be255 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/Cargo.toml +++ b/rust/myfsio-engine/crates/myfsio-crypto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "myfsio-crypto" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true [dependencies] myfsio-common = { path = "../myfsio-common" } diff --git a/rust/myfsio-engine/crates/myfsio-crypto/src/aes_gcm.rs b/rust/myfsio-engine/crates/myfsio-crypto/src/aes_gcm.rs index dc7a346..b349de3 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/src/aes_gcm.rs +++ b/rust/myfsio-engine/crates/myfsio-crypto/src/aes_gcm.rs @@ -193,7 +193,10 @@ mod tests { let decrypted = dir.path().join("decrypted.bin"); let data = b"Hello, this is a test of AES-256-GCM chunked encryption!"; - std::fs::File::create(&input).unwrap().write_all(data).unwrap(); + std::fs::File::create(&input) + .unwrap() + .write_all(data) + .unwrap(); let key = [0x42u8; 32]; let nonce = [0x01u8; 12]; @@ -212,9 +215,18 @@ mod tests { fn test_invalid_key_size() { let dir = tempfile::tempdir().unwrap(); let input = dir.path().join("input.bin"); - std::fs::File::create(&input).unwrap().write_all(b"test").unwrap(); + std::fs::File::create(&input) + .unwrap() + .write_all(b"test") + .unwrap(); - let result = encrypt_stream_chunked(&input, &dir.path().join("out"), &[0u8; 16], &[0u8; 12], None); + let result = encrypt_stream_chunked( + &input, + &dir.path().join("out"), + &[0u8; 16], + &[0u8; 12], + None, + ); assert!(matches!(result, Err(CryptoError::InvalidKeySize(16)))); } @@ -225,7 +237,10 @@ mod tests { let encrypted = dir.path().join("encrypted.bin"); let decrypted = dir.path().join("decrypted.bin"); - std::fs::File::create(&input).unwrap().write_all(b"secret data").unwrap(); + std::fs::File::create(&input) + .unwrap() + .write_all(b"secret data") + .unwrap(); let key = [0x42u8; 32]; let nonce = [0x01u8; 12]; diff --git a/rust/myfsio-engine/crates/myfsio-crypto/src/encryption.rs b/rust/myfsio-engine/crates/myfsio-crypto/src/encryption.rs index 2a78f3c..37a5193 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/src/encryption.rs +++ b/rust/myfsio-engine/crates/myfsio-crypto/src/encryption.rs @@ -4,9 +4,7 @@ use rand::RngCore; use std::collections::HashMap; use std::path::Path; -use crate::aes_gcm::{ - encrypt_stream_chunked, decrypt_stream_chunked, CryptoError, -}; +use crate::aes_gcm::{decrypt_stream_chunked, encrypt_stream_chunked, CryptoError}; use crate::kms::KmsService; #[derive(Debug, Clone, PartialEq)] @@ -172,15 +170,14 @@ impl EncryptionService { let ciphertext = kms.encrypt_data(kid, &data_key).await?; (Some(B64.encode(&ciphertext)), Some(kid.clone())) } - SseAlgorithm::CustomerProvided => { - (None, None) - } + SseAlgorithm::CustomerProvided => (None, None), }; let actual_key = if ctx.algorithm == SseAlgorithm::CustomerProvided { - let ck = ctx.customer_key.as_ref().ok_or_else(|| { - CryptoError::EncryptionFailed("No customer key provided".into()) - })?; + let ck = ctx + .customer_key + .as_ref() + .ok_or_else(|| CryptoError::EncryptionFailed("No customer key provided".into()))?; if ck.len() != 32 { return Err(CryptoError::InvalidKeySize(ck.len())); } @@ -195,11 +192,9 @@ impl EncryptionService { let op = output_path.to_owned(); let ak = actual_key; let n = nonce; - tokio::task::spawn_blocking(move || { - encrypt_stream_chunked(&ip, &op, &ak, &n, None) - }) - .await - .map_err(|e| CryptoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))??; + tokio::task::spawn_blocking(move || encrypt_stream_chunked(&ip, &op, &ak, &n, None)) + .await + .map_err(|e| CryptoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))??; Ok(EncryptionMetadata { algorithm: ctx.algorithm.as_str().to_string(), @@ -216,9 +211,9 @@ impl EncryptionService { enc_meta: &EncryptionMetadata, customer_key: Option<&[u8]>, ) -> Result<(), CryptoError> { - let nonce_bytes = B64.decode(&enc_meta.nonce).map_err(|e| { - CryptoError::EncryptionFailed(format!("Bad nonce encoding: {}", e)) - })?; + let nonce_bytes = B64 + .decode(&enc_meta.nonce) + .map_err(|e| CryptoError::EncryptionFailed(format!("Bad nonce encoding: {}", e)))?; if nonce_bytes.len() != 12 { return Err(CryptoError::InvalidNonceSize(nonce_bytes.len())); } @@ -262,11 +257,9 @@ impl EncryptionService { let ip = input_path.to_owned(); let op = output_path.to_owned(); let nb: [u8; 12] = nonce_bytes.try_into().unwrap(); - tokio::task::spawn_blocking(move || { - decrypt_stream_chunked(&ip, &op, &data_key, &nb) - }) - .await - .map_err(|e| CryptoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))??; + tokio::task::spawn_blocking(move || decrypt_stream_chunked(&ip, &op, &data_key, &nb)) + .await + .map_err(|e| CryptoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))??; Ok(()) } @@ -298,7 +291,10 @@ mod tests { let decrypted = dir.path().join("dec.bin"); let data = b"SSE-S3 encrypted content for testing!"; - std::fs::File::create(&input).unwrap().write_all(data).unwrap(); + std::fs::File::create(&input) + .unwrap() + .write_all(data) + .unwrap(); let svc = EncryptionService::new(test_master_key(), None); @@ -328,7 +324,10 @@ mod tests { let decrypted = dir.path().join("dec.bin"); let data = b"SSE-C encrypted content!"; - std::fs::File::create(&input).unwrap().write_all(data).unwrap(); + std::fs::File::create(&input) + .unwrap() + .write_all(data) + .unwrap(); let customer_key = [0xBBu8; 32]; let svc = EncryptionService::new(test_master_key(), None); @@ -369,7 +368,10 @@ mod tests { fn test_is_encrypted() { let mut meta = HashMap::new(); assert!(!EncryptionMetadata::is_encrypted(&meta)); - meta.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string()); + meta.insert( + "x-amz-server-side-encryption".to_string(), + "AES256".to_string(), + ); assert!(EncryptionMetadata::is_encrypted(&meta)); } } diff --git a/rust/myfsio-engine/crates/myfsio-crypto/src/hashing.rs b/rust/myfsio-engine/crates/myfsio-crypto/src/hashing.rs index 88d4f54..31e2277 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/src/hashing.rs +++ b/rust/myfsio-engine/crates/myfsio-crypto/src/hashing.rs @@ -99,7 +99,10 @@ mod tests { #[test] fn test_sha256_bytes() { let hash = sha256_bytes(b"hello"); - assert_eq!(hash, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + assert_eq!( + hash, + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + ); } #[test] @@ -118,7 +121,10 @@ mod tests { tmp.flush().unwrap(); let (md5, sha) = md5_sha256_file(tmp.path()).unwrap(); assert_eq!(md5, "5d41402abc4b2a76b9719d911017c592"); - assert_eq!(sha, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + assert_eq!( + sha, + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + ); } #[tokio::test] diff --git a/rust/myfsio-engine/crates/myfsio-crypto/src/kms.rs b/rust/myfsio-engine/crates/myfsio-crypto/src/kms.rs index 790c9fe..afdd8d3 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/src/kms.rs +++ b/rust/myfsio-engine/crates/myfsio-crypto/src/kms.rs @@ -132,9 +132,7 @@ impl KmsService { async fn save(&self) -> Result<(), CryptoError> { let keys = self.keys.read().await; - let store = KmsStore { - keys: keys.clone(), - }; + let store = KmsStore { keys: keys.clone() }; let json = serde_json::to_string_pretty(&store) .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; std::fs::write(&self.keys_path, json).map_err(CryptoError::Io)?; diff --git a/rust/myfsio-engine/crates/myfsio-crypto/src/lib.rs b/rust/myfsio-engine/crates/myfsio-crypto/src/lib.rs index 402bb7a..7f57a35 100644 --- a/rust/myfsio-engine/crates/myfsio-crypto/src/lib.rs +++ b/rust/myfsio-engine/crates/myfsio-crypto/src/lib.rs @@ -1,4 +1,4 @@ -pub mod hashing; pub mod aes_gcm; -pub mod kms; pub mod encryption; +pub mod hashing; +pub mod kms; diff --git a/rust/myfsio-engine/crates/myfsio-server/Cargo.toml b/rust/myfsio-engine/crates/myfsio-server/Cargo.toml index 58aa363..6a1092b 100644 --- a/rust/myfsio-engine/crates/myfsio-server/Cargo.toml +++ b/rust/myfsio-engine/crates/myfsio-server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "myfsio-server" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true [dependencies] myfsio-common = { path = "../myfsio-common" } @@ -45,6 +45,9 @@ tera = { workspace = true } cookie = { workspace = true } subtle = { workspace = true } clap = { workspace = true } +dotenvy = { workspace = true } +sysinfo = "0.32" +aes-gcm = { workspace = true } [dev-dependencies] tempfile = "3" diff --git a/rust/myfsio-engine/crates/myfsio-server/src/config.rs b/rust/myfsio-engine/crates/myfsio-server/src/config.rs index 009fa1a..6fffe74 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/config.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/config.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; #[derive(Debug, Clone)] pub struct ServerConfig { pub bind_addr: SocketAddr, + pub ui_bind_addr: SocketAddr, pub storage_root: PathBuf, pub region: String, pub iam_config_path: PathBuf, @@ -16,6 +17,11 @@ pub struct ServerConfig { pub gc_enabled: bool, pub integrity_enabled: bool, pub metrics_enabled: bool, + pub metrics_history_enabled: bool, + pub metrics_interval_minutes: u64, + pub metrics_retention_hours: u64, + pub metrics_history_interval_minutes: u64, + pub metrics_history_retention_hours: u64, pub lifecycle_enabled: bool, pub website_hosting_enabled: bool, pub replication_connect_timeout_secs: u64, @@ -42,22 +48,28 @@ impl ServerConfig { .unwrap_or_else(|_| "5000".to_string()) .parse() .unwrap_or(5000); - let storage_root = std::env::var("STORAGE_ROOT") - .unwrap_or_else(|_| "./data".to_string()); - let region = std::env::var("AWS_REGION") - .unwrap_or_else(|_| "us-east-1".to_string()); + let ui_port: u16 = std::env::var("UI_PORT") + .unwrap_or_else(|_| "5100".to_string()) + .parse() + .unwrap_or(5100); + let storage_root = std::env::var("STORAGE_ROOT").unwrap_or_else(|_| "./data".to_string()); + let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string()); let storage_path = PathBuf::from(&storage_root); let iam_config_path = std::env::var("IAM_CONFIG") .map(PathBuf::from) .unwrap_or_else(|_| { - storage_path.join(".myfsio.sys").join("config").join("iam.json") + storage_path + .join(".myfsio.sys") + .join("config") + .join("iam.json") }); - let sigv4_timestamp_tolerance_secs: u64 = std::env::var("SIGV4_TIMESTAMP_TOLERANCE_SECONDS") - .unwrap_or_else(|_| "900".to_string()) - .parse() - .unwrap_or(900); + let sigv4_timestamp_tolerance_secs: u64 = + std::env::var("SIGV4_TIMESTAMP_TOLERANCE_SECONDS") + .unwrap_or_else(|_| "900".to_string()) + .parse() + .unwrap_or(900); let presigned_url_min_expiry: u64 = std::env::var("PRESIGNED_URL_MIN_EXPIRY_SECONDS") .unwrap_or_else(|_| "1".to_string()) @@ -78,40 +90,60 @@ impl ServerConfig { .join(".myfsio.sys") .join("config") .join(".secret"); - std::fs::read_to_string(&secret_file).ok().map(|s| s.trim().to_string()) + std::fs::read_to_string(&secret_file) + .ok() + .map(|s| s.trim().to_string()) } } }; let encryption_enabled = std::env::var("ENCRYPTION_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let kms_enabled = std::env::var("KMS_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let gc_enabled = std::env::var("GC_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let integrity_enabled = std::env::var("INTEGRITY_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let metrics_enabled = std::env::var("OPERATION_METRICS_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; + + let metrics_history_enabled = std::env::var("METRICS_HISTORY_ENABLED") + .unwrap_or_else(|_| "false".to_string()) + .to_lowercase() + == "true"; + + let metrics_interval_minutes = parse_u64_env("OPERATION_METRICS_INTERVAL_MINUTES", 5); + let metrics_retention_hours = parse_u64_env("OPERATION_METRICS_RETENTION_HOURS", 24); + let metrics_history_interval_minutes = parse_u64_env("METRICS_HISTORY_INTERVAL_MINUTES", 5); + let metrics_history_retention_hours = parse_u64_env("METRICS_HISTORY_RETENTION_HOURS", 24); let lifecycle_enabled = std::env::var("LIFECYCLE_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let website_hosting_enabled = std::env::var("WEBSITE_HOSTING_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; - let replication_connect_timeout_secs = parse_u64_env("REPLICATION_CONNECT_TIMEOUT_SECONDS", 5); + let replication_connect_timeout_secs = + parse_u64_env("REPLICATION_CONNECT_TIMEOUT_SECONDS", 5); let replication_read_timeout_secs = parse_u64_env("REPLICATION_READ_TIMEOUT_SECONDS", 30); let replication_max_retries = parse_u64_env("REPLICATION_MAX_RETRIES", 2) as u32; let replication_streaming_threshold_bytes = @@ -121,20 +153,23 @@ impl ServerConfig { let site_sync_enabled = std::env::var("SITE_SYNC_ENABLED") .unwrap_or_else(|_| "false".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let site_sync_interval_secs = parse_u64_env("SITE_SYNC_INTERVAL_SECONDS", 60); let site_sync_batch_size = parse_u64_env("SITE_SYNC_BATCH_SIZE", 100) as usize; let site_sync_connect_timeout_secs = parse_u64_env("SITE_SYNC_CONNECT_TIMEOUT_SECONDS", 10); let site_sync_read_timeout_secs = parse_u64_env("SITE_SYNC_READ_TIMEOUT_SECONDS", 120); let site_sync_max_retries = parse_u64_env("SITE_SYNC_MAX_RETRIES", 2) as u32; - let site_sync_clock_skew_tolerance: f64 = std::env::var("SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(1.0); + let site_sync_clock_skew_tolerance: f64 = + std::env::var("SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(1.0); let ui_enabled = std::env::var("UI_ENABLED") .unwrap_or_else(|_| "true".to_string()) - .to_lowercase() == "true"; + .to_lowercase() + == "true"; let templates_dir = std::env::var("TEMPLATES_DIR") .map(PathBuf::from) .unwrap_or_else(|_| default_templates_dir()); @@ -142,8 +177,10 @@ impl ServerConfig { .map(PathBuf::from) .unwrap_or_else(|_| default_static_dir()); + let host_ip: std::net::IpAddr = host.parse().unwrap(); Self { - bind_addr: SocketAddr::new(host.parse().unwrap(), port), + bind_addr: SocketAddr::new(host_ip, port), + ui_bind_addr: SocketAddr::new(host_ip, ui_port), storage_root: storage_path, region, iam_config_path, @@ -156,6 +193,11 @@ impl ServerConfig { gc_enabled, integrity_enabled, metrics_enabled, + metrics_history_enabled, + metrics_interval_minutes, + metrics_retention_hours, + metrics_history_interval_minutes, + metrics_history_retention_hours, lifecycle_enabled, website_hosting_enabled, replication_connect_timeout_secs, diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/admin.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/admin.rs index e09f83f..8a08e2b 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/admin.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/admin.rs @@ -26,15 +26,31 @@ fn json_error(code: &str, message: &str, status: StatusCode) -> Response { ) } +fn push_issue(result: &mut serde_json::Value, issue: serde_json::Value) { + if let Some(items) = result + .get_mut("issues") + .and_then(|value| value.as_array_mut()) + { + items.push(issue); + } +} + fn require_admin(principal: &Principal) -> Option { if !principal.is_admin { - return Some(json_error("AccessDenied", "Admin access required", StatusCode::FORBIDDEN)); + return Some(json_error( + "AccessDenied", + "Admin access required", + StatusCode::FORBIDDEN, + )); } None } async fn read_json_body(body: Body) -> Option { - let bytes = http_body_util::BodyExt::collect(body).await.ok()?.to_bytes(); + let bytes = http_body_util::BodyExt::collect(body) + .await + .ok()? + .to_bytes(); serde_json::from_slice(&bytes).ok() } @@ -46,7 +62,10 @@ fn validate_site_id(site_id: &str) -> Option { if !first.is_ascii_alphanumeric() { return Some("site_id must start with alphanumeric".to_string()); } - if !site_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { + if !site_id + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { return Some("site_id must contain only alphanumeric, hyphens, underscores".to_string()); } None @@ -78,7 +97,9 @@ pub async fn get_local_site( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } if let Some(ref registry) = state.site_registry { if let Some(local) = registry.get_local_site() { @@ -86,7 +107,11 @@ pub async fn get_local_site( } } - json_error("NotFound", "Local site not configured", StatusCode::NOT_FOUND) + json_error( + "NotFound", + "Local site not configured", + StatusCode::NOT_FOUND, + ) } pub async fn update_local_site( @@ -94,27 +119,51 @@ pub async fn update_local_site( Extension(principal): Extension, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("InvalidRequest", "Site registry not available", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Site registry not available", + StatusCode::BAD_REQUEST, + ) + } }; let payload = match read_json_body(body).await { Some(v) => v, - None => return json_error("MalformedJSON", "Invalid JSON body", StatusCode::BAD_REQUEST), + None => { + return json_error( + "MalformedJSON", + "Invalid JSON body", + StatusCode::BAD_REQUEST, + ) + } }; let site_id = match payload.get("site_id").and_then(|v| v.as_str()) { Some(s) => s.to_string(), - None => return json_error("ValidationError", "site_id is required", StatusCode::BAD_REQUEST), + None => { + return json_error( + "ValidationError", + "site_id is required", + StatusCode::BAD_REQUEST, + ) + } }; if let Some(err) = validate_site_id(&site_id) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); } - let endpoint = payload.get("endpoint").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let endpoint = payload + .get("endpoint") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); if !endpoint.is_empty() { if let Some(err) = validate_endpoint(&endpoint) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); @@ -137,9 +186,20 @@ pub async fn update_local_site( let site = SiteInfo { site_id: site_id.clone(), endpoint, - region: payload.get("region").and_then(|v| v.as_str()).unwrap_or("us-east-1").to_string(), - priority: payload.get("priority").and_then(|v| v.as_i64()).unwrap_or(100) as i32, - display_name: payload.get("display_name").and_then(|v| v.as_str()).unwrap_or(&site_id).to_string(), + region: payload + .get("region") + .and_then(|v| v.as_str()) + .unwrap_or("us-east-1") + .to_string(), + priority: payload + .get("priority") + .and_then(|v| v.as_i64()) + .unwrap_or(100) as i32, + display_name: payload + .get("display_name") + .and_then(|v| v.as_str()) + .unwrap_or(&site_id) + .to_string(), created_at: existing.and_then(|e| e.created_at), }; @@ -151,20 +211,30 @@ pub async fn list_all_sites( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_response(StatusCode::OK, serde_json::json!({"local": null, "peers": [], "total_peers": 0})), + None => { + return json_response( + StatusCode::OK, + serde_json::json!({"local": null, "peers": [], "total_peers": 0}), + ) + } }; let local = registry.get_local_site(); let peers = registry.list_peers(); - json_response(StatusCode::OK, serde_json::json!({ - "local": local, - "peers": peers, - "total_peers": peers.len(), - })) + json_response( + StatusCode::OK, + serde_json::json!({ + "local": local, + "peers": peers, + "total_peers": peers.len(), + }), + ) } pub async fn register_peer_site( @@ -172,20 +242,40 @@ pub async fn register_peer_site( Extension(principal): Extension, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("InvalidRequest", "Site registry not available", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Site registry not available", + StatusCode::BAD_REQUEST, + ) + } }; let payload = match read_json_body(body).await { Some(v) => v, - None => return json_error("MalformedJSON", "Invalid JSON body", StatusCode::BAD_REQUEST), + None => { + return json_error( + "MalformedJSON", + "Invalid JSON body", + StatusCode::BAD_REQUEST, + ) + } }; let site_id = match payload.get("site_id").and_then(|v| v.as_str()) { Some(s) => s.to_string(), - None => return json_error("ValidationError", "site_id is required", StatusCode::BAD_REQUEST), + None => { + return json_error( + "ValidationError", + "site_id is required", + StatusCode::BAD_REQUEST, + ) + } }; if let Some(err) = validate_site_id(&site_id) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); @@ -193,24 +283,41 @@ pub async fn register_peer_site( let endpoint = match payload.get("endpoint").and_then(|v| v.as_str()) { Some(e) => e.to_string(), - None => return json_error("ValidationError", "endpoint is required", StatusCode::BAD_REQUEST), + None => { + return json_error( + "ValidationError", + "endpoint is required", + StatusCode::BAD_REQUEST, + ) + } }; if let Some(err) = validate_endpoint(&endpoint) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); } - let region = payload.get("region").and_then(|v| v.as_str()).unwrap_or("us-east-1").to_string(); + let region = payload + .get("region") + .and_then(|v| v.as_str()) + .unwrap_or("us-east-1") + .to_string(); if let Some(err) = validate_region(®ion) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); } - let priority = payload.get("priority").and_then(|v| v.as_i64()).unwrap_or(100); + let priority = payload + .get("priority") + .and_then(|v| v.as_i64()) + .unwrap_or(100); if let Some(err) = validate_priority(priority) { return json_error("ValidationError", &err, StatusCode::BAD_REQUEST); } if registry.get_peer(&site_id).is_some() { - return json_error("AlreadyExists", &format!("Peer site '{}' already exists", site_id), StatusCode::CONFLICT); + return json_error( + "AlreadyExists", + &format!("Peer site '{}' already exists", site_id), + StatusCode::CONFLICT, + ); } let peer = PeerSite { @@ -218,8 +325,15 @@ pub async fn register_peer_site( endpoint, region, priority: priority as i32, - display_name: payload.get("display_name").and_then(|v| v.as_str()).unwrap_or(&site_id).to_string(), - connection_id: payload.get("connection_id").and_then(|v| v.as_str()).map(|s| s.to_string()), + display_name: payload + .get("display_name") + .and_then(|v| v.as_str()) + .unwrap_or(&site_id) + .to_string(), + connection_id: payload + .get("connection_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), created_at: Some(chrono::Utc::now().to_rfc3339()), is_healthy: false, last_health_check: None, @@ -234,15 +348,27 @@ pub async fn get_peer_site( Extension(principal): Extension, Path(site_id): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("NotFound", "Site registry not available", StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + "Site registry not available", + StatusCode::NOT_FOUND, + ) + } }; match registry.get_peer(&site_id) { Some(peer) => json_response(StatusCode::OK, serde_json::to_value(&peer).unwrap()), - None => json_error("NotFound", &format!("Peer site '{}' not found", site_id), StatusCode::NOT_FOUND), + None => json_error( + "NotFound", + &format!("Peer site '{}' not found", site_id), + StatusCode::NOT_FOUND, + ), } } @@ -252,20 +378,40 @@ pub async fn update_peer_site( Path(site_id): Path, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("NotFound", "Site registry not available", StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + "Site registry not available", + StatusCode::NOT_FOUND, + ) + } }; let existing = match registry.get_peer(&site_id) { Some(p) => p, - None => return json_error("NotFound", &format!("Peer site '{}' not found", site_id), StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + &format!("Peer site '{}' not found", site_id), + StatusCode::NOT_FOUND, + ) + } }; let payload = match read_json_body(body).await { Some(v) => v, - None => return json_error("MalformedJSON", "Invalid JSON body", StatusCode::BAD_REQUEST), + None => { + return json_error( + "MalformedJSON", + "Invalid JSON body", + StatusCode::BAD_REQUEST, + ) + } }; if let Some(ep) = payload.get("endpoint").and_then(|v| v.as_str()) { @@ -286,11 +432,30 @@ pub async fn update_peer_site( let peer = PeerSite { site_id: site_id.clone(), - endpoint: payload.get("endpoint").and_then(|v| v.as_str()).unwrap_or(&existing.endpoint).to_string(), - region: payload.get("region").and_then(|v| v.as_str()).unwrap_or(&existing.region).to_string(), - priority: payload.get("priority").and_then(|v| v.as_i64()).unwrap_or(existing.priority as i64) as i32, - display_name: payload.get("display_name").and_then(|v| v.as_str()).unwrap_or(&existing.display_name).to_string(), - connection_id: payload.get("connection_id").and_then(|v| v.as_str()).map(|s| s.to_string()).or(existing.connection_id), + endpoint: payload + .get("endpoint") + .and_then(|v| v.as_str()) + .unwrap_or(&existing.endpoint) + .to_string(), + region: payload + .get("region") + .and_then(|v| v.as_str()) + .unwrap_or(&existing.region) + .to_string(), + priority: payload + .get("priority") + .and_then(|v| v.as_i64()) + .unwrap_or(existing.priority as i64) as i32, + display_name: payload + .get("display_name") + .and_then(|v| v.as_str()) + .unwrap_or(&existing.display_name) + .to_string(), + connection_id: payload + .get("connection_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or(existing.connection_id), created_at: existing.created_at, is_healthy: existing.is_healthy, last_health_check: existing.last_health_check, @@ -305,14 +470,26 @@ pub async fn delete_peer_site( Extension(principal): Extension, Path(site_id): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("NotFound", "Site registry not available", StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + "Site registry not available", + StatusCode::NOT_FOUND, + ) + } }; if !registry.delete_peer(&site_id) { - return json_error("NotFound", &format!("Peer site '{}' not found", site_id), StatusCode::NOT_FOUND); + return json_error( + "NotFound", + &format!("Peer site '{}' not found", site_id), + StatusCode::NOT_FOUND, + ); } StatusCode::NO_CONTENT.into_response() } @@ -322,32 +499,77 @@ pub async fn check_peer_health( Extension(principal): Extension, Path(site_id): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("NotFound", "Site registry not available", StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + "Site registry not available", + StatusCode::NOT_FOUND, + ) + } }; if registry.get_peer(&site_id).is_none() { - return json_error("NotFound", &format!("Peer site '{}' not found", site_id), StatusCode::NOT_FOUND); + return json_error( + "NotFound", + &format!("Peer site '{}' not found", site_id), + StatusCode::NOT_FOUND, + ); } - json_response(StatusCode::OK, serde_json::json!({ - "site_id": site_id, - "is_healthy": false, - "error": "Health check not implemented in standalone mode", - "checked_at": chrono::Utc::now().timestamp_millis() as f64 / 1000.0, - })) + let peer = registry.get_peer(&site_id).unwrap(); + let checked_at = chrono::Utc::now().timestamp_millis() as f64 / 1000.0; + let mut is_healthy = false; + let mut error: Option = None; + + if let Some(connection_id) = peer.connection_id.as_deref() { + if let Some(connection) = state.connections.get(connection_id) { + is_healthy = state.replication.check_endpoint(&connection).await; + if !is_healthy { + error = Some(format!( + "Cannot reach endpoint: {}", + connection.endpoint_url + )); + } + } else { + error = Some(format!("Connection '{}' not found", connection_id)); + } + } else { + error = Some("No connection configured for this peer".to_string()); + } + + registry.update_health(&site_id, is_healthy); + + json_response( + StatusCode::OK, + serde_json::json!({ + "site_id": site_id, + "is_healthy": is_healthy, + "error": error, + "checked_at": checked_at, + }), + ) } pub async fn get_topology( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_response(StatusCode::OK, serde_json::json!({"sites": [], "total": 0, "healthy_count": 0})), + None => { + return json_response( + StatusCode::OK, + serde_json::json!({"sites": [], "total": 0, "healthy_count": 0}), + ) + } }; let local = registry.get_local_site(); @@ -356,25 +578,41 @@ pub async fn get_topology( let mut sites: Vec = Vec::new(); if let Some(l) = local { let mut v = serde_json::to_value(&l).unwrap(); - v.as_object_mut().unwrap().insert("is_local".to_string(), serde_json::json!(true)); - v.as_object_mut().unwrap().insert("is_healthy".to_string(), serde_json::json!(true)); + v.as_object_mut() + .unwrap() + .insert("is_local".to_string(), serde_json::json!(true)); + v.as_object_mut() + .unwrap() + .insert("is_healthy".to_string(), serde_json::json!(true)); sites.push(v); } for p in &peers { let mut v = serde_json::to_value(p).unwrap(); - v.as_object_mut().unwrap().insert("is_local".to_string(), serde_json::json!(false)); + v.as_object_mut() + .unwrap() + .insert("is_local".to_string(), serde_json::json!(false)); sites.push(v); } sites.sort_by_key(|s| s.get("priority").and_then(|v| v.as_i64()).unwrap_or(100)); - let healthy_count = sites.iter().filter(|s| s.get("is_healthy").and_then(|v| v.as_bool()).unwrap_or(false)).count(); + let healthy_count = sites + .iter() + .filter(|s| { + s.get("is_healthy") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + }) + .count(); - json_response(StatusCode::OK, serde_json::json!({ - "sites": sites, - "total": sites.len(), - "healthy_count": healthy_count, - })) + json_response( + StatusCode::OK, + serde_json::json!({ + "sites": sites, + "total": sites.len(), + "healthy_count": healthy_count, + }), + ) } pub async fn check_bidirectional_status( @@ -382,34 +620,315 @@ pub async fn check_bidirectional_status( Extension(principal): Extension, Path(site_id): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let registry = match &state.site_registry { Some(r) => r, - None => return json_error("NotFound", "Site registry not available", StatusCode::NOT_FOUND), + None => { + return json_error( + "NotFound", + "Site registry not available", + StatusCode::NOT_FOUND, + ) + } }; if registry.get_peer(&site_id).is_none() { - return json_error("NotFound", &format!("Peer site '{}' not found", site_id), StatusCode::NOT_FOUND); + return json_error( + "NotFound", + &format!("Peer site '{}' not found", site_id), + StatusCode::NOT_FOUND, + ); } let local = registry.get_local_site(); - json_response(StatusCode::OK, serde_json::json!({ + let peer = registry.get_peer(&site_id).unwrap(); + let local_bidirectional_rules: Vec = state + .replication + .list_rules() + .into_iter() + .filter(|rule| { + peer.connection_id + .as_deref() + .map(|connection_id| rule.target_connection_id == connection_id) + .unwrap_or(false) + && rule.mode == crate::services::replication::MODE_BIDIRECTIONAL + }) + .map(|rule| { + serde_json::json!({ + "bucket_name": rule.bucket_name, + "target_bucket": rule.target_bucket, + "enabled": rule.enabled, + }) + }) + .collect(); + + let mut result = serde_json::json!({ "site_id": site_id, - "local_site_id": local.as_ref().map(|l| &l.site_id), - "local_endpoint": local.as_ref().map(|l| &l.endpoint), - "local_bidirectional_rules": [], - "local_site_sync_enabled": false, + "local_site_id": local.as_ref().map(|l| l.site_id.clone()), + "local_endpoint": local.as_ref().map(|l| l.endpoint.clone()), + "local_bidirectional_rules": local_bidirectional_rules, + "local_site_sync_enabled": state.config.site_sync_enabled, "remote_status": null, - "issues": [{"code": "NOT_IMPLEMENTED", "message": "Bidirectional status check not implemented in standalone mode", "severity": "warning"}], + "issues": Vec::::new(), "is_fully_configured": false, - })) + }); + + if local + .as_ref() + .map(|site| site.site_id.trim().is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + serde_json::json!({ + "code": "NO_LOCAL_SITE_ID", + "message": "Local site identity not configured", + "severity": "error", + }), + ); + } + if local + .as_ref() + .map(|site| site.endpoint.trim().is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + serde_json::json!({ + "code": "NO_LOCAL_ENDPOINT", + "message": "Local site endpoint not configured (remote site cannot reach back)", + "severity": "error", + }), + ); + } + + let Some(connection_id) = peer.connection_id.as_deref() else { + push_issue( + &mut result, + serde_json::json!({ + "code": "NO_CONNECTION", + "message": "No connection configured for this peer", + "severity": "error", + }), + ); + return json_response(StatusCode::OK, result); + }; + + let Some(connection) = state.connections.get(connection_id) else { + push_issue( + &mut result, + serde_json::json!({ + "code": "CONNECTION_NOT_FOUND", + "message": format!("Connection '{}' not found", connection_id), + "severity": "error", + }), + ); + return json_response(StatusCode::OK, result); + }; + + if result["local_bidirectional_rules"] + .as_array() + .map(|rules| rules.is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + serde_json::json!({ + "code": "NO_LOCAL_BIDIRECTIONAL_RULES", + "message": "No bidirectional replication rules configured on this site", + "severity": "warning", + }), + ); + } + if !state.config.site_sync_enabled { + push_issue( + &mut result, + serde_json::json!({ + "code": "SITE_SYNC_DISABLED", + "message": "Site sync worker is disabled (SITE_SYNC_ENABLED=false). Pull operations will not work.", + "severity": "warning", + }), + ); + } + if !state.replication.check_endpoint(&connection).await { + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_UNREACHABLE", + "message": "Remote endpoint is not reachable", + "severity": "error", + }), + ); + return json_response(StatusCode::OK, result); + } + + let admin_url = format!( + "{}/admin/sites", + connection.endpoint_url.trim_end_matches('/') + ); + match reqwest::Client::new() + .get(&admin_url) + .header("accept", "application/json") + .header("x-access-key", &connection.access_key) + .header("x-secret-key", &connection.secret_key) + .timeout(std::time::Duration::from_secs(10)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => match resp.json::().await { + Ok(remote_data) => { + let remote_local = remote_data + .get("local") + .cloned() + .unwrap_or(serde_json::Value::Null); + let remote_peers = remote_data + .get("peers") + .and_then(|value| value.as_array()) + .cloned() + .unwrap_or_default(); + let mut has_peer_for_us = false; + let mut peer_connection_configured = false; + + for remote_peer in &remote_peers { + let matches_site = local + .as_ref() + .map(|site| { + remote_peer.get("site_id").and_then(|v| v.as_str()) + == Some(site.site_id.as_str()) + || remote_peer.get("endpoint").and_then(|v| v.as_str()) + == Some(site.endpoint.as_str()) + }) + .unwrap_or(false); + if matches_site { + has_peer_for_us = true; + peer_connection_configured = remote_peer + .get("connection_id") + .and_then(|v| v.as_str()) + .map(|v| !v.trim().is_empty()) + .unwrap_or(false); + break; + } + } + + result["remote_status"] = serde_json::json!({ + "reachable": true, + "local_site": remote_local, + "site_sync_enabled": serde_json::Value::Null, + "has_peer_for_us": has_peer_for_us, + "peer_connection_configured": peer_connection_configured, + "has_bidirectional_rules_for_us": serde_json::Value::Null, + }); + + if !has_peer_for_us { + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_NO_PEER_FOR_US", + "message": "Remote site does not have this site registered as a peer", + "severity": "error", + }), + ); + } else if !peer_connection_configured { + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_NO_CONNECTION_FOR_US", + "message": "Remote site has us as peer but no connection configured (cannot push back)", + "severity": "error", + }), + ); + } + } + Err(_) => { + result["remote_status"] = serde_json::json!({ + "reachable": true, + "invalid_response": true, + }); + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_INVALID_RESPONSE", + "message": "Remote admin API returned invalid JSON", + "severity": "warning", + }), + ); + } + }, + Ok(resp) + if resp.status() == StatusCode::UNAUTHORIZED + || resp.status() == StatusCode::FORBIDDEN => + { + result["remote_status"] = serde_json::json!({ + "reachable": true, + "admin_access_denied": true, + }); + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_ADMIN_ACCESS_DENIED", + "message": "Cannot verify remote configuration (admin access denied)", + "severity": "warning", + }), + ); + } + Ok(resp) => { + result["remote_status"] = serde_json::json!({ + "reachable": true, + "admin_api_error": resp.status().as_u16(), + }); + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_ADMIN_API_ERROR", + "message": format!("Remote admin API returned status {}", resp.status().as_u16()), + "severity": "warning", + }), + ); + } + Err(_) => { + result["remote_status"] = serde_json::json!({ + "reachable": false, + "error": "Connection failed", + }); + push_issue( + &mut result, + serde_json::json!({ + "code": "REMOTE_ADMIN_UNREACHABLE", + "message": "Could not reach remote admin API", + "severity": "warning", + }), + ); + } + } + + let has_errors = result["issues"] + .as_array() + .map(|items| { + items.iter().any(|issue| { + issue.get("severity").and_then(|value| value.as_str()) == Some("error") + }) + }) + .unwrap_or(true); + result["is_fully_configured"] = serde_json::json!( + !has_errors + && result["local_bidirectional_rules"] + .as_array() + .map(|rules| !rules.is_empty()) + .unwrap_or(false) + ); + + json_response(StatusCode::OK, result) } pub async fn iam_list_users( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let users = state.iam.list_users().await; json_response(StatusCode::OK, serde_json::json!({"users": users})) } @@ -419,10 +938,16 @@ pub async fn iam_get_user( Extension(principal): Extension, Path(identifier): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.get_user(&identifier).await { Some(user) => json_response(StatusCode::OK, user), - None => json_error("NotFound", &format!("User '{}' not found", identifier), StatusCode::NOT_FOUND), + None => json_error( + "NotFound", + &format!("User '{}' not found", identifier), + StatusCode::NOT_FOUND, + ), } } @@ -431,10 +956,16 @@ pub async fn iam_get_user_policies( Extension(principal): Extension, Path(identifier): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.get_user_policies(&identifier) { Some(policies) => json_response(StatusCode::OK, serde_json::json!({"policies": policies})), - None => json_error("NotFound", &format!("User '{}' not found", identifier), StatusCode::NOT_FOUND), + None => json_error( + "NotFound", + &format!("User '{}' not found", identifier), + StatusCode::NOT_FOUND, + ), } } @@ -443,7 +974,9 @@ pub async fn iam_create_access_key( Extension(principal): Extension, Path(identifier): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.create_access_key(&identifier) { Ok(result) => json_response(StatusCode::CREATED, result), Err(e) => json_error("InvalidRequest", &e, StatusCode::BAD_REQUEST), @@ -455,7 +988,9 @@ pub async fn iam_delete_access_key( Extension(principal): Extension, Path((_identifier, access_key)): Path<(String, String)>, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.delete_access_key(&access_key) { Ok(()) => StatusCode::NO_CONTENT.into_response(), Err(e) => json_error("InvalidRequest", &e, StatusCode::BAD_REQUEST), @@ -467,7 +1002,9 @@ pub async fn iam_disable_user( Extension(principal): Extension, Path(identifier): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.set_user_enabled(&identifier, false).await { Ok(()) => json_response(StatusCode::OK, serde_json::json!({"status": "disabled"})), Err(e) => json_error("InvalidRequest", &e, StatusCode::BAD_REQUEST), @@ -479,7 +1016,9 @@ pub async fn iam_enable_user( Extension(principal): Extension, Path(identifier): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match state.iam.set_user_enabled(&identifier, true).await { Ok(()) => json_response(StatusCode::OK, serde_json::json!({"status": "enabled"})), Err(e) => json_error("InvalidRequest", &e, StatusCode::BAD_REQUEST), @@ -490,10 +1029,18 @@ pub async fn list_website_domains( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let store = match &state.website_domains { Some(s) => s, - None => return json_error("InvalidRequest", "Website hosting is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Website hosting is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; json_response(StatusCode::OK, serde_json::json!(store.list_all())) } @@ -503,41 +1050,85 @@ pub async fn create_website_domain( Extension(principal): Extension, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let store = match &state.website_domains { Some(s) => s, - None => return json_error("InvalidRequest", "Website hosting is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Website hosting is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let payload = match read_json_body(body).await { Some(v) => v, - None => return json_error("MalformedJSON", "Invalid JSON body", StatusCode::BAD_REQUEST), + None => { + return json_error( + "MalformedJSON", + "Invalid JSON body", + StatusCode::BAD_REQUEST, + ) + } }; let domain = normalize_domain(payload.get("domain").and_then(|v| v.as_str()).unwrap_or("")); if domain.is_empty() { - return json_error("ValidationError", "domain is required", StatusCode::BAD_REQUEST); + return json_error( + "ValidationError", + "domain is required", + StatusCode::BAD_REQUEST, + ); } if !is_valid_domain(&domain) { - return json_error("ValidationError", &format!("Invalid domain: '{}'", domain), StatusCode::BAD_REQUEST); + return json_error( + "ValidationError", + &format!("Invalid domain: '{}'", domain), + StatusCode::BAD_REQUEST, + ); } - let bucket = payload.get("bucket").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(); + let bucket = payload + .get("bucket") + .and_then(|v| v.as_str()) + .unwrap_or("") + .trim() + .to_string(); if bucket.is_empty() { - return json_error("ValidationError", "bucket is required", StatusCode::BAD_REQUEST); + return json_error( + "ValidationError", + "bucket is required", + StatusCode::BAD_REQUEST, + ); } match state.storage.bucket_exists(&bucket).await { Ok(true) => {} - _ => return json_error("NoSuchBucket", &format!("Bucket '{}' does not exist", bucket), StatusCode::NOT_FOUND), + _ => { + return json_error( + "NoSuchBucket", + &format!("Bucket '{}' does not exist", bucket), + StatusCode::NOT_FOUND, + ) + } } if store.get_bucket(&domain).is_some() { - return json_error("Conflict", &format!("Domain '{}' is already mapped", domain), StatusCode::CONFLICT); + return json_error( + "Conflict", + &format!("Domain '{}' is already mapped", domain), + StatusCode::CONFLICT, + ); } store.set_mapping(&domain, &bucket); - json_response(StatusCode::CREATED, serde_json::json!({"domain": domain, "bucket": bucket})) + json_response( + StatusCode::CREATED, + serde_json::json!({"domain": domain, "bucket": bucket}), + ) } pub async fn get_website_domain( @@ -545,16 +1136,31 @@ pub async fn get_website_domain( Extension(principal): Extension, Path(domain): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let store = match &state.website_domains { Some(s) => s, - None => return json_error("InvalidRequest", "Website hosting is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Website hosting is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let domain = normalize_domain(&domain); match store.get_bucket(&domain) { - Some(bucket) => json_response(StatusCode::OK, serde_json::json!({"domain": domain, "bucket": bucket})), - None => json_error("NotFound", &format!("No mapping found for domain '{}'", domain), StatusCode::NOT_FOUND), + Some(bucket) => json_response( + StatusCode::OK, + serde_json::json!({"domain": domain, "bucket": bucket}), + ), + None => json_error( + "NotFound", + &format!("No mapping found for domain '{}'", domain), + StatusCode::NOT_FOUND, + ), } } @@ -564,34 +1170,70 @@ pub async fn update_website_domain( Path(domain): Path, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let store = match &state.website_domains { Some(s) => s, - None => return json_error("InvalidRequest", "Website hosting is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Website hosting is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let domain = normalize_domain(&domain); let payload = match read_json_body(body).await { Some(v) => v, - None => return json_error("MalformedJSON", "Invalid JSON body", StatusCode::BAD_REQUEST), + None => { + return json_error( + "MalformedJSON", + "Invalid JSON body", + StatusCode::BAD_REQUEST, + ) + } }; - let bucket = payload.get("bucket").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(); + let bucket = payload + .get("bucket") + .and_then(|v| v.as_str()) + .unwrap_or("") + .trim() + .to_string(); if bucket.is_empty() { - return json_error("ValidationError", "bucket is required", StatusCode::BAD_REQUEST); + return json_error( + "ValidationError", + "bucket is required", + StatusCode::BAD_REQUEST, + ); } match state.storage.bucket_exists(&bucket).await { Ok(true) => {} - _ => return json_error("NoSuchBucket", &format!("Bucket '{}' does not exist", bucket), StatusCode::NOT_FOUND), + _ => { + return json_error( + "NoSuchBucket", + &format!("Bucket '{}' does not exist", bucket), + StatusCode::NOT_FOUND, + ) + } } if store.get_bucket(&domain).is_none() { - return json_error("NotFound", &format!("No mapping found for domain '{}'", domain), StatusCode::NOT_FOUND); + return json_error( + "NotFound", + &format!("No mapping found for domain '{}'", domain), + StatusCode::NOT_FOUND, + ); } store.set_mapping(&domain, &bucket); - json_response(StatusCode::OK, serde_json::json!({"domain": domain, "bucket": bucket})) + json_response( + StatusCode::OK, + serde_json::json!({"domain": domain, "bucket": bucket}), + ) } pub async fn delete_website_domain( @@ -599,15 +1241,27 @@ pub async fn delete_website_domain( Extension(principal): Extension, Path(domain): Path, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let store = match &state.website_domains { Some(s) => s, - None => return json_error("InvalidRequest", "Website hosting is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Website hosting is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let domain = normalize_domain(&domain); if !store.delete_mapping(&domain) { - return json_error("NotFound", &format!("No mapping found for domain '{}'", domain), StatusCode::NOT_FOUND); + return json_error( + "NotFound", + &format!("No mapping found for domain '{}'", domain), + StatusCode::NOT_FOUND, + ); } StatusCode::NO_CONTENT.into_response() } @@ -622,10 +1276,15 @@ pub async fn gc_status( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match &state.gc { Some(gc) => json_response(StatusCode::OK, gc.status().await), - None => json_response(StatusCode::OK, serde_json::json!({"enabled": false, "message": "GC is not enabled. Set GC_ENABLED=true to enable."})), + None => json_response( + StatusCode::OK, + serde_json::json!({"enabled": false, "message": "GC is not enabled. Set GC_ENABLED=true to enable."}), + ), } } @@ -634,14 +1293,25 @@ pub async fn gc_run( Extension(principal): Extension, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let gc = match &state.gc { Some(gc) => gc, - None => return json_error("InvalidRequest", "GC is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "GC is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let payload = read_json_body(body).await.unwrap_or(serde_json::json!({})); - let dry_run = payload.get("dry_run").and_then(|v| v.as_bool()).unwrap_or(false); + let dry_run = payload + .get("dry_run") + .and_then(|v| v.as_bool()) + .unwrap_or(false); match gc.run_now(dry_run).await { Ok(result) => json_response(StatusCode::OK, result), @@ -653,9 +1323,14 @@ pub async fn gc_history( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match &state.gc { - Some(gc) => json_response(StatusCode::OK, serde_json::json!({"executions": gc.history().await})), + Some(gc) => json_response( + StatusCode::OK, + serde_json::json!({"executions": gc.history().await}), + ), None => json_response(StatusCode::OK, serde_json::json!({"executions": []})), } } @@ -664,10 +1339,15 @@ pub async fn integrity_status( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match &state.integrity { Some(checker) => json_response(StatusCode::OK, checker.status().await), - None => json_response(StatusCode::OK, serde_json::json!({"enabled": false, "message": "Integrity checker is not enabled. Set INTEGRITY_ENABLED=true to enable."})), + None => json_response( + StatusCode::OK, + serde_json::json!({"enabled": false, "message": "Integrity checker is not enabled. Set INTEGRITY_ENABLED=true to enable."}), + ), } } @@ -676,15 +1356,29 @@ pub async fn integrity_run( Extension(principal): Extension, body: Body, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } let checker = match &state.integrity { Some(c) => c, - None => return json_error("InvalidRequest", "Integrity checker is not enabled", StatusCode::BAD_REQUEST), + None => { + return json_error( + "InvalidRequest", + "Integrity checker is not enabled", + StatusCode::BAD_REQUEST, + ) + } }; let payload = read_json_body(body).await.unwrap_or(serde_json::json!({})); - let dry_run = payload.get("dry_run").and_then(|v| v.as_bool()).unwrap_or(false); - let auto_heal = payload.get("auto_heal").and_then(|v| v.as_bool()).unwrap_or(false); + let dry_run = payload + .get("dry_run") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let auto_heal = payload + .get("auto_heal") + .and_then(|v| v.as_bool()) + .unwrap_or(false); match checker.run_now(dry_run, auto_heal).await { Ok(result) => json_response(StatusCode::OK, result), @@ -696,9 +1390,14 @@ pub async fn integrity_history( State(state): State, Extension(principal): Extension, ) -> Response { - if let Some(err) = require_admin(&principal) { return err; } + if let Some(err) = require_admin(&principal) { + return err; + } match &state.integrity { - Some(checker) => json_response(StatusCode::OK, serde_json::json!({"executions": checker.history().await})), + Some(checker) => json_response( + StatusCode::OK, + serde_json::json!({"executions": checker.history().await}), + ), None => json_response(StatusCode::OK, serde_json::json!({"executions": []})), } } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/chunked.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/chunked.rs index c061781..316dad1 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/chunked.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/chunked.rs @@ -41,7 +41,10 @@ impl AwsChunkedStream { fn parse_chunk_size(line: &[u8]) -> std::io::Result { let text = std::str::from_utf8(line).map_err(|_| { - std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid chunk size encoding") + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid chunk size encoding", + ) })?; let head = text.split(';').next().unwrap_or("").trim(); u64::from_str_radix(head, 16).map_err(|_| { @@ -179,4 +182,3 @@ pub fn decode_body(body: axum::body::Body) -> impl AsyncRead + Send + Unpin { ); AwsChunkedStream::new(stream) } - diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/config.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/config.rs index a28b3b5..971e55c 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/config.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/config.rs @@ -13,8 +13,14 @@ fn xml_response(status: StatusCode, xml: String) -> Response { fn storage_err(err: myfsio_storage::error::StorageError) -> Response { let s3err = S3Error::from(err); - let status = StatusCode::from_u16(s3err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); - (status, [("content-type", "application/xml")], s3err.to_xml()).into_response() + let status = + StatusCode::from_u16(s3err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + ( + status, + [("content-type", "application/xml")], + s3err.to_xml(), + ) + .into_response() } fn json_response(status: StatusCode, value: serde_json::Value) -> Response { @@ -68,7 +74,7 @@ pub async fn get_tagging(state: &AppState, bucket: &str) -> Response { Ok(config) => { let mut xml = String::from( "\ - " + ", ); for tag in &config.tags { xml.push_str(&format!( @@ -130,7 +136,11 @@ pub async fn get_cors(state: &AppState, bucket: &str) -> Response { } else { xml_response( StatusCode::NOT_FOUND, - S3Error::new(S3ErrorCode::NoSuchKey, "The CORS configuration does not exist").to_xml(), + S3Error::new( + S3ErrorCode::NoSuchKey, + "The CORS configuration does not exist", + ) + .to_xml(), ) } } @@ -192,7 +202,8 @@ pub async fn get_encryption(state: &AppState, bucket: &str) -> Response { S3Error::new( S3ErrorCode::InvalidRequest, "The server side encryption configuration was not found", - ).to_xml(), + ) + .to_xml(), ) } } @@ -240,7 +251,11 @@ pub async fn get_lifecycle(state: &AppState, bucket: &str) -> Response { } else { xml_response( StatusCode::NOT_FOUND, - S3Error::new(S3ErrorCode::NoSuchKey, "The lifecycle configuration does not exist").to_xml(), + S3Error::new( + S3ErrorCode::NoSuchKey, + "The lifecycle configuration does not exist", + ) + .to_xml(), ) } } @@ -328,17 +343,17 @@ pub async fn put_quota(state: &AppState, bucket: &str, body: Body) -> Response { Err(_) => { return xml_response( StatusCode::BAD_REQUEST, - S3Error::new(S3ErrorCode::InvalidArgument, "Request body must be valid JSON").to_xml(), + S3Error::new( + S3ErrorCode::InvalidArgument, + "Request body must be valid JSON", + ) + .to_xml(), ); } }; - let max_size = payload - .get("max_size_bytes") - .and_then(|v| v.as_u64()); - let max_objects = payload - .get("max_objects") - .and_then(|v| v.as_u64()); + let max_size = payload.get("max_size_bytes").and_then(|v| v.as_u64()); + let max_objects = payload.get("max_objects").and_then(|v| v.as_u64()); if max_size.is_none() && max_objects.is_none() { return xml_response( @@ -603,7 +618,11 @@ pub async fn get_website(state: &AppState, bucket: &str) -> Response { } else { xml_response( StatusCode::NOT_FOUND, - S3Error::new(S3ErrorCode::NoSuchKey, "The website configuration does not exist").to_xml(), + S3Error::new( + S3ErrorCode::NoSuchKey, + "The website configuration does not exist", + ) + .to_xml(), ) } } @@ -677,19 +696,120 @@ pub async fn get_notification(state: &AppState, bucket: &str) -> Response { } pub async fn get_logging(state: &AppState, bucket: &str) -> Response { - match state.storage.get_bucket_config(bucket).await { - Ok(config) => { - if let Some(l) = &config.logging { - xml_response(StatusCode::OK, l.to_string()) - } else { - let xml = "\ - \ - "; - xml_response(StatusCode::OK, xml.to_string()) - } + match state.storage.bucket_exists(bucket).await { + Ok(true) => {} + Ok(false) => { + return storage_err(myfsio_storage::error::StorageError::BucketNotFound( + bucket.to_string(), + )) } - Err(e) => storage_err(e), + Err(e) => return storage_err(e), } + + let logging_config = if let Some(cfg) = state.access_logging.get(bucket) { + Some(cfg) + } else { + match state.storage.get_bucket_config(bucket).await { + Ok(config) => { + let legacy = legacy_logging_config(&config); + if let Some(cfg) = legacy.as_ref() { + if let Err(err) = state.access_logging.set(bucket, cfg.clone()) { + tracing::warn!( + "Failed to migrate legacy bucket logging config for {}: {}", + bucket, + err + ); + } + } + legacy + } + Err(e) => return storage_err(e), + } + }; + + let body = match logging_config { + Some(cfg) if cfg.enabled => format!( + "\ + \ + {}{}\ + ", + xml_escape(&cfg.target_bucket), + xml_escape(&cfg.target_prefix), + ), + _ => "\ + " + .to_string(), + }; + xml_response(StatusCode::OK, body) +} + +fn xml_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn legacy_logging_config( + config: &myfsio_common::types::BucketConfig, +) -> Option { + let value = config.logging.as_ref()?; + match value { + serde_json::Value::String(xml) => parse_logging_config_xml(xml), + serde_json::Value::Object(_) => parse_logging_config_value(value.clone()), + _ => None, + } +} + +fn parse_logging_config_value( + value: serde_json::Value, +) -> Option { + let logging_enabled = value.get("LoggingEnabled")?; + let target_bucket = logging_enabled + .get("TargetBucket") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty())? + .to_string(); + let target_prefix = logging_enabled + .get("TargetPrefix") + .and_then(|value| value.as_str()) + .unwrap_or_default() + .to_string(); + Some(crate::services::access_logging::LoggingConfiguration { + target_bucket, + target_prefix, + enabled: true, + }) +} + +fn parse_logging_config_xml( + xml: &str, +) -> Option { + let doc = roxmltree::Document::parse(xml).ok()?; + let root = doc.root_element(); + let logging_enabled = root + .children() + .find(|n| n.is_element() && n.tag_name().name() == "LoggingEnabled")?; + let target_bucket = logging_enabled + .children() + .find(|n| n.is_element() && n.tag_name().name() == "TargetBucket") + .and_then(|n| n.text()) + .map(str::trim) + .filter(|value| !value.is_empty())? + .to_string(); + let target_prefix = logging_enabled + .children() + .find(|n| n.is_element() && n.tag_name().name() == "TargetPrefix") + .and_then(|n| n.text()) + .unwrap_or_default() + .to_string(); + Some(crate::services::access_logging::LoggingConfiguration { + target_bucket, + target_prefix, + enabled: true, + }) } pub async fn put_object_lock(state: &AppState, bucket: &str, body: Body) -> Response { @@ -757,35 +877,125 @@ pub async fn delete_notification(state: &AppState, bucket: &str) -> Response { } pub async fn put_logging(state: &AppState, bucket: &str, body: Body) -> Response { + match state.storage.bucket_exists(bucket).await { + Ok(true) => {} + Ok(false) => { + return storage_err(myfsio_storage::error::StorageError::BucketNotFound( + bucket.to_string(), + )) + } + Err(e) => return storage_err(e), + } + let body_bytes = match http_body_util::BodyExt::collect(body).await { Ok(collected) => collected.to_bytes(), Err(_) => return StatusCode::BAD_REQUEST.into_response(), }; - let value = serde_json::Value::String(String::from_utf8_lossy(&body_bytes).to_string()); - match state.storage.get_bucket_config(bucket).await { - Ok(mut config) => { - config.logging = Some(value); - match state.storage.set_bucket_config(bucket, &config).await { - Ok(()) => StatusCode::OK.into_response(), - Err(e) => storage_err(e), - } - } - Err(e) => storage_err(e), + if body_bytes.iter().all(u8::is_ascii_whitespace) { + state.access_logging.delete(bucket); + return StatusCode::OK.into_response(); } + + let xml = match std::str::from_utf8(&body_bytes) { + Ok(s) => s, + Err(_) => { + return s3_error_response( + S3ErrorCode::MalformedXML, + "Unable to parse XML document", + StatusCode::BAD_REQUEST, + ) + } + }; + + let doc = match roxmltree::Document::parse(xml) { + Ok(d) => d, + Err(_) => { + return s3_error_response( + S3ErrorCode::MalformedXML, + "Unable to parse XML document", + StatusCode::BAD_REQUEST, + ) + } + }; + + let root = doc.root_element(); + let logging_enabled = root + .children() + .find(|n| n.is_element() && n.tag_name().name() == "LoggingEnabled"); + + let Some(le) = logging_enabled else { + state.access_logging.delete(bucket); + return StatusCode::OK.into_response(); + }; + + let target_bucket = le + .children() + .find(|n| n.is_element() && n.tag_name().name() == "TargetBucket") + .and_then(|n| n.text()) + .map(str::trim) + .unwrap_or_default(); + + if target_bucket.is_empty() { + return s3_error_response( + S3ErrorCode::InvalidArgument, + "TargetBucket is required", + StatusCode::BAD_REQUEST, + ); + } + + let cfg = crate::services::access_logging::LoggingConfiguration { + target_bucket: target_bucket.to_string(), + target_prefix: le + .children() + .find(|n| n.is_element() && n.tag_name().name() == "TargetPrefix") + .and_then(|n| n.text()) + .unwrap_or_default() + .to_string(), + enabled: true, + }; + + match state.storage.bucket_exists(&cfg.target_bucket).await { + Ok(true) => {} + Ok(false) => { + return s3_error_response( + S3ErrorCode::InvalidArgument, + "Target bucket does not exist", + StatusCode::BAD_REQUEST, + ) + } + Err(e) => return storage_err(e), + } + + if let Err(e) = state.access_logging.set(bucket, cfg) { + tracing::error!( + "Failed to persist bucket logging config for {}: {}", + bucket, + e + ); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + StatusCode::OK.into_response() } pub async fn delete_logging(state: &AppState, bucket: &str) -> Response { - match state.storage.get_bucket_config(bucket).await { - Ok(mut config) => { - config.logging = None; - match state.storage.set_bucket_config(bucket, &config).await { - Ok(()) => StatusCode::NO_CONTENT.into_response(), - Err(e) => storage_err(e), - } + match state.storage.bucket_exists(bucket).await { + Ok(true) => {} + Ok(false) => { + return storage_err(myfsio_storage::error::StorageError::BucketNotFound( + bucket.to_string(), + )) } - Err(e) => storage_err(e), + Err(e) => return storage_err(e), } + state.access_logging.delete(bucket); + StatusCode::NO_CONTENT.into_response() +} + +fn s3_error_response(code: S3ErrorCode, message: &str, status: StatusCode) -> Response { + let err = S3Error::new(code, message.to_string()); + (status, [("content-type", "application/xml")], err.to_xml()).into_response() } pub async fn list_object_versions(state: &AppState, bucket: &str) -> Response { @@ -812,7 +1022,7 @@ pub async fn list_object_versions(state: &AppState, bucket: &str) -> Response { let mut xml = String::from( "\ - " + ", ); xml.push_str(&format!("{}", bucket)); @@ -842,7 +1052,7 @@ pub async fn get_object_tagging(state: &AppState, bucket: &str, key: &str) -> Re Ok(tags) => { let mut xml = String::from( "\ - " + ", ); for tag in &tags { xml.push_str(&format!( @@ -910,20 +1120,24 @@ pub async fn put_object_acl(state: &AppState, bucket: &str, key: &str, _body: Bo pub async fn get_object_retention(state: &AppState, bucket: &str, key: &str) -> Response { match state.storage.head_object(bucket, key).await { - Ok(_) => { - xml_response( - StatusCode::NOT_FOUND, - S3Error::new( - S3ErrorCode::InvalidRequest, - "No retention policy configured", - ).to_xml(), + Ok(_) => xml_response( + StatusCode::NOT_FOUND, + S3Error::new( + S3ErrorCode::InvalidRequest, + "No retention policy configured", ) - } + .to_xml(), + ), Err(e) => storage_err(e), } } -pub async fn put_object_retention(state: &AppState, bucket: &str, key: &str, _body: Body) -> Response { +pub async fn put_object_retention( + state: &AppState, + bucket: &str, + key: &str, + _body: Body, +) -> Response { match state.storage.head_object(bucket, key).await { Ok(_) => StatusCode::OK.into_response(), Err(e) => storage_err(e), @@ -942,13 +1156,68 @@ pub async fn get_object_legal_hold(state: &AppState, bucket: &str, key: &str) -> } } -pub async fn put_object_legal_hold(state: &AppState, bucket: &str, key: &str, _body: Body) -> Response { +pub async fn put_object_legal_hold( + state: &AppState, + bucket: &str, + key: &str, + _body: Body, +) -> Response { match state.storage.head_object(bucket, key).await { Ok(_) => StatusCode::OK.into_response(), Err(e) => storage_err(e), } } +#[cfg(test)] +mod tests { + use super::{legacy_logging_config, parse_logging_config_xml}; + use myfsio_common::types::BucketConfig; + + #[test] + fn parses_legacy_logging_xml_string() { + let mut config = BucketConfig::default(); + config.logging = Some(serde_json::Value::String( + "\ + \ + logsaudit/\ + " + .to_string(), + )); + + let parsed = legacy_logging_config(&config).expect("expected legacy logging config"); + assert_eq!(parsed.target_bucket, "logs"); + assert_eq!(parsed.target_prefix, "audit/"); + assert!(parsed.enabled); + } + + #[test] + fn parses_legacy_logging_json_object() { + let mut config = BucketConfig::default(); + config.logging = Some(serde_json::json!({ + "LoggingEnabled": { + "TargetBucket": "logs", + "TargetPrefix": "archive/" + } + })); + + let parsed = legacy_logging_config(&config).expect("expected legacy logging config"); + assert_eq!(parsed.target_bucket, "logs"); + assert_eq!(parsed.target_prefix, "archive/"); + assert!(parsed.enabled); + } + + #[test] + fn ignores_logging_xml_without_enabled_block() { + let parsed = parse_logging_config_xml( + "\ + \ + ", + ); + + assert!(parsed.is_none()); + } +} + fn parse_tagging_xml(xml: &str) -> Vec { let mut tags = Vec::new(); let mut in_tag = false; diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/kms.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/kms.rs index 0a4fba0..857382e 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/kms.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/kms.rs @@ -1,14 +1,17 @@ +use aes_gcm::aead::Aead; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use axum::body::Body; use axum::extract::State; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use base64::engine::general_purpose::STANDARD as B64; use base64::Engine; -use serde_json::json; +use rand::RngCore; +use serde_json::{json, Value}; use crate::state::AppState; -fn json_ok(value: serde_json::Value) -> Response { +fn json_ok(value: Value) -> Response { ( StatusCode::OK, [("content-type", "application/json")], @@ -26,14 +29,54 @@ fn json_err(status: StatusCode, msg: &str) -> Response { .into_response() } +async fn read_json(body: Body) -> Result { + let body_bytes = http_body_util::BodyExt::collect(body) + .await + .map_err(|_| json_err(StatusCode::BAD_REQUEST, "Invalid request body"))? + .to_bytes(); + if body_bytes.is_empty() { + Ok(json!({})) + } else { + serde_json::from_slice(&body_bytes) + .map_err(|_| json_err(StatusCode::BAD_REQUEST, "Invalid JSON")) + } +} + +fn require_kms( + state: &AppState, +) -> Result<&std::sync::Arc, Response> { + state + .kms + .as_ref() + .ok_or_else(|| json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled")) +} + +fn decode_b64(value: &str, field: &str) -> Result, Response> { + B64.decode(value).map_err(|_| { + json_err( + StatusCode::BAD_REQUEST, + &format!("Invalid base64 {}", field), + ) + }) +} + +fn require_str<'a>(value: &'a Value, names: &[&str], message: &str) -> Result<&'a str, Response> { + for name in names { + if let Some(found) = value.get(*name).and_then(|v| v.as_str()) { + return Ok(found); + } + } + Err(json_err(StatusCode::BAD_REQUEST, message)) +} + pub async fn list_keys(State(state): State) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, }; let keys = kms.list_keys().await; - let keys_json: Vec = keys + let keys_json: Vec = keys .iter() .map(|k| { json!({ @@ -53,31 +96,22 @@ pub async fn list_keys(State(state): State) -> Response { } pub async fn create_key(State(state): State, body: Body) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, }; - let body_bytes = match http_body_util::BodyExt::collect(body).await { - Ok(c) => c.to_bytes(), - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid request body"), - }; + let description = req + .get("Description") + .or_else(|| req.get("description")) + .and_then(|d| d.as_str()) + .unwrap_or(""); - let description = if body_bytes.is_empty() { - String::new() - } else { - match serde_json::from_slice::(&body_bytes) { - Ok(v) => v - .get("Description") - .or_else(|| v.get("description")) - .and_then(|d| d.as_str()) - .unwrap_or("") - .to_string(), - Err(_) => String::new(), - } - }; - - match kms.create_key(&description).await { + match kms.create_key(description).await { Ok(key) => json_ok(json!({ "KeyId": key.key_id, "Arn": key.arn, @@ -94,9 +128,9 @@ pub async fn get_key( State(state): State, axum::extract::Path(key_id): axum::extract::Path, ) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, }; match kms.get_key(&key_id).await { @@ -118,9 +152,9 @@ pub async fn delete_key( State(state): State, axum::extract::Path(key_id): axum::extract::Path, ) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, }; match kms.delete_key(&key_id).await { @@ -134,9 +168,9 @@ pub async fn enable_key( State(state): State, axum::extract::Path(key_id): axum::extract::Path, ) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, }; match kms.enable_key(&key_id).await { @@ -150,9 +184,9 @@ pub async fn disable_key( State(state): State, axum::extract::Path(key_id): axum::extract::Path, ) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, }; match kms.disable_key(&key_id).await { @@ -163,32 +197,26 @@ pub async fn disable_key( } pub async fn encrypt(State(state): State, body: Body) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, }; - let body_bytes = match http_body_util::BodyExt::collect(body).await { - Ok(c) => c.to_bytes(), - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid request body"), + let key_id = match require_str(&req, &["KeyId", "key_id"], "Missing KeyId") { + Ok(value) => value, + Err(response) => return response, }; - - let req: serde_json::Value = match serde_json::from_slice(&body_bytes) { - Ok(v) => v, - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid JSON"), + let plaintext_b64 = match require_str(&req, &["Plaintext", "plaintext"], "Missing Plaintext") { + Ok(value) => value, + Err(response) => return response, }; - - let key_id = match req.get("KeyId").and_then(|v| v.as_str()) { - Some(k) => k, - None => return json_err(StatusCode::BAD_REQUEST, "Missing KeyId"), - }; - let plaintext_b64 = match req.get("Plaintext").and_then(|v| v.as_str()) { - Some(p) => p, - None => return json_err(StatusCode::BAD_REQUEST, "Missing Plaintext"), - }; - let plaintext = match B64.decode(plaintext_b64) { - Ok(p) => p, - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid base64 Plaintext"), + let plaintext = match decode_b64(plaintext_b64, "Plaintext") { + Ok(value) => value, + Err(response) => return response, }; match kms.encrypt_data(key_id, &plaintext).await { @@ -201,32 +229,30 @@ pub async fn encrypt(State(state): State, body: Body) -> Response { } pub async fn decrypt(State(state): State, body: Body) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, }; - let body_bytes = match http_body_util::BodyExt::collect(body).await { - Ok(c) => c.to_bytes(), - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid request body"), + let key_id = match require_str(&req, &["KeyId", "key_id"], "Missing KeyId") { + Ok(value) => value, + Err(response) => return response, }; - - let req: serde_json::Value = match serde_json::from_slice(&body_bytes) { - Ok(v) => v, - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid JSON"), + let ciphertext_b64 = match require_str( + &req, + &["CiphertextBlob", "ciphertext_blob"], + "Missing CiphertextBlob", + ) { + Ok(value) => value, + Err(response) => return response, }; - - let key_id = match req.get("KeyId").and_then(|v| v.as_str()) { - Some(k) => k, - None => return json_err(StatusCode::BAD_REQUEST, "Missing KeyId"), - }; - let ct_b64 = match req.get("CiphertextBlob").and_then(|v| v.as_str()) { - Some(c) => c, - None => return json_err(StatusCode::BAD_REQUEST, "Missing CiphertextBlob"), - }; - let ciphertext = match B64.decode(ct_b64) { - Ok(c) => c, - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid base64"), + let ciphertext = match decode_b64(ciphertext_b64, "CiphertextBlob") { + Ok(value) => value, + Err(response) => return response, }; match kms.decrypt_data(key_id, &ciphertext).await { @@ -239,39 +265,276 @@ pub async fn decrypt(State(state): State, body: Body) -> Response { } pub async fn generate_data_key(State(state): State, body: Body) -> Response { - let kms = match &state.kms { - Some(k) => k, - None => return json_err(StatusCode::SERVICE_UNAVAILABLE, "KMS not enabled"), + generate_data_key_inner(state, body, true).await +} + +pub async fn generate_data_key_without_plaintext( + State(state): State, + body: Body, +) -> Response { + generate_data_key_inner(state, body, false).await +} + +async fn generate_data_key_inner(state: AppState, body: Body, include_plaintext: bool) -> Response { + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, }; - let body_bytes = match http_body_util::BodyExt::collect(body).await { - Ok(c) => c.to_bytes(), - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid request body"), - }; - - let req: serde_json::Value = match serde_json::from_slice(&body_bytes) { - Ok(v) => v, - Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid JSON"), - }; - - let key_id = match req.get("KeyId").and_then(|v| v.as_str()) { - Some(k) => k, - None => return json_err(StatusCode::BAD_REQUEST, "Missing KeyId"), + let key_id = match require_str(&req, &["KeyId", "key_id"], "Missing KeyId") { + Ok(value) => value, + Err(response) => return response, }; let num_bytes = req .get("NumberOfBytes") .and_then(|v| v.as_u64()) .unwrap_or(32) as usize; - if num_bytes < 1 || num_bytes > 1024 { + if !(1..=1024).contains(&num_bytes) { return json_err(StatusCode::BAD_REQUEST, "NumberOfBytes must be 1-1024"); } match kms.generate_data_key(key_id, num_bytes).await { - Ok((plaintext, wrapped)) => json_ok(json!({ - "KeyId": key_id, - "Plaintext": B64.encode(&plaintext), - "CiphertextBlob": B64.encode(&wrapped), + Ok((plaintext, wrapped)) => { + let mut value = json!({ + "KeyId": key_id, + "CiphertextBlob": B64.encode(&wrapped), + }); + if include_plaintext { + value["Plaintext"] = json!(B64.encode(&plaintext)); + } + json_ok(value) + } + Err(e) => json_err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + } +} + +pub async fn re_encrypt(State(state): State, body: Body) -> Response { + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, + }; + + let ciphertext_b64 = match require_str( + &req, + &["CiphertextBlob", "ciphertext_blob"], + "CiphertextBlob is required", + ) { + Ok(value) => value, + Err(response) => return response, + }; + let destination_key_id = match require_str( + &req, + &["DestinationKeyId", "destination_key_id"], + "DestinationKeyId is required", + ) { + Ok(value) => value, + Err(response) => return response, + }; + let ciphertext = match decode_b64(ciphertext_b64, "CiphertextBlob") { + Ok(value) => value, + Err(response) => return response, + }; + + let keys = kms.list_keys().await; + let mut source_key_id: Option = None; + let mut plaintext: Option> = None; + for key in keys { + if !key.enabled { + continue; + } + if let Ok(value) = kms.decrypt_data(&key.key_id, &ciphertext).await { + source_key_id = Some(key.key_id); + plaintext = Some(value); + break; + } + } + + let Some(source_key_id) = source_key_id else { + return json_err( + StatusCode::BAD_REQUEST, + "Could not determine source key for CiphertextBlob", + ); + }; + let plaintext = plaintext.unwrap_or_default(); + + match kms.encrypt_data(destination_key_id, &plaintext).await { + Ok(new_ciphertext) => json_ok(json!({ + "CiphertextBlob": B64.encode(&new_ciphertext), + "SourceKeyId": source_key_id, + "KeyId": destination_key_id, + })), + Err(e) => json_err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + } +} + +pub async fn generate_random(State(state): State, body: Body) -> Response { + if let Err(response) = require_kms(&state) { + return response; + } + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, + }; + let num_bytes = req + .get("NumberOfBytes") + .and_then(|v| v.as_u64()) + .unwrap_or(32) as usize; + + if !(1..=1024).contains(&num_bytes) { + return json_err(StatusCode::BAD_REQUEST, "NumberOfBytes must be 1-1024"); + } + + let mut bytes = vec![0u8; num_bytes]; + rand::thread_rng().fill_bytes(&mut bytes); + json_ok(json!({ + "Plaintext": B64.encode(bytes), + })) +} + +pub async fn client_generate_key(State(state): State) -> Response { + let _ = state; + + let mut key = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut key); + json_ok(json!({ + "Key": B64.encode(key), + "Algorithm": "AES-256-GCM", + "KeySize": 32, + })) +} + +pub async fn client_encrypt(State(state): State, body: Body) -> Response { + let _ = state; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, + }; + let plaintext_b64 = + match require_str(&req, &["Plaintext", "plaintext"], "Plaintext is required") { + Ok(value) => value, + Err(response) => return response, + }; + let key_b64 = match require_str(&req, &["Key", "key"], "Key is required") { + Ok(value) => value, + Err(response) => return response, + }; + + let plaintext = match decode_b64(plaintext_b64, "Plaintext") { + Ok(value) => value, + Err(response) => return response, + }; + let key_bytes = match decode_b64(key_b64, "Key") { + Ok(value) => value, + Err(response) => return response, + }; + if key_bytes.len() != 32 { + return json_err(StatusCode::BAD_REQUEST, "Key must decode to 32 bytes"); + } + + let cipher = match Aes256Gcm::new_from_slice(&key_bytes) { + Ok(cipher) => cipher, + Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid encryption key"), + }; + let mut nonce_bytes = [0u8; 12]; + rand::thread_rng().fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + match cipher.encrypt(nonce, plaintext.as_ref()) { + Ok(ciphertext) => json_ok(json!({ + "Ciphertext": B64.encode(ciphertext), + "Nonce": B64.encode(nonce_bytes), + "Algorithm": "AES-256-GCM", + })), + Err(e) => json_err(StatusCode::BAD_REQUEST, &e.to_string()), + } +} + +pub async fn client_decrypt(State(state): State, body: Body) -> Response { + let _ = state; + let req = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, + }; + let ciphertext_b64 = match require_str( + &req, + &["Ciphertext", "ciphertext"], + "Ciphertext is required", + ) { + Ok(value) => value, + Err(response) => return response, + }; + let nonce_b64 = match require_str(&req, &["Nonce", "nonce"], "Nonce is required") { + Ok(value) => value, + Err(response) => return response, + }; + let key_b64 = match require_str(&req, &["Key", "key"], "Key is required") { + Ok(value) => value, + Err(response) => return response, + }; + + let ciphertext = match decode_b64(ciphertext_b64, "Ciphertext") { + Ok(value) => value, + Err(response) => return response, + }; + let nonce_bytes = match decode_b64(nonce_b64, "Nonce") { + Ok(value) => value, + Err(response) => return response, + }; + let key_bytes = match decode_b64(key_b64, "Key") { + Ok(value) => value, + Err(response) => return response, + }; + if key_bytes.len() != 32 { + return json_err(StatusCode::BAD_REQUEST, "Key must decode to 32 bytes"); + } + if nonce_bytes.len() != 12 { + return json_err(StatusCode::BAD_REQUEST, "Nonce must decode to 12 bytes"); + } + + let cipher = match Aes256Gcm::new_from_slice(&key_bytes) { + Ok(cipher) => cipher, + Err(_) => return json_err(StatusCode::BAD_REQUEST, "Invalid encryption key"), + }; + let nonce = Nonce::from_slice(&nonce_bytes); + + match cipher.decrypt(nonce, ciphertext.as_ref()) { + Ok(plaintext) => json_ok(json!({ + "Plaintext": B64.encode(plaintext), + })), + Err(e) => json_err(StatusCode::BAD_REQUEST, &e.to_string()), + } +} + +pub async fn materials( + State(state): State, + axum::extract::Path(key_id): axum::extract::Path, + body: Body, +) -> Response { + let kms = match require_kms(&state) { + Ok(kms) => kms, + Err(response) => return response, + }; + let _ = match read_json(body).await { + Ok(req) => req, + Err(response) => return response, + }; + + match kms.generate_data_key(&key_id, 32).await { + Ok((plaintext, wrapped)) => json_ok(json!({ + "PlaintextKey": B64.encode(plaintext), + "EncryptedKey": B64.encode(wrapped), + "KeyId": key_id, + "Algorithm": "AES-256-GCM", + "KeyWrapAlgorithm": "kms", })), Err(e) => json_err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/mod.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/mod.rs index 1ba61d4..2f63435 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/mod.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/mod.rs @@ -4,6 +4,7 @@ mod config; pub mod kms; mod select; pub mod ui; +pub mod ui_api; pub mod ui_pages; use std::collections::HashMap; @@ -15,6 +16,7 @@ use axum::response::{IntoResponse, Response}; use base64::engine::general_purpose::URL_SAFE; use base64::Engine; use chrono::{DateTime, Utc}; +use serde_json::json; use myfsio_common::error::{S3Error, S3ErrorCode}; use myfsio_common::types::PartInfo; @@ -25,7 +27,8 @@ use tokio_util::io::ReaderStream; use crate::state::AppState; fn s3_error_response(err: S3Error) -> Response { - let status = StatusCode::from_u16(err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + let status = + StatusCode::from_u16(err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let resource = if err.resource.is_empty() { "/".to_string() } else { @@ -35,12 +38,7 @@ fn s3_error_response(err: S3Error) -> Response { .with_resource(resource) .with_request_id(uuid::Uuid::new_v4().simple().to_string()) .to_xml(); - ( - status, - [("content-type", "application/xml")], - body, - ) - .into_response() + (status, [("content-type", "application/xml")], body).into_response() } fn storage_err_response(err: myfsio_storage::error::StorageError) -> Response { @@ -51,17 +49,25 @@ pub async fn list_buckets(State(state): State) -> Response { match state.storage.list_buckets().await { Ok(buckets) => { let xml = myfsio_xml::response::list_buckets_xml("myfsio", "myfsio", &buckets); - ( - StatusCode::OK, - [("content-type", "application/xml")], - xml, - ) - .into_response() + (StatusCode::OK, [("content-type", "application/xml")], xml).into_response() } Err(e) => storage_err_response(e), } } +pub async fn health_check() -> Response { + ( + StatusCode::OK, + [("content-type", "application/json")], + json!({ + "status": "ok", + "version": env!("CARGO_PKG_VERSION"), + }) + .to_string(), + ) + .into_response() +} + pub async fn create_bucket( State(state): State, Path(bucket): Path, @@ -109,14 +115,12 @@ pub async fn create_bucket( } match state.storage.create_bucket(&bucket).await { - Ok(()) => { - ( - StatusCode::OK, - [("location", format!("/{}", bucket).as_str())], - "", - ) - .into_response() - } + Ok(()) => ( + StatusCode::OK, + [("location", format!("/{}", bucket).as_str())], + "", + ) + .into_response(), Err(e) => storage_err_response(e), } } @@ -162,9 +166,7 @@ pub async fn get_bucket( Query(query): Query, ) -> Response { if !matches!(state.storage.bucket_exists(&bucket).await, Ok(true)) { - return storage_err_response( - myfsio_storage::error::StorageError::BucketNotFound(bucket), - ); + return storage_err_response(myfsio_storage::error::StorageError::BucketNotFound(bucket)); } if query.quota.is_some() { @@ -258,8 +260,16 @@ pub async fn get_bucket( let params = myfsio_common::types::ListParams { max_keys, continuation_token: effective_start.clone(), - prefix: if prefix.is_empty() { None } else { Some(prefix.clone()) }, - start_after: if is_v2 { query.start_after.clone() } else { None }, + prefix: if prefix.is_empty() { + None + } else { + Some(prefix.clone()) + }, + start_after: if is_v2 { + query.start_after.clone() + } else { + None + }, }; match state.storage.list_objects(&bucket, ¶ms).await { Ok(result) => { @@ -411,19 +421,16 @@ pub async fn delete_bucket( } } -pub async fn head_bucket( - State(state): State, - Path(bucket): Path, -) -> Response { +pub async fn head_bucket(State(state): State, Path(bucket): Path) -> Response { match state.storage.bucket_exists(&bucket).await { Ok(true) => { let mut headers = HeaderMap::new(); headers.insert("x-amz-bucket-region", state.config.region.parse().unwrap()); (StatusCode::OK, headers).into_response() } - Ok(false) => storage_err_response( - myfsio_storage::error::StorageError::BucketNotFound(bucket), - ), + Ok(false) => { + storage_err_response(myfsio_storage::error::StorageError::BucketNotFound(bucket)) + } Err(e) => storage_err_response(e), } } @@ -458,22 +465,34 @@ pub struct ObjectQuery { fn apply_response_overrides(headers: &mut HeaderMap, query: &ObjectQuery) { if let Some(ref v) = query.response_content_type { - if let Ok(val) = v.parse() { headers.insert("content-type", val); } + if let Ok(val) = v.parse() { + headers.insert("content-type", val); + } } if let Some(ref v) = query.response_content_disposition { - if let Ok(val) = v.parse() { headers.insert("content-disposition", val); } + if let Ok(val) = v.parse() { + headers.insert("content-disposition", val); + } } if let Some(ref v) = query.response_content_language { - if let Ok(val) = v.parse() { headers.insert("content-language", val); } + if let Ok(val) = v.parse() { + headers.insert("content-language", val); + } } if let Some(ref v) = query.response_content_encoding { - if let Ok(val) = v.parse() { headers.insert("content-encoding", val); } + if let Ok(val) = v.parse() { + headers.insert("content-encoding", val); + } } if let Some(ref v) = query.response_cache_control { - if let Ok(val) = v.parse() { headers.insert("cache-control", val); } + if let Ok(val) = v.parse() { + headers.insert("cache-control", val); + } } if let Some(ref v) = query.response_expires { - if let Ok(val) = v.parse() { headers.insert("expires", val); } + if let Ok(val) = v.parse() { + headers.insert("expires", val); + } } } @@ -490,12 +509,18 @@ fn guessed_content_type(key: &str, explicit: Option<&str>) -> String { } fn is_aws_chunked(headers: &HeaderMap) -> bool { - if let Some(enc) = headers.get("content-encoding").and_then(|v| v.to_str().ok()) { + if let Some(enc) = headers + .get("content-encoding") + .and_then(|v| v.to_str().ok()) + { if enc.to_ascii_lowercase().contains("aws-chunked") { return true; } } - if let Some(sha) = headers.get("x-amz-content-sha256").and_then(|v| v.to_str().ok()) { + if let Some(sha) = headers + .get("x-amz-content-sha256") + .and_then(|v| v.to_str().ok()) + { let lower = sha.to_ascii_lowercase(); if lower.starts_with("streaming-") { return true; @@ -535,7 +560,10 @@ pub async fn put_object( if let Some(ref upload_id) = query.upload_id { if let Some(part_number) = query.part_number { - if let Some(copy_source) = headers.get("x-amz-copy-source").and_then(|v| v.to_str().ok()) { + if let Some(copy_source) = headers + .get("x-amz-copy-source") + .and_then(|v| v.to_str().ok()) + { let range = headers .get("x-amz-copy-source-range") .and_then(|v| v.to_str().ok()); @@ -562,15 +590,16 @@ pub async fn put_object( } } - if let Some(copy_source) = headers.get("x-amz-copy-source").and_then(|v| v.to_str().ok()) { + if let Some(copy_source) = headers + .get("x-amz-copy-source") + .and_then(|v| v.to_str().ok()) + { return copy_object_handler(&state, copy_source, &bucket, &key, &headers).await; } let content_type = guessed_content_type( &key, - headers - .get("content-type") - .and_then(|v| v.to_str().ok()), + headers.get("content-type").and_then(|v| v.to_str().ok()), ); let mut metadata = HashMap::new(); @@ -589,14 +618,18 @@ pub async fn put_object( Box::pin(chunked::decode_body(body)) } else { let stream = tokio_util::io::StreamReader::new( - http_body_util::BodyStream::new(body).map_ok(|frame| { - frame.into_data().unwrap_or_default() - }).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), + http_body_util::BodyStream::new(body) + .map_ok(|frame| frame.into_data().unwrap_or_default()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), ); Box::pin(stream) }; - match state.storage.put_object(&bucket, &key, boxed, Some(metadata)).await { + match state + .storage + .put_object(&bucket, &key, boxed, Some(metadata)) + .await + { Ok(meta) => { if let Some(enc_ctx) = resolve_encryption_context(&state, &bucket, &headers).await { if let Some(ref enc_svc) = state.encryption { @@ -612,26 +645,39 @@ pub async fn put_object( Ok(enc_meta) => { if let Err(e) = tokio::fs::rename(&enc_tmp, &obj_path).await { let _ = tokio::fs::remove_file(&enc_tmp).await; - return storage_err_response(myfsio_storage::error::StorageError::Io(e)); + return storage_err_response( + myfsio_storage::error::StorageError::Io(e), + ); } - let enc_size = tokio::fs::metadata(&obj_path).await.map(|m| m.len()).unwrap_or(0); + let enc_size = tokio::fs::metadata(&obj_path) + .await + .map(|m| m.len()) + .unwrap_or(0); let mut enc_metadata = enc_meta.to_metadata_map(); - let all_meta = match state.storage.get_object_metadata(&bucket, &key).await { - Ok(m) => m, - Err(_) => HashMap::new(), - }; + let all_meta = + match state.storage.get_object_metadata(&bucket, &key).await { + Ok(m) => m, + Err(_) => HashMap::new(), + }; for (k, v) in &all_meta { enc_metadata.entry(k.clone()).or_insert_with(|| v.clone()); } enc_metadata.insert("__size__".to_string(), enc_size.to_string()); - let _ = state.storage.put_object_metadata(&bucket, &key, &enc_metadata).await; + let _ = state + .storage + .put_object_metadata(&bucket, &key, &enc_metadata) + .await; let mut resp_headers = HeaderMap::new(); if let Some(ref etag) = meta.etag { - resp_headers.insert("etag", format!("\"{}\"", etag).parse().unwrap()); + resp_headers + .insert("etag", format!("\"{}\"", etag).parse().unwrap()); } - resp_headers.insert("x-amz-server-side-encryption", enc_ctx.algorithm.as_str().parse().unwrap()); + resp_headers.insert( + "x-amz-server-side-encryption", + enc_ctx.algorithm.as_str().parse().unwrap(), + ); return (StatusCode::OK, resp_headers).into_response(); } Err(e) => { @@ -697,7 +743,11 @@ pub async fn get_object( return range_get_handler(&state, &bucket, &key, range_str, &query).await; } - let all_meta = state.storage.get_object_metadata(&bucket, &key).await.unwrap_or_default(); + let all_meta = state + .storage + .get_object_metadata(&bucket, &key) + .await + .unwrap_or_default(); let enc_meta = myfsio_crypto::encryption::EncryptionMetadata::from_metadata(&all_meta); if let (Some(ref enc_info), Some(ref enc_svc)) = (&enc_meta, &state.encryption) { @@ -712,7 +762,10 @@ pub async fn get_object( let customer_key = extract_sse_c_key(&headers); let ck_ref = customer_key.as_deref(); - if let Err(e) = enc_svc.decrypt_object(&obj_path, &dec_tmp, enc_info, ck_ref).await { + if let Err(e) = enc_svc + .decrypt_object(&obj_path, &dec_tmp, enc_info, ck_ref) + .await + { let _ = tokio::fs::remove_file(&dec_tmp).await; return s3_error_response(S3Error::new( myfsio_common::error::S3ErrorCode::InternalError, @@ -747,10 +800,17 @@ pub async fn get_object( insert_content_type(&mut resp_headers, &key, meta.content_type.as_deref()); resp_headers.insert( "last-modified", - meta.last_modified.format("%a, %d %b %Y %H:%M:%S GMT").to_string().parse().unwrap(), + meta.last_modified + .format("%a, %d %b %Y %H:%M:%S GMT") + .to_string() + .parse() + .unwrap(), ); resp_headers.insert("accept-ranges", "bytes".parse().unwrap()); - resp_headers.insert("x-amz-server-side-encryption", enc_info.algorithm.parse().unwrap()); + resp_headers.insert( + "x-amz-server-side-encryption", + enc_info.algorithm.parse().unwrap(), + ); for (k, v) in &meta.metadata { if let Ok(header_val) = v.parse() { @@ -889,11 +949,7 @@ pub async fn head_object( } } -async fn initiate_multipart_handler( - state: &AppState, - bucket: &str, - key: &str, -) -> Response { +async fn initiate_multipart_handler(state: &AppState, bucket: &str, key: &str) -> Response { match state.storage.initiate_multipart(bucket, key, None).await { Ok(upload_id) => { let xml = myfsio_xml::response::initiate_multipart_upload_xml(bucket, key, &upload_id); @@ -915,14 +971,18 @@ async fn upload_part_handler_with_chunking( Box::pin(chunked::decode_body(body)) } else { let stream = tokio_util::io::StreamReader::new( - http_body_util::BodyStream::new(body).map_ok(|frame| { - frame.into_data().unwrap_or_default() - }).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), + http_body_util::BodyStream::new(body) + .map_ok(|frame| frame.into_data().unwrap_or_default()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), ); Box::pin(stream) }; - match state.storage.upload_part(bucket, upload_id, part_number, boxed).await { + match state + .storage + .upload_part(bucket, upload_id, part_number, boxed) + .await + { Ok(etag) => { let mut headers = HeaderMap::new(); headers.insert("etag", format!("\"{}\"", etag).parse().unwrap()); @@ -1052,7 +1112,11 @@ async fn complete_multipart_handler( }) .collect(); - match state.storage.complete_multipart(bucket, upload_id, &parts).await { + match state + .storage + .complete_multipart(bucket, upload_id, &parts) + .await + { Ok(meta) => { let etag = meta.etag.as_deref().unwrap_or(""); let xml = myfsio_xml::response::complete_multipart_upload_xml( @@ -1067,21 +1131,14 @@ async fn complete_multipart_handler( } } -async fn abort_multipart_handler( - state: &AppState, - bucket: &str, - upload_id: &str, -) -> Response { +async fn abort_multipart_handler(state: &AppState, bucket: &str, upload_id: &str) -> Response { match state.storage.abort_multipart(bucket, upload_id).await { Ok(()) => StatusCode::NO_CONTENT.into_response(), Err(e) => storage_err_response(e), } } -async fn list_multipart_uploads_handler( - state: &AppState, - bucket: &str, -) -> Response { +async fn list_multipart_uploads_handler(state: &AppState, bucket: &str) -> Response { match state.storage.list_multipart_uploads(bucket).await { Ok(uploads) => { let xml = myfsio_xml::response::list_multipart_uploads_xml(bucket, &uploads); @@ -1128,9 +1185,7 @@ async fn object_attributes_handler( .collect(); let all = attrs.is_empty(); - let mut xml = String::from( - "" - ); + let mut xml = String::from(""); xml.push_str(""); if all || attrs.contains("etag") { @@ -1139,10 +1194,7 @@ async fn object_attributes_handler( } } if all || attrs.contains("storageclass") { - let sc = meta - .storage_class - .as_deref() - .unwrap_or("STANDARD"); + let sc = meta.storage_class.as_deref().unwrap_or("STANDARD"); xml.push_str(&format!("{}", xml_escape(sc))); } if all || attrs.contains("objectsize") { @@ -1185,7 +1237,11 @@ async fn copy_object_handler( return resp; } - match state.storage.copy_object(src_bucket, src_key, dst_bucket, dst_key).await { + match state + .storage + .copy_object(src_bucket, src_key, dst_bucket, dst_key) + .await + { Ok(meta) => { let etag = meta.etag.as_deref().unwrap_or(""); let last_modified = myfsio_xml::response::format_s3_datetime(&meta.last_modified); @@ -1196,11 +1252,7 @@ async fn copy_object_handler( } } -async fn delete_objects_handler( - state: &AppState, - bucket: &str, - body: Body, -) -> Response { +async fn delete_objects_handler(state: &AppState, bucket: &str, body: Body) -> Response { let body_bytes = match http_body_util::BodyExt::collect(body).await { Ok(collected) => collected.to_bytes(), Err(_) => { @@ -1289,7 +1341,9 @@ async fn range_get_handler( headers.insert("content-length", length.to_string().parse().unwrap()); headers.insert( "content-range", - format!("bytes {}-{}/{}", start, end, total_size).parse().unwrap(), + format!("bytes {}-{}/{}", start, end, total_size) + .parse() + .unwrap(), ); if let Some(ref etag) = meta.etag { headers.insert("etag", format!("\"{}\"", etag).parse().unwrap()); @@ -1471,7 +1525,10 @@ async fn resolve_encryption_context( bucket: &str, headers: &HeaderMap, ) -> Option { - if let Some(alg) = headers.get("x-amz-server-side-encryption").and_then(|v| v.to_str().ok()) { + if let Some(alg) = headers + .get("x-amz-server-side-encryption") + .and_then(|v| v.to_str().ok()) + { let algorithm = match alg { "AES256" => myfsio_crypto::encryption::SseAlgorithm::Aes256, "aws:kms" => myfsio_crypto::encryption::SseAlgorithm::AwsKms, @@ -1606,11 +1663,21 @@ async fn post_object_form_handler( let key_template = match fields.get("key").cloned() { Some(k) => k, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing key field")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing key field", + )) + } }; let policy_b64 = match fields.get("policy").cloned() { Some(v) => v, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing policy field")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing policy field", + )) + } }; let signature = match fields .iter() @@ -1618,7 +1685,12 @@ async fn post_object_form_handler( .map(|(_, v)| v.clone()) { Some(v) => v, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing signature")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing signature", + )) + } }; let credential = match fields .iter() @@ -1626,7 +1698,12 @@ async fn post_object_form_handler( .map(|(_, v)| v.clone()) { Some(v) => v, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing credential")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing credential", + )) + } }; let algorithm = match fields .iter() @@ -1634,7 +1711,12 @@ async fn post_object_form_handler( .map(|(_, v)| v.clone()) { Some(v) => v, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing algorithm")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing algorithm", + )) + } }; if algorithm != "AWS4-HMAC-SHA256" { return s3_error_response(S3Error::new( @@ -1667,7 +1749,10 @@ async fn post_object_form_handler( match chrono::DateTime::parse_from_rfc3339(&normalized) { Ok(exp_time) => { if Utc::now() > exp_time.with_timezone(&Utc) { - return s3_error_response(S3Error::new(S3ErrorCode::AccessDenied, "Policy expired")); + return s3_error_response(S3Error::new( + S3ErrorCode::AccessDenied, + "Policy expired", + )); } } Err(_) => { @@ -1688,14 +1773,23 @@ async fn post_object_form_handler( }; if let Some(conditions) = policy_value.get("conditions").and_then(|v| v.as_array()) { - if let Err(msg) = validate_post_policy_conditions(bucket, &object_key, conditions, &fields, content_length) { + if let Err(msg) = validate_post_policy_conditions( + bucket, + &object_key, + conditions, + &fields, + content_length, + ) { return s3_error_response(S3Error::new(S3ErrorCode::AccessDenied, msg)); } } let credential_parts: Vec<&str> = credential.split('/').collect(); if credential_parts.len() != 5 { - return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Invalid credential format")); + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Invalid credential format", + )); } let access_key = credential_parts[0]; let date_stamp = credential_parts[1]; @@ -1704,9 +1798,15 @@ async fn post_object_form_handler( let secret_key = match state.iam.get_secret_key(access_key) { Some(s) => s, - None => return s3_error_response(S3Error::new(S3ErrorCode::AccessDenied, "Invalid access key")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::AccessDenied, + "Invalid access key", + )) + } }; - let signing_key = myfsio_auth::sigv4::derive_signing_key(&secret_key, date_stamp, region, service); + let signing_key = + myfsio_auth::sigv4::derive_signing_key(&secret_key, date_stamp, region, service); let expected = myfsio_auth::sigv4::compute_post_policy_signature(&signing_key, &policy_b64); if !myfsio_auth::sigv4::constant_time_compare(&expected, &signature) { return s3_error_response(S3Error::new( @@ -1717,7 +1817,12 @@ async fn post_object_form_handler( let file_data = match file_bytes { Some(b) => b, - None => return s3_error_response(S3Error::new(S3ErrorCode::InvalidArgument, "Missing file field")), + None => { + return s3_error_response(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing file field", + )) + } }; let mut metadata = HashMap::new(); @@ -1741,7 +1846,11 @@ async fn post_object_form_handler( let cursor = std::io::Cursor::new(file_data.to_vec()); let boxed: myfsio_storage::traits::AsyncReadStream = Box::pin(cursor); - let meta = match state.storage.put_object(bucket, &object_key, boxed, Some(metadata)).await { + let meta = match state + .storage + .put_object(bucket, &object_key, boxed, Some(metadata)) + .await + { Ok(m) => m, Err(e) => return storage_err_response(e), }; diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/select.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/select.rs index 211f35a..cf77f80 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/select.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/select.rs @@ -51,14 +51,12 @@ pub async fn post_select_object_content( let object_path = match state.storage.get_object_path(bucket, key).await { Ok(path) => path, Err(_) => { - return s3_error_response(S3Error::new( - S3ErrorCode::NoSuchKey, - "Object not found", - )); + return s3_error_response(S3Error::new(S3ErrorCode::NoSuchKey, "Object not found")); } }; - let join_res = tokio::task::spawn_blocking(move || execute_select_query(object_path, request)).await; + let join_res = + tokio::task::spawn_blocking(move || execute_select_query(object_path, request)).await; let chunks = match join_res { Ok(Ok(chunks)) => chunks, Ok(Err(message)) => { @@ -79,7 +77,10 @@ pub async fn post_select_object_content( } let stats_payload = build_stats_xml(0, bytes_returned); - events.push(Bytes::from(encode_select_event("Stats", stats_payload.as_bytes()))); + events.push(Bytes::from(encode_select_event( + "Stats", + stats_payload.as_bytes(), + ))); events.push(Bytes::from(encode_select_event("End", b""))); let stream = stream::iter(events.into_iter().map(Ok::)); @@ -166,10 +167,18 @@ fn parse_select_request(payload: &[u8]) -> Result { )); } - let input_node = child(&root, "InputSerialization") - .ok_or_else(|| S3Error::new(S3ErrorCode::InvalidRequest, "InputSerialization is required"))?; - let output_node = child(&root, "OutputSerialization") - .ok_or_else(|| S3Error::new(S3ErrorCode::InvalidRequest, "OutputSerialization is required"))?; + let input_node = child(&root, "InputSerialization").ok_or_else(|| { + S3Error::new( + S3ErrorCode::InvalidRequest, + "InputSerialization is required", + ) + })?; + let output_node = child(&root, "OutputSerialization").ok_or_else(|| { + S3Error::new( + S3ErrorCode::InvalidRequest, + "OutputSerialization is required", + ) + })?; let input_format = parse_input_format(&input_node)?; let output_format = parse_output_format(&output_node)?; @@ -187,8 +196,10 @@ fn parse_input_format(node: &roxmltree::Node<'_, '_>) -> Result) -> Result) -> Result { if let Some(csv_node) = child(node, "CSV") { return Ok(OutputFormat::Csv(CsvOutputConfig { - field_delimiter: child_text(&csv_node, "FieldDelimiter").unwrap_or_else(|| ",".to_string()), - record_delimiter: child_text(&csv_node, "RecordDelimiter").unwrap_or_else(|| "\n".to_string()), - quote_character: child_text(&csv_node, "QuoteCharacter").unwrap_or_else(|| "\"".to_string()), + field_delimiter: child_text(&csv_node, "FieldDelimiter") + .unwrap_or_else(|| ",".to_string()), + record_delimiter: child_text(&csv_node, "RecordDelimiter") + .unwrap_or_else(|| "\n".to_string()), + quote_character: child_text(&csv_node, "QuoteCharacter") + .unwrap_or_else(|| "\"".to_string()), })); } if let Some(json_node) = child(node, "JSON") { return Ok(OutputFormat::Json(JsonOutputConfig { - record_delimiter: child_text(&json_node, "RecordDelimiter").unwrap_or_else(|| "\n".to_string()), + record_delimiter: child_text(&json_node, "RecordDelimiter") + .unwrap_or_else(|| "\n".to_string()), })); } @@ -231,7 +246,10 @@ fn parse_output_format(node: &roxmltree::Node<'_, '_>) -> Result(node: &'a roxmltree::Node<'a, 'input>, name: &str) -> Option> { +fn child<'a, 'input>( + node: &'a roxmltree::Node<'a, 'input>, + name: &str, +) -> Option> { node.children() .find(|n| n.is_element() && n.tag_name().name() == name) } @@ -243,7 +261,8 @@ fn child_text(node: &roxmltree::Node<'_, '_>, name: &str) -> Option { } fn execute_select_query(path: PathBuf, request: SelectRequest) -> Result>, String> { - let conn = Connection::open_in_memory().map_err(|e| format!("DuckDB connection error: {}", e))?; + let conn = + Connection::open_in_memory().map_err(|e| format!("DuckDB connection error: {}", e))?; load_input_table(&conn, &path, &request.input_format)?; @@ -341,7 +360,10 @@ fn collect_csv_chunks( let mut chunks: Vec> = Vec::new(); let mut buffer = String::new(); - while let Some(row) = rows.next().map_err(|e| format!("SQL execution error: {}", e))? { + while let Some(row) = rows + .next() + .map_err(|e| format!("SQL execution error: {}", e))? + { let mut fields: Vec = Vec::with_capacity(col_count); for i in 0..col_count { let value = row @@ -353,7 +375,10 @@ fn collect_csv_chunks( } let mut text = value_ref_to_string(value); - if text.contains(&delimiter) || text.contains("e) || text.contains(&record_delimiter) { + if text.contains(&delimiter) + || text.contains("e) + || text.contains(&record_delimiter) + { text = text.replace("e, &(quote.clone() + "e)); text = format!("{}{}{}", quote, text, quote); } @@ -385,16 +410,16 @@ fn collect_json_chunks( let mut chunks: Vec> = Vec::new(); let mut buffer = String::new(); - while let Some(row) = rows.next().map_err(|e| format!("SQL execution error: {}", e))? { + while let Some(row) = rows + .next() + .map_err(|e| format!("SQL execution error: {}", e))? + { let mut record: HashMap = HashMap::with_capacity(col_count); for i in 0..col_count { let value = row .get_ref(i) .map_err(|e| format!("SQL execution error: {}", e))?; - let key = columns - .get(i) - .cloned() - .unwrap_or_else(|| format!("_{}", i)); + let key = columns.get(i).cloned().unwrap_or_else(|| format!("_{}", i)); record.insert(key, value_ref_to_json(value)); } let line = serde_json::to_string(&record) @@ -452,7 +477,9 @@ fn value_ref_to_json(value: ValueRef<'_>) -> serde_json::Value { ValueRef::Double(v) => serde_json::json!(v), ValueRef::Decimal(v) => serde_json::Value::String(v.to_string()), ValueRef::Text(v) => serde_json::Value::String(String::from_utf8_lossy(v).into_owned()), - ValueRef::Blob(v) => serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v)), + ValueRef::Blob(v) => { + serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v)) + } _ => serde_json::Value::String(format!("{:?}", value)), } } @@ -477,7 +504,8 @@ fn require_xml_content_type(headers: &HeaderMap) -> Option { } fn s3_error_response(err: S3Error) -> Response { - let status = StatusCode::from_u16(err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + let status = + StatusCode::from_u16(err.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let resource = if err.resource.is_empty() { "/".to_string() } else { @@ -487,12 +515,7 @@ fn s3_error_response(err: S3Error) -> Response { .with_resource(resource) .with_request_id(uuid::Uuid::new_v4().simple().to_string()) .to_xml(); - ( - status, - [("content-type", "application/xml")], - body, - ) - .into_response() + (status, [("content-type", "application/xml")], body).into_response() } fn build_stats_xml(bytes_scanned: usize, bytes_returned: usize) -> String { @@ -508,7 +531,10 @@ fn encode_select_event(event_type: &str, payload: &[u8]) -> Vec { let mut headers = Vec::new(); headers.extend(encode_select_header(":event-type", event_type)); if event_type == "Records" { - headers.extend(encode_select_header(":content-type", "application/octet-stream")); + headers.extend(encode_select_header( + ":content-type", + "application/octet-stream", + )); } else if event_type == "Stats" { headers.extend(encode_select_header(":content-type", "text/xml")); } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui.rs index 551af37..6414d5e 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::error::Error as StdError; use axum::extract::{Extension, Form, State}; use axum::http::{header, HeaderMap, StatusCode}; @@ -100,6 +101,10 @@ pub async fn csrf_error_page( resp } +pub async fn root_redirect() -> Response { + Redirect::to("/ui/buckets").into_response() +} + pub async fn not_found_page( State(state): State, Extension(session): Extension, @@ -119,9 +124,15 @@ pub async fn require_login( return next.run(req).await; } let path = req.uri().path().to_string(); - let query = req.uri().query().map(|q| format!("?{}", q)).unwrap_or_default(); + let query = req + .uri() + .query() + .map(|q| format!("?{}", q)) + .unwrap_or_default(); let next_url = format!("{}{}", path, query); - let encoded = percent_encoding::utf8_percent_encode(&next_url, percent_encoding::NON_ALPHANUMERIC).to_string(); + let encoded = + percent_encoding::utf8_percent_encode(&next_url, percent_encoding::NON_ALPHANUMERIC) + .to_string(); let target = format!("/login?next={}", encoded); Redirect::to(&target).into_response() } @@ -130,22 +141,45 @@ pub fn render(state: &AppState, template: &str, ctx: &Context) -> Response { let engine = match &state.templates { Some(e) => e, None => { - return (StatusCode::INTERNAL_SERVER_ERROR, "Templates not configured").into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Templates not configured", + ) + .into_response(); } }; match engine.render(template, ctx) { Ok(html) => { let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, "text/html; charset=utf-8".parse().unwrap()); + headers.insert( + header::CONTENT_TYPE, + "text/html; charset=utf-8".parse().unwrap(), + ); (StatusCode::OK, headers, html).into_response() } Err(e) => { - tracing::error!("Template render failed ({}): {}", template, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Template error: {}", e), - ) - .into_response() + let mut detail = format!("{}", e); + let mut src = StdError::source(&e); + while let Some(s) = src { + detail.push_str(" | "); + detail.push_str(&s.to_string()); + src = s.source(); + } + tracing::error!("Template render failed ({}): {}", template, detail); + let fallback_ctx = Context::new(); + let body = if template != "500.html" { + engine + .render("500.html", &fallback_ctx) + .unwrap_or_else(|_| "Internal Server Error".to_string()) + } else { + "Internal Server Error".to_string() + }; + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + "text/html; charset=utf-8".parse().unwrap(), + ); + (StatusCode::INTERNAL_SERVER_ERROR, headers, body).into_response() } } } @@ -159,6 +193,8 @@ pub fn base_context(session: &SessionHandle, endpoint: Option<&str>) -> Context ctx.insert("current_user_display_name", &snapshot.display_name); ctx.insert("current_endpoint", &endpoint.unwrap_or("")); ctx.insert("request_args", &HashMap::::new()); + ctx.insert("null", &serde_json::Value::Null); + ctx.insert("none", &serde_json::Value::Null); ctx } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_api.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_api.rs new file mode 100644 index 0000000..c4b3db7 --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_api.rs @@ -0,0 +1,3506 @@ +use std::collections::{BTreeMap, HashMap}; +use std::io::Cursor; +use std::path::{Component, Path as FsPath, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use axum::body::{to_bytes, Body}; +use axum::extract::{Extension, Path, Query, State}; +use axum::http::{header, HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use chrono::{DateTime, Datelike, Timelike, Utc}; +use futures::TryStreamExt; +use http_body_util::BodyStream; +use myfsio_auth::sigv4; +use myfsio_common::constants::{BUCKET_VERSIONS_DIR, SYSTEM_BUCKETS_DIR, SYSTEM_ROOT}; +use myfsio_common::types::{ListParams, PartInfo, Tag}; +use myfsio_crypto::encryption::EncryptionMetadata; +use myfsio_storage::error::StorageError; +use myfsio_storage::traits::StorageEngine; +use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; +use roxmltree::Document; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde_json::{json, Value}; +use sysinfo::{Disks, System}; +use tokio::io::AsyncReadExt; + +use crate::handlers::{self, ObjectQuery}; +use crate::middleware::session::SessionHandle; +use crate::state::AppState; +use crate::stores::connections::RemoteConnection; + +const UI_KEY_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~') + .remove(b'/'); + +const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~'); + +const AWS_QUERY_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~'); + +fn url_templates_for(bucket: &str) -> Value { + json!({ + "download": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/download", bucket), + "preview": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/preview", bucket), + "delete": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/delete", bucket), + "presign": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/presign", bucket), + "metadata": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/metadata", bucket), + "versions": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/versions", bucket), + "restore": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/restore/VERSION_ID_PLACEHOLDER", bucket), + "tags": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/tags", bucket), + "copy": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/copy", bucket), + "move": format!("/ui/buckets/{}/objects/KEY_PLACEHOLDER/move", bucket), + }) +} + +fn encode_object_key(key: &str) -> String { + utf8_percent_encode(key, UI_KEY_ENCODE_SET).to_string() +} + +fn encode_path_segment(value: &str) -> String { + utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET).to_string() +} + +fn build_ui_object_url(bucket: &str, key: &str, action: &str) -> String { + format!( + "/ui/buckets/{}/objects/{}/{}", + bucket, + encode_object_key(key), + action + ) +} + +fn human_size(bytes: u64) -> String { + const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = bytes as f64; + let mut idx = 0; + while size >= 1024.0 && idx < UNITS.len() - 1 { + size /= 1024.0; + idx += 1; + } + if idx == 0 { + format!("{} {}", bytes, UNITS[idx]) + } else { + format!("{:.1} {}", size, UNITS[idx]) + } +} + +fn json_error(status: StatusCode, message: impl Into) -> Response { + (status, Json(json!({ "error": message.into() }))).into_response() +} + +fn json_ok(value: Value) -> Response { + Json(value).into_response() +} + +fn push_issue(result: &mut Value, issue: Value) { + if let Some(items) = result + .get_mut("issues") + .and_then(|value| value.as_array_mut()) + { + items.push(issue); + } +} + +fn storage_status(err: &StorageError) -> StatusCode { + match err { + StorageError::BucketNotFound(_) + | StorageError::ObjectNotFound { .. } + | StorageError::UploadNotFound(_) => StatusCode::NOT_FOUND, + StorageError::InvalidBucketName(_) + | StorageError::InvalidObjectKey(_) + | StorageError::InvalidRange + | StorageError::QuotaExceeded(_) => StatusCode::BAD_REQUEST, + StorageError::BucketAlreadyExists(_) => StatusCode::CONFLICT, + StorageError::BucketNotEmpty(_) => StatusCode::CONFLICT, + StorageError::Io(_) | StorageError::Json(_) | StorageError::Internal(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + } +} + +fn storage_json_error(err: StorageError) -> Response { + json_error(storage_status(&err), err.to_string()) +} + +fn parse_bool_flag(value: Option<&str>) -> bool { + matches!( + value.map(|v| v.trim().to_ascii_lowercase()), + Some(v) if v == "1" || v == "true" || v == "on" || v == "yes" + ) +} + +fn parse_form_body(bytes: &[u8]) -> HashMap { + String::from_utf8_lossy(bytes) + .split('&') + .filter(|pair| !pair.is_empty()) + .map(|pair| { + let mut parts = pair.splitn(2, '='); + let key = parts.next().unwrap_or_default(); + let value = parts.next().unwrap_or_default(); + (decode_form_value(key), decode_form_value(value)) + }) + .collect() +} + +fn decode_form_value(value: &str) -> String { + percent_encoding::percent_decode_str(&value.replace('+', " ")) + .decode_utf8_lossy() + .into_owned() +} + +fn current_access_key(session: &SessionHandle) -> Option { + session.read(|s| s.user_id.clone()) +} + +fn owner_id_or_default(session: &SessionHandle) -> String { + current_access_key(session).unwrap_or_else(|| "myfsio".to_string()) +} + +fn safe_attachment_filename(key: &str) -> String { + let raw = key.rsplit('/').next().unwrap_or(key); + let sanitized = raw + .replace('"', "'") + .replace('\\', "_") + .chars() + .filter(|c| c.is_ascii_graphic() || *c == ' ') + .collect::(); + if sanitized.trim().is_empty() { + "download".to_string() + } else { + sanitized + } +} + +fn parse_api_base(state: &AppState) -> String { + std::env::var("API_BASE_URL") + .unwrap_or_else(|_| format!("http://{}", state.config.bind_addr)) + .trim_end_matches('/') + .to_string() +} + +fn aws_query_encode(value: &str) -> String { + utf8_percent_encode(value, AWS_QUERY_ENCODE_SET).to_string() +} + +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn key_relative_path(key: &str) -> Result { + let mut out = PathBuf::new(); + for component in FsPath::new(key).components() { + match component { + Component::Normal(part) => out.push(part), + _ => return Err("Invalid object key".to_string()), + } + } + if out.as_os_str().is_empty() { + return Err("Invalid object key".to_string()); + } + Ok(out) +} + +fn object_live_path(state: &AppState, bucket: &str, key: &str) -> Result { + let rel = key_relative_path(key)?; + Ok(state.config.storage_root.join(bucket).join(rel)) +} + +fn version_root_for_bucket(state: &AppState, bucket: &str) -> PathBuf { + state + .config + .storage_root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join(BUCKET_VERSIONS_DIR) +} + +fn version_dir_for_object(state: &AppState, bucket: &str, key: &str) -> Result { + let rel = key_relative_path(key)?; + Ok(version_root_for_bucket(state, bucket).join(rel)) +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct VersionManifest { + #[serde(default)] + version_id: String, + #[serde(default)] + key: String, + #[serde(default)] + size: u64, + #[serde(default)] + archived_at: Option, + #[serde(default)] + etag: Option, + #[serde(default)] + metadata: HashMap, + #[serde(default)] + reason: Option, +} + +fn manifest_timestamp(value: &VersionManifest) -> DateTime { + value + .archived_at + .as_deref() + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or_else(Utc::now) +} + +fn manifest_to_json(record: &VersionManifest) -> Value { + let ts = manifest_timestamp(record); + json!({ + "version_id": record.version_id, + "key": record.key, + "size": record.size, + "etag": record.etag, + "archived_at": ts.to_rfc3339(), + "last_modified": ts.to_rfc3339(), + "metadata": record.metadata, + "reason": record.reason.clone().unwrap_or_else(|| "update".to_string()), + "is_latest": false, + }) +} + +fn read_version_manifests_for_object( + state: &AppState, + bucket: &str, + key: &str, +) -> Result, String> { + let version_dir = version_dir_for_object(state, bucket, key)?; + if !version_dir.exists() { + return Ok(Vec::new()); + } + + let mut entries = Vec::new(); + for entry in std::fs::read_dir(&version_dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + if !entry.file_type().map_err(|e| e.to_string())?.is_file() { + continue; + } + if entry.path().extension().and_then(|ext| ext.to_str()) != Some("json") { + continue; + } + let text = std::fs::read_to_string(entry.path()).map_err(|e| e.to_string())?; + let mut manifest: VersionManifest = + serde_json::from_str(&text).map_err(|e| e.to_string())?; + if manifest.version_id.is_empty() { + manifest.version_id = entry + .path() + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_default() + .to_string(); + } + if manifest.key.is_empty() { + manifest.key = key.to_string(); + } + entries.push(manifest); + } + + entries.sort_by(|a, b| manifest_timestamp(b).cmp(&manifest_timestamp(a))); + Ok(entries) +} + +async fn read_object_bytes_for_zip( + state: &AppState, + bucket: &str, + key: &str, +) -> Result, String> { + let all_meta = state + .storage + .get_object_metadata(bucket, key) + .await + .map_err(|e| e.to_string())?; + + if let Some(enc_meta) = EncryptionMetadata::from_metadata(&all_meta) { + let enc_svc = state + .encryption + .as_ref() + .ok_or_else(|| "Encryption service is not available".to_string())?; + let obj_path = state + .storage + .get_object_path(bucket, key) + .await + .map_err(|e| e.to_string())?; + let tmp_dir = state.config.storage_root.join(SYSTEM_ROOT).join("tmp"); + let _ = tokio::fs::create_dir_all(&tmp_dir).await; + let dec_tmp = tmp_dir.join(format!("zip-dec-{}", uuid::Uuid::new_v4())); + enc_svc + .decrypt_object(&obj_path, &dec_tmp, &enc_meta, None) + .await + .map_err(|e| e.to_string())?; + let bytes = tokio::fs::read(&dec_tmp).await.map_err(|e| e.to_string())?; + let _ = tokio::fs::remove_file(&dec_tmp).await; + return Ok(bytes); + } + + let (_meta, mut reader) = state + .storage + .get_object(bucket, key) + .await + .map_err(|e| e.to_string())?; + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .await + .map_err(|e| e.to_string())?; + Ok(bytes) +} + +fn value_to_string_vec(value: Option<&Value>, field_name: &str) -> Vec { + match value { + Some(Value::Array(items)) => items + .iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(), + Some(Value::String(s)) if !s.trim().is_empty() => vec![s.to_string()], + Some(Value::Null) | None => Vec::new(), + Some(_) => vec![field_name.to_string()], + } +} + +fn xml_child<'a>(node: roxmltree::Node<'a, 'a>, name: &str) -> Option> { + node.children() + .find(|child| child.is_element() && child.tag_name().name() == name) +} + +fn xml_child_text(node: roxmltree::Node<'_, '_>, name: &str) -> Option { + xml_child(node, name) + .and_then(|child| child.text()) + .map(|text| text.trim().to_string()) + .filter(|text| !text.is_empty()) +} + +fn xml_children_texts(node: roxmltree::Node<'_, '_>, name: &str) -> Vec { + node.children() + .filter(|child| child.is_element() && child.tag_name().name() == name) + .filter_map(|child| child.text().map(|text| text.trim().to_string())) + .filter(|text| !text.is_empty()) + .collect() +} + +fn parse_acl_value(value: Option<&Value>, owner: &str) -> Value { + let default_grant = json!({ + "grantee": owner, + "permission": "FULL_CONTROL", + "grantee_type": "CanonicalUser", + "display_name": owner, + "grantee_id": owner, + "grantee_uri": Value::Null, + }); + + let Some(value) = value else { + return json!({ + "owner": owner, + "grants": [default_grant], + "canned_acls": ["private", "public-read", "public-read-write", "authenticated-read"], + }); + }; + + match value { + Value::String(xml) => { + let doc = match Document::parse(xml) { + Ok(doc) => doc, + Err(_) => { + return json!({ + "owner": owner, + "grants": [default_grant], + "canned_acls": ["private", "public-read", "public-read-write", "authenticated-read"], + }); + } + }; + let owner_node = doc + .descendants() + .find(|node| node.is_element() && node.tag_name().name() == "Owner"); + let owner_id = owner_node + .and_then(|node| xml_child_text(node, "ID")) + .unwrap_or_else(|| owner.to_string()); + + let grants = doc + .descendants() + .filter(|node| node.is_element() && node.tag_name().name() == "Grant") + .map(|grant| { + let grantee = xml_child(grant, "Grantee"); + let permission = xml_child_text(grant, "Permission").unwrap_or_default(); + let grantee_id = grantee.and_then(|node| xml_child_text(node, "ID")); + let display_name = grantee.and_then(|node| xml_child_text(node, "DisplayName")); + let grantee_uri = grantee.and_then(|node| xml_child_text(node, "URI")); + let grantee_type = grantee + .and_then(|node| { + node.attributes() + .find(|attr| { + attr.name() == "type" || attr.name().ends_with(":type") + }) + .map(|attr| attr.value().to_string()) + }) + .or_else(|| { + if grantee_uri.is_some() { + Some("Group".to_string()) + } else { + Some("CanonicalUser".to_string()) + } + }) + .unwrap_or_else(|| "CanonicalUser".to_string()); + let grantee_label = display_name + .clone() + .or_else(|| grantee_id.clone()) + .or_else(|| grantee_uri.clone()) + .unwrap_or_else(|| "unknown".to_string()); + + json!({ + "grantee": grantee_label, + "permission": permission, + "grantee_type": grantee_type, + "display_name": display_name, + "grantee_id": grantee_id, + "grantee_uri": grantee_uri, + }) + }) + .collect::>(); + + json!({ + "owner": owner_id, + "grants": if grants.is_empty() { vec![default_grant] } else { grants }, + "canned_acls": ["private", "public-read", "public-read-write", "authenticated-read"], + }) + } + Value::Object(map) => { + let grants = map + .get("grants") + .and_then(|value| value.as_array()) + .cloned() + .unwrap_or_else(|| vec![default_grant]); + json!({ + "owner": map.get("owner").and_then(|v| v.as_str()).unwrap_or(owner), + "grants": grants, + "canned_acls": ["private", "public-read", "public-read-write", "authenticated-read"], + }) + } + _ => json!({ + "owner": owner, + "grants": [default_grant], + "canned_acls": ["private", "public-read", "public-read-write", "authenticated-read"], + }), + } +} + +fn parse_cors_value(value: Option<&Value>) -> Value { + let Some(value) = value else { + return json!({ "rules": [] }); + }; + + match value { + Value::String(xml) => { + let doc = match Document::parse(xml) { + Ok(doc) => doc, + Err(_) => return json!({ "rules": [] }), + }; + let rules = doc + .descendants() + .filter(|node| node.is_element() && node.tag_name().name() == "CORSRule") + .map(|rule| { + let allowed_origins = xml_children_texts(rule, "AllowedOrigin"); + let allowed_methods = xml_children_texts(rule, "AllowedMethod"); + let allowed_headers = xml_children_texts(rule, "AllowedHeader"); + let expose_headers = xml_children_texts(rule, "ExposeHeader"); + let max_age_seconds = + xml_child_text(rule, "MaxAgeSeconds").and_then(|v| v.parse::().ok()); + json!({ + "AllowedOrigins": allowed_origins, + "AllowedMethods": allowed_methods, + "AllowedHeaders": allowed_headers, + "ExposeHeaders": expose_headers, + "MaxAgeSeconds": max_age_seconds, + "allowed_origins": allowed_origins, + "allowed_methods": allowed_methods, + "allowed_headers": allowed_headers, + "expose_headers": expose_headers, + "max_age_seconds": max_age_seconds, + }) + }) + .collect::>(); + json!({ "rules": rules }) + } + Value::Array(rules) => json!({ "rules": rules }), + Value::Object(map) => { + if let Some(rules) = map.get("rules").and_then(|value| value.as_array()) { + json!({ "rules": rules }) + } else { + json!({ "rules": [map] }) + } + } + _ => json!({ "rules": [] }), + } +} + +fn parse_lifecycle_value(value: Option<&Value>) -> Value { + let Some(value) = value else { + return json!({ "rules": [] }); + }; + + match value { + Value::String(xml) => { + let doc = match Document::parse(xml) { + Ok(doc) => doc, + Err(_) => return json!({ "rules": [] }), + }; + let rules = doc + .descendants() + .filter(|node| node.is_element() && node.tag_name().name() == "Rule") + .map(|rule| { + let rule_id = xml_child_text(rule, "ID").unwrap_or_default(); + let status = xml_child_text(rule, "Status").unwrap_or_else(|| "Enabled".to_string()); + let prefix = xml_child(rule, "Filter") + .and_then(|filter| xml_child_text(filter, "Prefix")) + .or_else(|| xml_child_text(rule, "Prefix")) + .unwrap_or_default(); + let expiration_days = xml_child(rule, "Expiration") + .and_then(|node| xml_child_text(node, "Days")) + .and_then(|v| v.parse::().ok()); + let noncurrent_days = xml_child(rule, "NoncurrentVersionExpiration") + .and_then(|node| xml_child_text(node, "NoncurrentDays")) + .and_then(|v| v.parse::().ok()); + let abort_days = xml_child(rule, "AbortIncompleteMultipartUpload") + .and_then(|node| xml_child_text(node, "DaysAfterInitiation")) + .and_then(|v| v.parse::().ok()); + + json!({ + "ID": rule_id, + "Status": status, + "Filter": { "Prefix": prefix }, + "Expiration": expiration_days.map(|days| json!({ "Days": days })), + "NoncurrentVersionExpiration": noncurrent_days.map(|days| json!({ "NoncurrentDays": days })), + "AbortIncompleteMultipartUpload": abort_days.map(|days| json!({ "DaysAfterInitiation": days })), + "id": rule_id, + "status": status, + "prefix": prefix, + "expiration_days": expiration_days, + "noncurrent_days": noncurrent_days, + "abort_mpu_days": abort_days, + }) + }) + .collect::>(); + json!({ "rules": rules }) + } + Value::Array(rules) => json!({ "rules": rules }), + Value::Object(map) => { + if let Some(rules) = map.get("rules").and_then(|value| value.as_array()) { + json!({ "rules": rules }) + } else { + json!({ "rules": [map] }) + } + } + _ => json!({ "rules": [] }), + } +} + +fn bucket_acl_xml_for_canned(owner_id: &str, canned_acl: &str) -> Result { + let mut grants = vec![format!( + "{}{}FULL_CONTROL", + xml_escape(owner_id), + xml_escape(owner_id), + )]; + + match canned_acl { + "private" => {} + "public-read" => grants.push( + "http://acs.amazonaws.com/groups/global/AllUsersREAD".to_string() + ), + "public-read-write" => { + grants.push( + "http://acs.amazonaws.com/groups/global/AllUsersREAD".to_string() + ); + grants.push( + "http://acs.amazonaws.com/groups/global/AllUsersWRITE".to_string() + ); + } + "authenticated-read" => grants.push( + "http://acs.amazonaws.com/groups/global/AuthenticatedUsersREAD".to_string() + ), + _ => return Err(format!("Invalid canned ACL: {}", canned_acl)), + } + + Ok(format!( + "{}{}{}", + xml_escape(owner_id), + xml_escape(owner_id), + grants.join("") + )) +} + +fn cors_xml_from_rules(rules: &[Value]) -> String { + let mut xml = String::from( + "", + ); + for rule in rules { + xml.push_str(""); + for origin in value_to_string_vec(rule.get("AllowedOrigins"), "AllowedOrigin") { + xml.push_str(&format!( + "{}", + xml_escape(&origin) + )); + } + for method in value_to_string_vec(rule.get("AllowedMethods"), "AllowedMethod") { + xml.push_str(&format!( + "{}", + xml_escape(&method) + )); + } + for header in value_to_string_vec(rule.get("AllowedHeaders"), "AllowedHeader") { + xml.push_str(&format!( + "{}", + xml_escape(&header) + )); + } + for header in value_to_string_vec(rule.get("ExposeHeaders"), "ExposeHeader") { + xml.push_str(&format!( + "{}", + xml_escape(&header) + )); + } + if let Some(max_age) = rule.get("MaxAgeSeconds").and_then(|v| v.as_u64()) { + xml.push_str(&format!("{}", max_age)); + } + xml.push_str(""); + } + xml.push_str(""); + xml +} + +fn lifecycle_xml_from_rules(rules: &[Value]) -> String { + let mut xml = String::from( + "", + ); + for rule in rules { + xml.push_str(""); + + let id = rule.get("ID").and_then(|v| v.as_str()).unwrap_or_default(); + if !id.is_empty() { + xml.push_str(&format!("{}", xml_escape(id))); + } + + let status = rule + .get("Status") + .and_then(|v| v.as_str()) + .unwrap_or("Enabled"); + xml.push_str(&format!("{}", xml_escape(status))); + + let prefix = rule + .get("Filter") + .and_then(|v| v.get("Prefix")) + .and_then(|v| v.as_str()) + .or_else(|| rule.get("Prefix").and_then(|v| v.as_str())) + .unwrap_or_default(); + xml.push_str(""); + xml.push_str(&format!("{}", xml_escape(prefix))); + xml.push_str(""); + + if let Some(days) = rule + .get("Expiration") + .and_then(|v| v.get("Days")) + .and_then(|v| v.as_u64()) + { + xml.push_str(&format!("{}", days)); + } + + if let Some(days) = rule + .get("NoncurrentVersionExpiration") + .and_then(|v| v.get("NoncurrentDays")) + .and_then(|v| v.as_u64()) + { + xml.push_str(&format!( + "{}", + days + )); + } + + if let Some(days) = rule + .get("AbortIncompleteMultipartUpload") + .and_then(|v| v.get("DaysAfterInitiation")) + .and_then(|v| v.as_u64()) + { + xml.push_str(&format!( + "{}", + days + )); + } + + xml.push_str(""); + } + xml.push_str(""); + xml +} + +fn zip_dos_time(dt: DateTime) -> (u16, u16) { + let year = dt.year().clamp(1980, 2107) as u16; + let month = dt.month() as u16; + let day = dt.day() as u16; + let hour = dt.hour() as u16; + let minute = dt.minute() as u16; + let second = (dt.second() / 2) as u16; + let dos_time = (hour << 11) | (minute << 5) | second; + let dos_date = ((year - 1980) << 9) | (month << 5) | day; + (dos_time, dos_date) +} + +fn write_u16(buf: &mut Vec, value: u16) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +fn write_u32(buf: &mut Vec, value: u32) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +fn build_zip_archive(entries: Vec<(String, Vec, DateTime)>) -> Result, String> { + #[derive(Clone)] + struct CentralEntry { + name: Vec, + crc32: u32, + size: u32, + offset: u32, + mod_time: u16, + mod_date: u16, + } + + let mut output = Vec::new(); + let mut central_entries = Vec::new(); + + for (name, data, modified) in entries { + if data.len() > u32::MAX as usize { + return Err(format!("Object '{}' is too large for ZIP export", name)); + } + let offset = output.len(); + if offset > u32::MAX as usize { + return Err("ZIP archive is too large".to_string()); + } + + let name_bytes = name.into_bytes(); + let mut hasher = crc32fast::Hasher::new(); + hasher.update(&data); + let crc32 = hasher.finalize(); + let size = data.len() as u32; + let (mod_time, mod_date) = zip_dos_time(modified); + let flags = 0x0800u16; + + write_u32(&mut output, 0x04034b50); + write_u16(&mut output, 20); + write_u16(&mut output, flags); + write_u16(&mut output, 0); + write_u16(&mut output, mod_time); + write_u16(&mut output, mod_date); + write_u32(&mut output, crc32); + write_u32(&mut output, size); + write_u32(&mut output, size); + write_u16(&mut output, name_bytes.len() as u16); + write_u16(&mut output, 0); + output.extend_from_slice(&name_bytes); + output.extend_from_slice(&data); + + central_entries.push(CentralEntry { + name: name_bytes, + crc32, + size, + offset: offset as u32, + mod_time, + mod_date, + }); + } + + let central_start = output.len(); + for entry in ¢ral_entries { + write_u32(&mut output, 0x02014b50); + write_u16(&mut output, 20); + write_u16(&mut output, 20); + write_u16(&mut output, 0x0800); + write_u16(&mut output, 0); + write_u16(&mut output, entry.mod_time); + write_u16(&mut output, entry.mod_date); + write_u32(&mut output, entry.crc32); + write_u32(&mut output, entry.size); + write_u32(&mut output, entry.size); + write_u16(&mut output, entry.name.len() as u16); + write_u16(&mut output, 0); + write_u16(&mut output, 0); + write_u16(&mut output, 0); + write_u16(&mut output, 0); + write_u32(&mut output, 0); + write_u32(&mut output, entry.offset); + output.extend_from_slice(&entry.name); + } + + let central_size = output.len() - central_start; + if central_entries.len() > u16::MAX as usize + || central_start > u32::MAX as usize + || central_size > u32::MAX as usize + { + return Err("ZIP archive exceeds classic ZIP limits".to_string()); + } + + write_u32(&mut output, 0x06054b50); + write_u16(&mut output, 0); + write_u16(&mut output, 0); + write_u16(&mut output, central_entries.len() as u16); + write_u16(&mut output, central_entries.len() as u16); + write_u32(&mut output, central_size as u32); + write_u32(&mut output, central_start as u32); + write_u16(&mut output, 0); + + Ok(output) +} + +fn dangerous_preview_content_type(content_type: Option<&str>, key: &str) -> Option { + let guessed = content_type + .map(|value| value.to_ascii_lowercase()) + .unwrap_or_else(|| { + mime_guess::from_path(key) + .first_raw() + .unwrap_or("application/octet-stream") + .to_ascii_lowercase() + }); + + match guessed.split(';').next().unwrap_or_default().trim() { + "text/html" + | "text/xml" + | "application/xml" + | "application/xhtml+xml" + | "image/svg+xml" => Some("text/plain; charset=utf-8".to_string()), + _ => None, + } +} + +async fn parse_json_body(body: Body) -> Result { + let bytes = to_bytes(body, usize::MAX) + .await + .map_err(|_| json_error(StatusCode::BAD_REQUEST, "Failed to read request body"))?; + serde_json::from_slice::(&bytes) + .map_err(|e| json_error(StatusCode::BAD_REQUEST, format!("Invalid JSON body: {}", e))) +} + +#[derive(Deserialize, Default)] +pub struct ListObjectsQuery { + #[serde(default)] + pub max_keys: Option, + #[serde(default)] + pub continuation_token: Option, + #[serde(default)] + pub prefix: Option, + #[serde(default)] + pub start_after: Option, +} + +pub async fn list_bucket_objects( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + if !matches!(state.storage.bucket_exists(&bucket_name).await, Ok(true)) { + return json_error(StatusCode::NOT_FOUND, "Bucket not found"); + } + + let max_keys = q.max_keys.unwrap_or(1000).min(5000); + let params = ListParams { + max_keys, + continuation_token: q.continuation_token.clone(), + prefix: q.prefix.clone(), + start_after: q.start_after.clone(), + }; + + let versioning_enabled = state + .storage + .is_versioning_enabled(&bucket_name) + .await + .unwrap_or(false); + + let stats = state.storage.bucket_stats(&bucket_name).await.ok(); + let total_count = stats.as_ref().map(|s| s.objects).unwrap_or(0); + + match state.storage.list_objects(&bucket_name, ¶ms).await { + Ok(res) => { + let objects: Vec = res + .objects + .iter() + .map(|o| { + json!({ + "key": o.key, + "size": o.size, + "last_modified": o.last_modified.to_rfc3339(), + "last_modified_iso": o.last_modified.to_rfc3339(), + "last_modified_display": o.last_modified.format("%Y-%m-%d %H:%M:%S").to_string(), + "etag": o.etag.clone().unwrap_or_default(), + "storage_class": o.storage_class.clone().unwrap_or_else(|| "STANDARD".to_string()), + "content_type": o.content_type.clone().unwrap_or_default(), + "download_url": build_ui_object_url(&bucket_name, &o.key, "download"), + "preview_url": build_ui_object_url(&bucket_name, &o.key, "preview"), + "delete_endpoint": build_ui_object_url(&bucket_name, &o.key, "delete"), + "presign_endpoint": build_ui_object_url(&bucket_name, &o.key, "presign"), + "metadata_url": build_ui_object_url(&bucket_name, &o.key, "metadata"), + "versions_endpoint": build_ui_object_url(&bucket_name, &o.key, "versions"), + "restore_template": format!( + "/ui/buckets/{}/objects/{}/restore/VERSION_ID_PLACEHOLDER", + bucket_name, + encode_object_key(&o.key) + ), + "tags_url": build_ui_object_url(&bucket_name, &o.key, "tags"), + "copy_url": build_ui_object_url(&bucket_name, &o.key, "copy"), + "move_url": build_ui_object_url(&bucket_name, &o.key, "move"), + }) + }) + .collect(); + + Json(json!({ + "versioning_enabled": versioning_enabled, + "total_count": total_count, + "is_truncated": res.is_truncated, + "next_continuation_token": res.next_continuation_token, + "url_templates": url_templates_for(&bucket_name), + "objects": objects, + })) + .into_response() + } + Err(e) => storage_json_error(e), + } +} + +#[derive(Deserialize, Default)] +pub struct StreamObjectsQuery { + #[serde(default)] + pub prefix: Option, + #[serde(default)] + pub delimiter: Option, +} + +pub async fn stream_bucket_objects( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + if !matches!(state.storage.bucket_exists(&bucket_name).await, Ok(true)) { + return (StatusCode::NOT_FOUND, "Bucket not found").into_response(); + } + + let versioning_enabled = state + .storage + .is_versioning_enabled(&bucket_name) + .await + .unwrap_or(false); + let stats = state.storage.bucket_stats(&bucket_name).await.ok(); + let total_count = stats.as_ref().map(|s| s.objects).unwrap_or(0); + + let mut lines: Vec = Vec::new(); + lines.push( + json!({ + "type": "meta", + "url_templates": url_templates_for(&bucket_name), + "versioning_enabled": versioning_enabled, + }) + .to_string(), + ); + lines.push(json!({ "type": "count", "total_count": total_count }).to_string()); + + let use_delimiter = q.delimiter.as_deref() == Some("/"); + let prefix = q.prefix.clone().unwrap_or_default(); + + if use_delimiter { + let params = myfsio_common::types::ShallowListParams { + prefix: prefix.clone(), + delimiter: "/".to_string(), + max_keys: 5000, + continuation_token: None, + }; + match state + .storage + .list_objects_shallow(&bucket_name, ¶ms) + .await + { + Ok(res) => { + for p in &res.common_prefixes { + lines.push(json!({ "type": "folder", "prefix": p }).to_string()); + } + for o in &res.objects { + lines.push( + json!({ + "type": "object", + "key": o.key, + "size": o.size, + "last_modified": o.last_modified.to_rfc3339(), + "last_modified_iso": o.last_modified.to_rfc3339(), + "last_modified_display": o.last_modified.format("%Y-%m-%d %H:%M:%S").to_string(), + "etag": o.etag.clone().unwrap_or_default(), + "storage_class": o.storage_class.clone().unwrap_or_else(|| "STANDARD".to_string()), + }) + .to_string(), + ); + } + } + Err(e) => lines.push(json!({ "type": "error", "error": e.to_string() }).to_string()), + } + } else { + let mut token: Option = None; + loop { + let params = ListParams { + max_keys: 1000, + continuation_token: token.clone(), + prefix: if prefix.is_empty() { + None + } else { + Some(prefix.clone()) + }, + start_after: None, + }; + match state.storage.list_objects(&bucket_name, ¶ms).await { + Ok(res) => { + for o in &res.objects { + lines.push( + json!({ + "type": "object", + "key": o.key, + "size": o.size, + "last_modified": o.last_modified.to_rfc3339(), + "last_modified_iso": o.last_modified.to_rfc3339(), + "last_modified_display": o.last_modified.format("%Y-%m-%d %H:%M:%S").to_string(), + "etag": o.etag.clone().unwrap_or_default(), + "storage_class": o.storage_class.clone().unwrap_or_else(|| "STANDARD".to_string()), + }) + .to_string(), + ); + } + if !res.is_truncated || res.next_continuation_token.is_none() { + break; + } + token = res.next_continuation_token; + } + Err(e) => { + lines.push(json!({ "type": "error", "error": e.to_string() }).to_string()); + break; + } + } + } + } + + lines.push(json!({ "type": "done" }).to_string()); + + let body = lines.join("\n") + "\n"; + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + "application/x-ndjson; charset=utf-8".parse().unwrap(), + ); + (StatusCode::OK, headers, body).into_response() +} + +pub async fn list_bucket_folders( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + if !matches!(state.storage.bucket_exists(&bucket_name).await, Ok(true)) { + return json_error(StatusCode::NOT_FOUND, "Bucket not found"); + } + + let prefix = q.prefix.clone().unwrap_or_default(); + let params = myfsio_common::types::ShallowListParams { + prefix: prefix.clone(), + delimiter: "/".to_string(), + max_keys: 5000, + continuation_token: None, + }; + match state + .storage + .list_objects_shallow(&bucket_name, ¶ms) + .await + { + Ok(res) => Json(json!({ + "prefixes": res.common_prefixes, + "current_prefix": prefix, + })) + .into_response(), + Err(e) => storage_json_error(e), + } +} + +pub async fn list_copy_targets( + State(state): State, + Extension(_session): Extension, + Path(_bucket_name): Path, +) -> Response { + let buckets: Vec = state + .storage + .list_buckets() + .await + .map(|list| list.into_iter().map(|b| b.name).collect()) + .unwrap_or_default(); + Json(json!({ "buckets": buckets })).into_response() +} + +pub async fn json_not_implemented() -> Response { + json_error( + StatusCode::NOT_IMPLEMENTED, + "This feature is not implemented yet", + ) +} + +#[derive(Deserialize)] +pub struct ConnectionTestPayload { + pub endpoint_url: String, + pub access_key: String, + pub secret_key: String, + #[serde(default = "default_region")] + pub region: String, +} + +fn default_region() -> String { + "us-east-1".to_string() +} + +pub async fn test_connection( + State(state): State, + Extension(_session): Extension, + body: Body, +) -> Response { + let payload: ConnectionTestPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(_) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "status": "error", + "message": "Invalid JSON payload", + })), + ) + .into_response() + } + }; + + if payload.endpoint_url.trim().is_empty() + || payload.access_key.trim().is_empty() + || payload.secret_key.trim().is_empty() + { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "status": "error", + "message": "Missing credentials", + })), + ) + .into_response(); + } + + let connection = RemoteConnection { + id: "test".to_string(), + name: "Test".to_string(), + endpoint_url: payload.endpoint_url.trim().to_string(), + access_key: payload.access_key.trim().to_string(), + secret_key: payload.secret_key.trim().to_string(), + region: payload.region.trim().to_string(), + }; + + if state.replication.check_endpoint(&connection).await { + Json(json!({ + "status": "ok", + "message": "Connection successful", + })) + .into_response() + } else { + ( + StatusCode::BAD_REQUEST, + Json(json!({ + "status": "error", + "message": format!("Connection failed or endpoint is unreachable: {}", connection.endpoint_url), + })), + ) + .into_response() + } +} + +pub async fn connection_health( + State(state): State, + Extension(_session): Extension, + Path(connection_id): Path, +) -> Response { + let Some(connection) = state.connections.get(&connection_id) else { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "healthy": false, + "error": "Connection not found", + })), + ) + .into_response(); + }; + + let healthy = state.replication.check_endpoint(&connection).await; + Json(json!({ + "healthy": healthy, + "error": if healthy { + Value::Null + } else { + Value::String(format!("Cannot reach endpoint: {}", connection.endpoint_url)) + } + })) + .into_response() +} + +async fn peer_health_payload(state: &AppState, site_id: &str) -> Result { + let Some(registry) = &state.site_registry else { + return Err(json_error( + StatusCode::NOT_FOUND, + "Site registry not available", + )); + }; + let Some(peer) = registry.get_peer(site_id) else { + return Err(json_error(StatusCode::NOT_FOUND, "Peer not found")); + }; + + let checked_at = chrono::Utc::now().timestamp_millis() as f64 / 1000.0; + let mut healthy = false; + let mut error: Option = None; + + if let Some(connection_id) = peer.connection_id.as_deref() { + if let Some(connection) = state.connections.get(connection_id) { + healthy = state.replication.check_endpoint(&connection).await; + if !healthy { + error = Some(format!( + "Cannot reach endpoint: {}", + connection.endpoint_url + )); + } + } else { + error = Some(format!("Connection '{}' not found", connection_id)); + } + } else { + error = Some("No connection configured for this peer".to_string()); + } + + registry.update_health(site_id, healthy); + Ok(json!({ + "site_id": site_id, + "is_healthy": healthy, + "checked_at": checked_at, + "error": error, + })) +} + +pub async fn peer_health( + State(state): State, + Extension(_session): Extension, + Path(site_id): Path, +) -> Response { + match peer_health_payload(&state, &site_id).await { + Ok(payload) => Json(payload).into_response(), + Err(response) => response, + } +} + +pub async fn peer_sync_stats( + State(state): State, + Extension(_session): Extension, + Path(site_id): Path, +) -> Response { + let Some(registry) = &state.site_registry else { + return json_error(StatusCode::NOT_FOUND, "Site registry not available"); + }; + let Some(peer) = registry.get_peer(&site_id) else { + return json_error(StatusCode::NOT_FOUND, "Peer not found"); + }; + let Some(connection_id) = peer.connection_id.as_deref() else { + return json_error(StatusCode::BAD_REQUEST, "No connection configured"); + }; + + let rules = state.replication.list_rules(); + let mut buckets: Vec = Vec::new(); + let mut buckets_syncing = 0u64; + let mut objects_synced = 0u64; + let mut objects_pending = 0u64; + let mut objects_failed = 0u64; + let mut bytes_synced = 0u64; + let mut last_sync_at: Option = None; + + for rule in rules + .into_iter() + .filter(|rule| rule.target_connection_id == connection_id) + { + buckets_syncing += 1; + objects_synced += rule.stats.objects_synced; + objects_pending += rule.stats.objects_pending; + bytes_synced += rule.stats.bytes_synced; + if let Some(sync_at) = rule.stats.last_sync_at { + if last_sync_at + .map(|current| sync_at > current) + .unwrap_or(true) + { + last_sync_at = Some(sync_at); + } + } + + let failures = state.replication.get_failure_count(&rule.bucket_name) as u64; + objects_failed += failures; + buckets.push(json!({ + "bucket_name": rule.bucket_name, + "target_bucket": rule.target_bucket, + "mode": rule.mode, + "enabled": rule.enabled, + "last_sync_at": rule.stats.last_sync_at, + "objects_synced": rule.stats.objects_synced, + "objects_pending": rule.stats.objects_pending, + "failures": failures, + })); + } + + Json(json!({ + "buckets_syncing": buckets_syncing, + "objects_synced": objects_synced, + "objects_pending": objects_pending, + "objects_failed": objects_failed, + "bytes_synced": bytes_synced, + "last_sync_at": last_sync_at, + "buckets": buckets, + })) + .into_response() +} + +pub async fn peer_bidirectional_status( + State(state): State, + Extension(_session): Extension, + Path(site_id): Path, +) -> Response { + let Some(registry) = &state.site_registry else { + return json_error(StatusCode::NOT_FOUND, "Site registry not available"); + }; + let Some(peer) = registry.get_peer(&site_id) else { + return json_error(StatusCode::NOT_FOUND, "Peer not found"); + }; + + let local_site = registry.get_local_site(); + let local_bidirectional_rules: Vec = state + .replication + .list_rules() + .into_iter() + .filter(|rule| { + peer.connection_id + .as_deref() + .map(|connection_id| rule.target_connection_id == connection_id) + .unwrap_or(false) + && rule.mode == crate::services::replication::MODE_BIDIRECTIONAL + }) + .map(|rule| { + json!({ + "bucket_name": rule.bucket_name, + "target_bucket": rule.target_bucket, + "enabled": rule.enabled, + }) + }) + .collect(); + + let mut result = json!({ + "site_id": site_id, + "local_site_id": local_site.as_ref().map(|site| site.site_id.clone()), + "local_endpoint": local_site.as_ref().map(|site| site.endpoint.clone()), + "local_bidirectional_rules": local_bidirectional_rules, + "local_site_sync_enabled": state.config.site_sync_enabled, + "remote_status": Value::Null, + "issues": Vec::::new(), + "is_fully_configured": false, + }); + + if local_site + .as_ref() + .map(|site| site.site_id.trim().is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + json!({ + "code": "NO_LOCAL_SITE_ID", + "message": "Local site identity not configured", + "severity": "error", + }), + ); + } + if local_site + .as_ref() + .map(|site| site.endpoint.trim().is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + json!({ + "code": "NO_LOCAL_ENDPOINT", + "message": "Local site endpoint not configured (remote site cannot reach back)", + "severity": "error", + }), + ); + } + + let Some(connection_id) = peer.connection_id.as_deref() else { + push_issue( + &mut result, + json!({ + "code": "NO_CONNECTION", + "message": "No connection configured for this peer", + "severity": "error", + }), + ); + return Json(result).into_response(); + }; + + let Some(connection) = state.connections.get(connection_id) else { + push_issue( + &mut result, + json!({ + "code": "CONNECTION_NOT_FOUND", + "message": format!("Connection '{}' not found", connection_id), + "severity": "error", + }), + ); + return Json(result).into_response(); + }; + + if result["local_bidirectional_rules"] + .as_array() + .map(|rules| rules.is_empty()) + .unwrap_or(true) + { + push_issue( + &mut result, + json!({ + "code": "NO_LOCAL_BIDIRECTIONAL_RULES", + "message": "No bidirectional replication rules configured on this site", + "severity": "warning", + }), + ); + } + if !state.config.site_sync_enabled { + push_issue( + &mut result, + json!({ + "code": "SITE_SYNC_DISABLED", + "message": "Site sync worker is disabled (SITE_SYNC_ENABLED=false). Pull operations will not work.", + "severity": "warning", + }), + ); + } + if !state.replication.check_endpoint(&connection).await { + push_issue( + &mut result, + json!({ + "code": "REMOTE_UNREACHABLE", + "message": "Remote endpoint is not reachable", + "severity": "error", + }), + ); + return Json(result).into_response(); + } + + let admin_url = format!( + "{}/admin/sites", + connection.endpoint_url.trim_end_matches('/') + ); + match reqwest::Client::new() + .get(&admin_url) + .header("accept", "application/json") + .header("x-access-key", &connection.access_key) + .header("x-secret-key", &connection.secret_key) + .timeout(std::time::Duration::from_secs(10)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => match resp.json::().await { + Ok(remote_data) => { + let remote_local = remote_data.get("local").cloned().unwrap_or(Value::Null); + let remote_peers = remote_data + .get("peers") + .and_then(|value| value.as_array()) + .cloned() + .unwrap_or_default(); + let mut has_peer_for_us = false; + let mut peer_connection_configured = false; + + for remote_peer in &remote_peers { + let matches_site = local_site + .as_ref() + .map(|site| { + remote_peer.get("site_id").and_then(|v| v.as_str()) + == Some(site.site_id.as_str()) + || remote_peer.get("endpoint").and_then(|v| v.as_str()) + == Some(site.endpoint.as_str()) + }) + .unwrap_or(false); + if matches_site { + has_peer_for_us = true; + peer_connection_configured = remote_peer + .get("connection_id") + .and_then(|v| v.as_str()) + .map(|v| !v.trim().is_empty()) + .unwrap_or(false); + break; + } + } + + result["remote_status"] = json!({ + "reachable": true, + "local_site": remote_local, + "site_sync_enabled": Value::Null, + "has_peer_for_us": has_peer_for_us, + "peer_connection_configured": peer_connection_configured, + "has_bidirectional_rules_for_us": Value::Null, + }); + + if !has_peer_for_us { + push_issue( + &mut result, + json!({ + "code": "REMOTE_NO_PEER_FOR_US", + "message": "Remote site does not have this site registered as a peer", + "severity": "error", + }), + ); + } else if !peer_connection_configured { + push_issue( + &mut result, + json!({ + "code": "REMOTE_NO_CONNECTION_FOR_US", + "message": "Remote site has us as peer but no connection configured (cannot push back)", + "severity": "error", + }), + ); + } + } + Err(_) => { + result["remote_status"] = json!({ + "reachable": true, + "invalid_response": true, + }); + push_issue( + &mut result, + json!({ + "code": "REMOTE_INVALID_RESPONSE", + "message": "Remote admin API returned invalid JSON", + "severity": "warning", + }), + ); + } + }, + Ok(resp) + if resp.status() == StatusCode::UNAUTHORIZED + || resp.status() == StatusCode::FORBIDDEN => + { + result["remote_status"] = json!({ + "reachable": true, + "admin_access_denied": true, + }); + push_issue( + &mut result, + json!({ + "code": "REMOTE_ADMIN_ACCESS_DENIED", + "message": "Cannot verify remote configuration (admin access denied)", + "severity": "warning", + }), + ); + } + Ok(resp) => { + result["remote_status"] = json!({ + "reachable": true, + "admin_api_error": resp.status().as_u16(), + }); + push_issue( + &mut result, + json!({ + "code": "REMOTE_ADMIN_API_ERROR", + "message": format!("Remote admin API returned status {}", resp.status().as_u16()), + "severity": "warning", + }), + ); + } + Err(_) => { + result["remote_status"] = json!({ + "reachable": false, + "error": "Connection failed", + }); + push_issue( + &mut result, + json!({ + "code": "REMOTE_ADMIN_UNREACHABLE", + "message": "Could not reach remote admin API", + "severity": "warning", + }), + ); + } + } + + let has_errors = result["issues"] + .as_array() + .map(|items| { + items.iter().any(|issue| { + issue.get("severity").and_then(|value| value.as_str()) == Some("error") + }) + }) + .unwrap_or(true); + result["is_fully_configured"] = json!( + !has_errors + && result["local_bidirectional_rules"] + .as_array() + .map(|rules| !rules.is_empty()) + .unwrap_or(false) + ); + + Json(result).into_response() +} + +#[derive(Clone, Copy)] +struct MetricsSettingsSnapshot { + enabled: bool, + retention_hours: u64, + interval_minutes: u64, +} + +static METRICS_SETTINGS: OnceLock> = OnceLock::new(); + +fn metrics_settings_snapshot(state: &AppState) -> MetricsSettingsSnapshot { + *METRICS_SETTINGS + .get_or_init(|| { + Mutex::new(MetricsSettingsSnapshot { + enabled: state.config.metrics_history_enabled, + retention_hours: state.config.metrics_history_retention_hours, + interval_minutes: state.config.metrics_history_interval_minutes, + }) + }) + .lock() + .unwrap() +} + +pub async fn metrics_settings(State(state): State) -> Response { + let settings = metrics_settings_snapshot(&state); + Json(json!({ + "enabled": settings.enabled, + "retention_hours": settings.retention_hours, + "interval_minutes": settings.interval_minutes, + })) + .into_response() +} + +pub async fn update_metrics_settings(State(state): State, body: Body) -> Response { + let payload: Value = parse_json_body(body).await.unwrap_or_else(|_| json!({})); + let mut settings = METRICS_SETTINGS + .get_or_init(|| { + Mutex::new(MetricsSettingsSnapshot { + enabled: state.config.metrics_history_enabled, + retention_hours: state.config.metrics_history_retention_hours, + interval_minutes: state.config.metrics_history_interval_minutes, + }) + }) + .lock() + .unwrap(); + let enabled = payload + .get("enabled") + .and_then(|value| value.as_bool()) + .unwrap_or(settings.enabled); + let retention_hours = payload + .get("retention_hours") + .and_then(|value| value.as_u64()) + .unwrap_or(settings.retention_hours) + .max(1); + let interval_minutes = payload + .get("interval_minutes") + .and_then(|value| value.as_u64()) + .unwrap_or(settings.interval_minutes) + .max(1); + *settings = MetricsSettingsSnapshot { + enabled, + retention_hours, + interval_minutes, + }; + + Json(json!({ + "enabled": enabled, + "retention_hours": retention_hours, + "interval_minutes": interval_minutes, + })) + .into_response() +} + +#[derive(Deserialize, Default)] +struct MultipartInitPayload { + #[serde(default)] + object_key: String, + #[serde(default)] + metadata: Option>, +} + +pub async fn upload_object( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + headers: HeaderMap, + body: Body, +) -> Response { + let content_type = match headers + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some(value) + if value + .to_ascii_lowercase() + .starts_with("multipart/form-data") => + { + value.to_string() + } + _ => return json_error(StatusCode::BAD_REQUEST, "Expected multipart form upload"), + }; + + let boundary = match multer::parse_boundary(&content_type) { + Ok(value) => value, + Err(_) => return json_error(StatusCode::BAD_REQUEST, "Missing multipart boundary"), + }; + + let stream = BodyStream::new(body) + .map_ok(|frame| frame.into_data().unwrap_or_default()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)); + let mut multipart = multer::Multipart::new(stream, boundary); + + let mut object_key: Option = None; + let mut metadata_raw: Option = None; + let mut file_name: Option = None; + let mut file_content_type: Option = None; + let mut file_bytes: Option> = None; + + while let Some(field) = match multipart.next_field().await { + Ok(field) => field, + Err(e) => { + return json_error( + StatusCode::BAD_REQUEST, + format!("Malformed multipart body: {}", e), + ) + } + } { + let name = field.name().unwrap_or_default().to_string(); + match name.as_str() { + "object_key" => match field.text().await { + Ok(value) if !value.trim().is_empty() => { + object_key = Some(value.trim().to_string()) + } + _ => {} + }, + "metadata" => match field.text().await { + Ok(value) if !value.trim().is_empty() => metadata_raw = Some(value), + _ => {} + }, + "object" => { + file_name = field.file_name().map(|s| s.to_string()); + file_content_type = field.content_type().map(|mime| mime.to_string()); + match field.bytes().await { + Ok(bytes) => file_bytes = Some(bytes.to_vec()), + Err(e) => { + return json_error( + StatusCode::BAD_REQUEST, + format!("Failed to read upload: {}", e), + ) + } + } + } + _ => { + let _ = field.bytes().await; + } + } + } + + let bytes = match file_bytes { + Some(bytes) if !bytes.is_empty() => bytes, + _ => return json_error(StatusCode::BAD_REQUEST, "Choose a file to upload"), + }; + + let key = object_key + .or(file_name.clone()) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| json_error(StatusCode::BAD_REQUEST, "Object key is required")); + let key = match key { + Ok(key) => key, + Err(response) => return response, + }; + + let metadata = if let Some(raw) = metadata_raw { + match serde_json::from_str::>(&raw) { + Ok(map) => Some( + map.into_iter() + .map(|(k, v)| (k, v.as_str().unwrap_or(&v.to_string()).to_string())) + .collect::>(), + ), + Err(_) => return json_error(StatusCode::BAD_REQUEST, "Metadata must be a JSON object"), + } + } else { + None + }; + + let mut upload_headers = HeaderMap::new(); + if let Some(content_type) = file_content_type.as_deref() { + if let Ok(value) = content_type.parse() { + upload_headers.insert(header::CONTENT_TYPE, value); + } + } + if let Some(metadata) = &metadata { + for (key, value) in metadata { + let header_name = format!("x-amz-meta-{}", key); + if let Ok(name) = header_name.parse::() { + if let Ok(value) = value.parse() { + upload_headers.insert(name, value); + } + } + } + } + + let response = handlers::put_object( + State(state), + Path((bucket_name.clone(), key.clone())), + Query(ObjectQuery::default()), + upload_headers, + Body::from(bytes), + ) + .await; + + if !response.status().is_success() { + return response; + } + + let mut message = format!("Uploaded '{}'", key); + if metadata.is_some() { + message.push_str(" with metadata"); + } + json_ok(json!({ + "status": "ok", + "message": message, + "key": key, + })) +} + +pub async fn initiate_multipart_upload( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: MultipartInitPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + let object_key = payload.object_key.trim(); + if object_key.is_empty() { + return json_error(StatusCode::BAD_REQUEST, "object_key is required"); + } + + match state + .storage + .initiate_multipart(&bucket_name, object_key, payload.metadata) + .await + { + Ok(upload_id) => json_ok(json!({ "upload_id": upload_id })), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +pub struct MultipartPartQuery { + #[serde(rename = "partNumber")] + part_number: Option, +} + +pub async fn upload_multipart_part( + State(state): State, + Extension(_session): Extension, + Path((bucket_name, upload_id)): Path<(String, String)>, + Query(query): Query, + body: Body, +) -> Response { + let Some(part_number) = query.part_number else { + return json_error(StatusCode::BAD_REQUEST, "partNumber is required"); + }; + if !(1..=10_000).contains(&part_number) { + return json_error( + StatusCode::BAD_REQUEST, + "partNumber must be between 1 and 10000", + ); + } + + let bytes = match to_bytes(body, usize::MAX).await { + Ok(bytes) if !bytes.is_empty() => bytes, + Ok(_) => return json_error(StatusCode::BAD_REQUEST, "Empty request body"), + Err(_) => return json_error(StatusCode::BAD_REQUEST, "Failed to read request body"), + }; + let reader: myfsio_storage::traits::AsyncReadStream = Box::pin(Cursor::new(bytes.to_vec())); + match state + .storage + .upload_part(&bucket_name, &upload_id, part_number, reader) + .await + { + Ok(etag) => json_ok(json!({ "etag": etag, "part_number": part_number })), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct CompleteMultipartPayload { + #[serde(default)] + parts: Vec, +} + +#[derive(Deserialize, Default)] +struct CompleteMultipartPartPayload { + #[serde(default, alias = "PartNumber")] + part_number: u32, + #[serde(default, alias = "ETag")] + etag: String, +} + +pub async fn complete_multipart_upload( + State(state): State, + Extension(_session): Extension, + Path((bucket_name, upload_id)): Path<(String, String)>, + body: Body, +) -> Response { + let payload: CompleteMultipartPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + if payload.parts.is_empty() { + return json_error(StatusCode::BAD_REQUEST, "parts array required"); + } + + let parts = payload + .parts + .iter() + .map(|part| PartInfo { + part_number: part.part_number, + etag: part.etag.trim_matches('"').to_string(), + }) + .collect::>(); + + match state + .storage + .complete_multipart(&bucket_name, &upload_id, &parts) + .await + { + Ok(meta) => json_ok(json!({ + "key": meta.key, + "size": meta.size, + "etag": meta.etag.unwrap_or_default(), + "last_modified": meta.last_modified.to_rfc3339(), + })), + Err(err) => storage_json_error(err), + } +} + +pub async fn abort_multipart_upload( + State(state): State, + Extension(_session): Extension, + Path((bucket_name, upload_id)): Path<(String, String)>, +) -> Response { + match state + .storage + .abort_multipart(&bucket_name, &upload_id) + .await + { + Ok(()) => json_ok(json!({ "status": "aborted" })), + Err(err) => storage_json_error(err), + } +} + +async fn get_bucket_config_json( + state: &AppState, + bucket: &str, +) -> Result { + state.storage.get_bucket_config(bucket).await +} + +pub async fn bucket_acl( + State(state): State, + Extension(session): Extension, + Path(bucket_name): Path, +) -> Response { + match get_bucket_config_json(&state, &bucket_name).await { + Ok(config) => Json(parse_acl_value( + config.acl.as_ref(), + &owner_id_or_default(&session), + )) + .into_response(), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct BucketAclPayload { + #[serde(default)] + canned_acl: String, +} + +pub async fn update_bucket_acl( + State(state): State, + Extension(session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: BucketAclPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + if payload.canned_acl.trim().is_empty() { + return json_error(StatusCode::BAD_REQUEST, "canned_acl is required"); + } + + let acl_xml = match bucket_acl_xml_for_canned( + &owner_id_or_default(&session), + payload.canned_acl.trim(), + ) { + Ok(xml) => xml, + Err(message) => return json_error(StatusCode::BAD_REQUEST, message), + }; + + match state.storage.get_bucket_config(&bucket_name).await { + Ok(mut config) => { + config.acl = Some(Value::String(acl_xml)); + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => json_ok(json!({ + "status": "ok", + "message": format!("ACL set to {}", payload.canned_acl.trim()), + })), + Err(err) => storage_json_error(err), + } + } + Err(err) => storage_json_error(err), + } +} + +pub async fn bucket_cors( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + match get_bucket_config_json(&state, &bucket_name).await { + Ok(config) => Json(parse_cors_value(config.cors.as_ref())).into_response(), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct BucketCorsPayload { + #[serde(default)] + rules: Vec, +} + +pub async fn update_bucket_cors( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: BucketCorsPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + match state.storage.get_bucket_config(&bucket_name).await { + Ok(mut config) => { + config.cors = if payload.rules.is_empty() { + None + } else { + Some(Value::String(cors_xml_from_rules(&payload.rules))) + }; + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => json_ok(json!({ + "status": "ok", + "message": "CORS configuration saved", + "rules": payload.rules, + })), + Err(err) => storage_json_error(err), + } + } + Err(err) => storage_json_error(err), + } +} + +pub async fn bucket_lifecycle( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + match get_bucket_config_json(&state, &bucket_name).await { + Ok(config) => Json(parse_lifecycle_value(config.lifecycle.as_ref())).into_response(), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct BucketLifecyclePayload { + #[serde(default)] + rules: Vec, +} + +pub async fn update_bucket_lifecycle( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: BucketLifecyclePayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + match state.storage.get_bucket_config(&bucket_name).await { + Ok(mut config) => { + config.lifecycle = if payload.rules.is_empty() { + None + } else { + Some(Value::String(lifecycle_xml_from_rules(&payload.rules))) + }; + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => json_ok(json!({ + "status": "ok", + "message": "Lifecycle rules saved", + "rules": payload.rules, + })), + Err(err) => storage_json_error(err), + } + } + Err(err) => storage_json_error(err), + } +} + +async fn serve_object_download_or_preview( + state: AppState, + bucket: String, + key: String, + headers: HeaderMap, + is_download: bool, +) -> Response { + let content_type = state + .storage + .head_object(&bucket, &key) + .await + .ok() + .and_then(|meta| meta.content_type); + + let mut query = ObjectQuery::default(); + if is_download { + query.response_content_disposition = Some(format!( + "attachment; filename=\"{}\"", + safe_attachment_filename(&key) + )); + } else if let Some(forced) = dangerous_preview_content_type(content_type.as_deref(), &key) { + query.response_content_type = Some(forced); + } + + let mut response = + handlers::get_object(State(state), Path((bucket, key)), Query(query), headers).await; + response + .headers_mut() + .insert("x-content-type-options", "nosniff".parse().unwrap()); + response +} + +async fn object_metadata_json(state: &AppState, bucket: &str, key: &str) -> Response { + let head = match state.storage.head_object(bucket, key).await { + Ok(meta) => meta, + Err(err) => return storage_json_error(err), + }; + let metadata = state + .storage + .get_object_metadata(bucket, key) + .await + .unwrap_or_default(); + + let mut out = metadata.clone(); + if let Some(content_type) = head.content_type { + out.insert("Content-Type".to_string(), content_type); + } + let display_length = metadata + .get("__size__") + .cloned() + .unwrap_or_else(|| head.size.to_string()); + out.insert("Content-Length".to_string(), display_length); + if let Some(algorithm) = metadata.get("x-amz-server-side-encryption") { + out.insert( + "x-amz-server-side-encryption".to_string(), + algorithm.to_string(), + ); + } + Json(json!({ "metadata": out })).into_response() +} + +async fn object_versions_json(state: &AppState, bucket: &str, key: &str) -> Response { + match read_version_manifests_for_object(state, bucket, key) { + Ok(entries) => Json(json!({ + "versions": entries.into_iter().map(|entry| manifest_to_json(&entry)).collect::>(), + })) + .into_response(), + Err(err) => json_error(StatusCode::BAD_REQUEST, err), + } +} + +async fn object_tags_json(state: &AppState, bucket: &str, key: &str) -> Response { + match state.storage.get_object_tags(bucket, key).await { + Ok(tags) => Json(json!({ + "tags": tags.into_iter().map(|tag| json!({ "Key": tag.key, "Value": tag.value })).collect::>(), + })) + .into_response(), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct PresignPayload { + #[serde(default = "default_presign_method")] + method: String, + #[serde(default)] + expires_in: Option, +} + +fn default_presign_method() -> String { + "GET".to_string() +} + +async fn object_presign_json( + state: &AppState, + session: &SessionHandle, + bucket: &str, + key: &str, + body: Body, +) -> Response { + let payload: PresignPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + let method = payload.method.trim().to_ascii_uppercase(); + if !matches!(method.as_str(), "GET" | "PUT" | "DELETE") { + return json_error( + StatusCode::BAD_REQUEST, + "Method must be GET, PUT, or DELETE", + ); + } + + let access_key = match current_access_key(session) { + Some(key) => key, + None => return json_error(StatusCode::FORBIDDEN, "Missing authenticated session"), + }; + let secret_key = match state.iam.get_secret_key(&access_key) { + Some(secret) => secret, + None => { + return json_error( + StatusCode::FORBIDDEN, + "Session credentials are no longer valid", + ) + } + }; + + let min_expiry = state.config.presigned_url_min_expiry; + let max_expiry = state.config.presigned_url_max_expiry; + let expires = payload + .expires_in + .unwrap_or(900) + .clamp(min_expiry, max_expiry); + + let api_base = parse_api_base(state); + let parsed = match reqwest::Url::parse(&api_base) { + Ok(url) => url, + Err(err) => { + return json_error( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Invalid API_BASE_URL: {}", err), + ) + } + }; + let host = match parsed.host_str() { + Some(host) => { + if let Some(port) = parsed.port() { + format!("{}:{}", host, port) + } else { + host.to_string() + } + } + None => { + return json_error( + StatusCode::INTERNAL_SERVER_ERROR, + "Unable to determine API host", + ) + } + }; + + let now = Utc::now(); + let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string(); + let date_stamp = now.format("%Y%m%d").to_string(); + let region = state.config.region.as_str(); + let credential = format!("{}/{}/{}/s3/aws4_request", access_key, date_stamp, region); + + let canonical_uri = format!("/{}/{}", bucket, encode_object_key(key)); + let mut query_params = vec![ + ( + "X-Amz-Algorithm".to_string(), + "AWS4-HMAC-SHA256".to_string(), + ), + ("X-Amz-Credential".to_string(), credential.clone()), + ("X-Amz-Date".to_string(), amz_date.clone()), + ("X-Amz-Expires".to_string(), expires.to_string()), + ("X-Amz-SignedHeaders".to_string(), "host".to_string()), + ]; + query_params.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); + + let canonical_query = query_params + .iter() + .map(|(name, value)| format!("{}={}", aws_query_encode(name), aws_query_encode(value))) + .collect::>() + .join("&"); + let canonical_request = format!( + "{}\n{}\n{}\nhost:{}\n\nhost\nUNSIGNED-PAYLOAD", + method, canonical_uri, canonical_query, host + ); + let scope = format!("{}/{}/s3/aws4_request", date_stamp, region); + let string_to_sign = sigv4::build_string_to_sign(&amz_date, &scope, &canonical_request); + let signing_key = sigv4::derive_signing_key(&secret_key, &date_stamp, region, "s3"); + let signature = sigv4::compute_signature(&signing_key, &string_to_sign); + + let final_query = format!("{}&X-Amz-Signature={}", canonical_query, signature); + let final_url = format!("{}{}?{}", api_base, canonical_uri, final_query); + + Json(json!({ + "url": final_url, + "method": method, + "expires_in": expires, + })) + .into_response() +} + +#[derive(Deserialize, Default)] +struct ObjectTagsPayload { + #[serde(default)] + tags: Vec, +} + +#[derive(Deserialize, Default)] +struct ObjectTagPayload { + #[serde(default, alias = "Key", alias = "key")] + key: String, + #[serde(default, alias = "Value", alias = "value")] + value: String, +} + +async fn update_object_tags(state: &AppState, bucket: &str, key: &str, body: Body) -> Response { + let payload: ObjectTagsPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + if payload.tags.len() > 50 { + return json_error(StatusCode::BAD_REQUEST, "Maximum 50 tags allowed"); + } + + let tags = payload + .tags + .iter() + .filter(|tag| !tag.key.trim().is_empty()) + .map(|tag| Tag { + key: tag.key.trim().to_string(), + value: tag.value.to_string(), + }) + .collect::>(); + + let result = if tags.is_empty() { + state.storage.delete_object_tags(bucket, key).await + } else { + state.storage.set_object_tags(bucket, key, &tags).await + }; + + match result { + Ok(()) => Json(json!({ + "status": "ok", + "message": "Tags saved", + "tags": tags.into_iter().map(|tag| json!({ "Key": tag.key, "Value": tag.value })).collect::>(), + })) + .into_response(), + Err(err) => storage_json_error(err), + } +} + +#[derive(Deserialize, Default)] +struct CopyMovePayload { + #[serde(default)] + dest_bucket: String, + #[serde(default)] + dest_key: String, +} + +async fn copy_object_json(state: &AppState, bucket: &str, key: &str, body: Body) -> Response { + let payload: CopyMovePayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + let dest_bucket = payload.dest_bucket.trim(); + let dest_key = payload.dest_key.trim(); + if dest_bucket.is_empty() || dest_key.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "dest_bucket and dest_key are required", + ); + } + + match state + .storage + .copy_object(bucket, key, dest_bucket, dest_key) + .await + { + Ok(_) => Json(json!({ + "status": "ok", + "message": format!("Copied to {}/{}", dest_bucket, dest_key), + "dest_bucket": dest_bucket, + "dest_key": dest_key, + })) + .into_response(), + Err(err) => storage_json_error(err), + } +} + +async fn move_object_json(state: &AppState, bucket: &str, key: &str, body: Body) -> Response { + let payload: CopyMovePayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + let dest_bucket = payload.dest_bucket.trim(); + let dest_key = payload.dest_key.trim(); + if dest_bucket.is_empty() || dest_key.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "dest_bucket and dest_key are required", + ); + } + if dest_bucket == bucket && dest_key == key { + return json_error( + StatusCode::BAD_REQUEST, + "Cannot move object to the same location", + ); + } + + match state.storage.copy_object(bucket, key, dest_bucket, dest_key).await { + Ok(_) => match state.storage.delete_object(bucket, key).await { + Ok(()) => Json(json!({ + "status": "ok", + "message": format!("Moved to {}/{}", dest_bucket, dest_key), + "dest_bucket": dest_bucket, + "dest_key": dest_key, + })) + .into_response(), + Err(_) => Json(json!({ + "status": "partial", + "message": format!("Copied to {}/{} but failed to delete source", dest_bucket, dest_key), + "dest_bucket": dest_bucket, + "dest_key": dest_key, + })) + .into_response(), + }, + Err(err) => storage_json_error(err), + } +} + +async fn purge_object_versions_for_key( + state: &AppState, + bucket: &str, + key: &str, +) -> Result<(), String> { + if let Ok(version_dir) = version_dir_for_object(state, bucket, key) { + if version_dir.exists() { + std::fs::remove_dir_all(&version_dir).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +async fn delete_object_json( + state: &AppState, + bucket: &str, + key: &str, + headers: &HeaderMap, + body: Body, +) -> Response { + let body_bytes = match to_bytes(body, usize::MAX).await { + Ok(bytes) => bytes, + Err(_) => return json_error(StatusCode::BAD_REQUEST, "Failed to read request body"), + }; + + let content_type = headers + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or_default(); + let form = if content_type.starts_with("application/x-www-form-urlencoded") { + parse_form_body(&body_bytes) + } else { + HashMap::new() + }; + let purge_versions = parse_bool_flag(form.get("purge_versions").map(|s| s.as_str())); + + if purge_versions { + if let Err(err) = state.storage.delete_object(bucket, key).await { + return storage_json_error(err); + } + if let Err(err) = purge_object_versions_for_key(state, bucket, key).await { + return json_error(StatusCode::BAD_REQUEST, err); + } + return Json(json!({ + "status": "ok", + "message": format!("Permanently deleted '{}' and all versions", key), + })) + .into_response(); + } + + match state.storage.delete_object(bucket, key).await { + Ok(()) => Json(json!({ + "status": "ok", + "message": format!("Deleted '{}'", key), + })) + .into_response(), + Err(err) => storage_json_error(err), + } +} + +async fn restore_object_version_json( + state: &AppState, + bucket: &str, + key: &str, + version_id: &str, +) -> Response { + let version_dir = match version_dir_for_object(state, bucket, key) { + Ok(path) => path, + Err(err) => return json_error(StatusCode::BAD_REQUEST, err), + }; + let data_path = version_dir.join(format!("{}.bin", version_id)); + let meta_path = version_dir.join(format!("{}.json", version_id)); + if !data_path.exists() || !meta_path.exists() { + return json_error(StatusCode::NOT_FOUND, "Version not found"); + } + + let manifest_text = match std::fs::read_to_string(&meta_path) { + Ok(text) => text, + Err(err) => return json_error(StatusCode::BAD_REQUEST, err.to_string()), + }; + let manifest: VersionManifest = match serde_json::from_str(&manifest_text) { + Ok(manifest) => manifest, + Err(err) => return json_error(StatusCode::BAD_REQUEST, err.to_string()), + }; + + let live_exists = state.storage.head_object(bucket, key).await.is_ok(); + let versioning_enabled = state + .storage + .is_versioning_enabled(bucket) + .await + .unwrap_or(false); + if live_exists { + if let Err(err) = state.storage.delete_object(bucket, key).await { + return storage_json_error(err); + } + } + + let destination = match object_live_path(state, bucket, key) { + Ok(path) => path, + Err(err) => return json_error(StatusCode::BAD_REQUEST, err), + }; + if let Some(parent) = destination.parent() { + if let Err(err) = tokio::fs::create_dir_all(parent).await { + return json_error(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()); + } + } + if let Err(err) = tokio::fs::copy(&data_path, &destination).await { + return json_error(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()); + } + if let Err(err) = state + .storage + .put_object_metadata(bucket, key, &manifest.metadata) + .await + { + return storage_json_error(err); + } + + let mut message = format!("Restored '{}'", key); + if live_exists && versioning_enabled { + message.push_str(" (previous current version was archived)"); + } + Json(json!({ "status": "ok", "message": message })).into_response() +} + +#[derive(Debug, Clone, Copy)] +enum ObjectGetAction { + Download, + Preview, + Metadata, + Versions, + Tags, +} + +#[derive(Debug, Clone)] +enum ObjectPostAction { + Delete, + Presign, + Tags, + Copy, + Move, + Restore(String), +} + +fn parse_object_get_action(rest: &str) -> Option<(String, ObjectGetAction)> { + for (suffix, action) in [ + ("/download", ObjectGetAction::Download), + ("/preview", ObjectGetAction::Preview), + ("/metadata", ObjectGetAction::Metadata), + ("/versions", ObjectGetAction::Versions), + ("/tags", ObjectGetAction::Tags), + ] { + if let Some(key) = rest.strip_suffix(suffix) { + return Some((key.to_string(), action)); + } + } + None +} + +fn parse_object_post_action(rest: &str) -> Option<(String, ObjectPostAction)> { + if let Some((key, version_id)) = rest.rsplit_once("/restore/") { + return Some(( + key.to_string(), + ObjectPostAction::Restore(version_id.to_string()), + )); + } + for (suffix, action) in [ + ("/delete", ObjectPostAction::Delete), + ("/presign", ObjectPostAction::Presign), + ("/tags", ObjectPostAction::Tags), + ("/copy", ObjectPostAction::Copy), + ("/move", ObjectPostAction::Move), + ] { + if let Some(key) = rest.strip_suffix(suffix) { + return Some((key.to_string(), action)); + } + } + None +} + +pub async fn object_get_dispatch( + State(state): State, + Extension(session): Extension, + Path((bucket_name, rest)): Path<(String, String)>, + headers: HeaderMap, +) -> Response { + let Some((key, action)) = parse_object_get_action(&rest) else { + return json_error(StatusCode::NOT_FOUND, "Unknown object action"); + }; + + match action { + ObjectGetAction::Download => { + serve_object_download_or_preview(state, bucket_name, key, headers, true).await + } + ObjectGetAction::Preview => { + serve_object_download_or_preview(state, bucket_name, key, headers, false).await + } + ObjectGetAction::Metadata => object_metadata_json(&state, &bucket_name, &key).await, + ObjectGetAction::Versions => object_versions_json(&state, &bucket_name, &key).await, + ObjectGetAction::Tags => { + let _ = session; + object_tags_json(&state, &bucket_name, &key).await + } + } +} + +pub async fn object_post_dispatch( + State(state): State, + Extension(session): Extension, + Path((bucket_name, rest)): Path<(String, String)>, + headers: HeaderMap, + body: Body, +) -> Response { + let Some((key, action)) = parse_object_post_action(&rest) else { + return json_error(StatusCode::NOT_FOUND, "Unknown object action"); + }; + + match action { + ObjectPostAction::Delete => { + delete_object_json(&state, &bucket_name, &key, &headers, body).await + } + ObjectPostAction::Presign => { + object_presign_json(&state, &session, &bucket_name, &key, body).await + } + ObjectPostAction::Tags => update_object_tags(&state, &bucket_name, &key, body).await, + ObjectPostAction::Copy => copy_object_json(&state, &bucket_name, &key, body).await, + ObjectPostAction::Move => move_object_json(&state, &bucket_name, &key, body).await, + ObjectPostAction::Restore(version_id) => { + restore_object_version_json(&state, &bucket_name, &key, &version_id).await + } + } +} + +#[derive(Deserialize, Default)] +struct BulkKeysPayload { + #[serde(default)] + keys: Vec, + #[serde(default)] + purge_versions: bool, +} + +async fn expand_bulk_keys( + state: &AppState, + bucket: &str, + keys: &[String], +) -> Result, StorageError> { + let mut expanded = Vec::new(); + for key in keys { + if key.ends_with('/') { + let params = ListParams { + max_keys: 5000, + continuation_token: None, + prefix: Some(key.clone()), + start_after: None, + }; + let objects = state.storage.list_objects(bucket, ¶ms).await?; + for object in objects.objects { + expanded.push(object.key); + } + } else { + expanded.push(key.clone()); + } + } + let mut unique = BTreeMap::new(); + for key in expanded { + unique.entry(key.clone()).or_insert(key); + } + Ok(unique.into_values().collect()) +} + +pub async fn bulk_delete_objects( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: BulkKeysPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + let cleaned = payload + .keys + .into_iter() + .map(|key| key.trim().to_string()) + .filter(|key| !key.is_empty()) + .collect::>(); + if cleaned.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "Select at least one object to delete", + ); + } + + let keys = match expand_bulk_keys(&state, &bucket_name, &cleaned).await { + Ok(keys) => keys, + Err(err) => return storage_json_error(err), + }; + if keys.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "No objects found under the selected folders", + ); + } + + let mut deleted = Vec::new(); + let mut errors = Vec::new(); + + for key in keys { + match state.storage.delete_object(&bucket_name, &key).await { + Ok(()) => { + if payload.purge_versions { + if let Err(err) = + purge_object_versions_for_key(&state, &bucket_name, &key).await + { + errors.push(json!({ "key": key, "error": err })); + continue; + } + } + deleted.push(key); + } + Err(err) => errors.push(json!({ "key": key, "error": err.to_string() })), + } + } + + if deleted.is_empty() && !errors.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "status": "error", + "message": "Unable to delete the selected objects", + "deleted": deleted, + "errors": errors, + })), + ) + .into_response(); + } + + let mut message = format!( + "Deleted {} object{}", + deleted.len(), + if deleted.len() == 1 { "" } else { "s" } + ); + if payload.purge_versions && !deleted.is_empty() { + message.push_str(" (including archived versions)"); + } + if !errors.is_empty() { + message.push_str(&format!("; {} failed", errors.len())); + } + + Json(json!({ + "status": if errors.is_empty() { "ok" } else { "partial" }, + "message": message, + "deleted": deleted, + "errors": errors, + })) + .into_response() +} + +pub async fn bulk_download_objects( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + body: Body, +) -> Response { + let payload: BulkKeysPayload = match parse_json_body(body).await { + Ok(payload) => payload, + Err(response) => return response, + }; + + let cleaned = payload + .keys + .into_iter() + .map(|key| key.trim().to_string()) + .filter(|key| !key.is_empty()) + .collect::>(); + if cleaned.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "Select at least one object to download", + ); + } + + let keys = match expand_bulk_keys(&state, &bucket_name, &cleaned).await { + Ok(keys) => keys, + Err(err) => return storage_json_error(err), + }; + if keys.is_empty() { + return json_error( + StatusCode::BAD_REQUEST, + "No objects found under the selected folders", + ); + } + + let mut total_bytes = 0u64; + let mut archive_entries = Vec::new(); + for key in keys { + match state.storage.head_object(&bucket_name, &key).await { + Ok(meta) => { + total_bytes = total_bytes.saturating_add(meta.size); + match read_object_bytes_for_zip(&state, &bucket_name, &key).await { + Ok(bytes) => archive_entries.push((key, bytes, meta.last_modified)), + Err(err) => return json_error(StatusCode::BAD_REQUEST, err), + } + } + Err(err) => return storage_json_error(err), + } + } + + let max_total_bytes = 256 * 1024 * 1024u64; + if total_bytes > max_total_bytes { + return json_error( + StatusCode::BAD_REQUEST, + "Total download size exceeds 256 MB limit. Select fewer objects.", + ); + } + + let zip_bytes = match build_zip_archive(archive_entries) { + Ok(bytes) => bytes, + Err(err) => return json_error(StatusCode::BAD_REQUEST, err), + }; + + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, "application/zip".parse().unwrap()); + headers.insert( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{}-download.zip\"", bucket_name) + .parse() + .unwrap(), + ); + (StatusCode::OK, headers, zip_bytes).into_response() +} + +pub async fn archived_objects( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + let versions_root = version_root_for_bucket(&state, &bucket_name); + if !versions_root.exists() { + return Json(json!({ "objects": [] })).into_response(); + } + + let mut grouped: BTreeMap> = BTreeMap::new(); + let mut stack = vec![versions_root]; + + while let Some(current) = stack.pop() { + let read_dir = match std::fs::read_dir(¤t) { + Ok(entries) => entries, + Err(_) => continue, + }; + for entry in read_dir.flatten() { + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(_) => continue, + }; + if file_type.is_dir() { + stack.push(entry.path()); + continue; + } + if entry.path().extension().and_then(|ext| ext.to_str()) != Some("json") { + continue; + } + let text = match std::fs::read_to_string(entry.path()) { + Ok(text) => text, + Err(_) => continue, + }; + let manifest = match serde_json::from_str::(&text) { + Ok(manifest) => manifest, + Err(_) => continue, + }; + if manifest.key.is_empty() { + continue; + } + grouped + .entry(manifest.key.clone()) + .or_default() + .push(manifest); + } + } + + let mut objects = Vec::new(); + for (key, mut versions) in grouped { + let live_exists = object_live_path(&state, &bucket_name, &key) + .map(|path| path.exists()) + .unwrap_or(false); + if live_exists { + continue; + } + versions.sort_by(|a, b| manifest_timestamp(b).cmp(&manifest_timestamp(a))); + let latest = versions.first().map(|record| manifest_to_json(record)); + objects.push(json!({ + "key": key, + "versions": versions.len(), + "total_size": versions.iter().map(|entry| entry.size).sum::(), + "latest": latest, + "restore_url": versions.first().map(|record| format!( + "/ui/buckets/{}/archived/{}/restore/{}", + bucket_name, + encode_object_key(&record.key), + encode_path_segment(&record.version_id) + )), + "purge_url": format!( + "/ui/buckets/{}/archived/{}/purge", + bucket_name, + encode_object_key(&key) + ), + })); + } + + Json(json!({ "objects": objects })).into_response() +} + +pub async fn archived_post_dispatch( + State(state): State, + Extension(_session): Extension, + Path((bucket_name, rest)): Path<(String, String)>, +) -> Response { + if let Some((key, version_id)) = rest.rsplit_once("/restore/") { + return restore_object_version_json(&state, &bucket_name, key, version_id).await; + } + if let Some(key) = rest.strip_suffix("/purge") { + match purge_object_versions_for_key(&state, &bucket_name, key).await { + Ok(()) => { + let _ = state.storage.delete_object(&bucket_name, key).await; + Json(json!({ + "status": "ok", + "message": format!("Removed archived versions for '{}'", key), + })) + .into_response() + } + Err(err) => json_error(StatusCode::BAD_REQUEST, err), + } + } else { + json_error(StatusCode::NOT_FOUND, "Unknown archived object action") + } +} + +pub async fn gc_status_ui( + State(state): State, + Extension(_session): Extension, +) -> Response { + match &state.gc { + Some(gc) => Json(gc.status().await).into_response(), + None => Json(json!({ + "enabled": false, + "message": "GC is not enabled. Set GC_ENABLED=true to enable." + })) + .into_response(), + } +} + +pub async fn gc_run_ui( + State(state): State, + Extension(_session): Extension, + body: Body, +) -> Response { + let Some(gc) = &state.gc else { + return json_error(StatusCode::BAD_REQUEST, "GC is not enabled"); + }; + let payload: Value = parse_json_body(body).await.unwrap_or_else(|_| json!({})); + let dry_run = payload + .get("dry_run") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + match gc.run_now(dry_run).await { + Ok(result) => Json(result).into_response(), + Err(err) => json_error(StatusCode::CONFLICT, err), + } +} + +pub async fn gc_history_ui( + State(state): State, + Extension(_session): Extension, + Query(params): Query>, +) -> Response { + let limit = params.get("limit").and_then(|v| v.parse::().ok()); + match &state.gc { + Some(gc) => Json(apply_history_limit(gc.history().await, limit)).into_response(), + None => Json(json!({ "executions": [] })).into_response(), + } +} + +pub async fn integrity_status_ui( + State(state): State, + Extension(_session): Extension, +) -> Response { + match &state.integrity { + Some(checker) => Json(checker.status().await).into_response(), + None => Json(json!({ + "enabled": false, + "message": "Integrity checker is not enabled. Set INTEGRITY_ENABLED=true to enable." + })) + .into_response(), + } +} + +pub async fn integrity_run_ui( + State(state): State, + Extension(_session): Extension, + body: Body, +) -> Response { + let Some(checker) = &state.integrity else { + return json_error(StatusCode::BAD_REQUEST, "Integrity checker is not enabled"); + }; + let payload: Value = parse_json_body(body).await.unwrap_or_else(|_| json!({})); + let dry_run = payload + .get("dry_run") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + let auto_heal = payload + .get("auto_heal") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + match checker.run_now(dry_run, auto_heal).await { + Ok(result) => Json(result).into_response(), + Err(err) => json_error(StatusCode::CONFLICT, err), + } +} + +pub async fn integrity_history_ui( + State(state): State, + Extension(_session): Extension, + Query(params): Query>, +) -> Response { + let limit = params.get("limit").and_then(|v| v.parse::().ok()); + match &state.integrity { + Some(checker) => Json(apply_history_limit(checker.history().await, limit)).into_response(), + None => Json(json!({ "executions": [] })).into_response(), + } +} + +fn apply_history_limit(mut value: Value, limit: Option) -> Value { + if let Some(limit) = limit { + if let Some(arr) = value.get_mut("executions").and_then(|v| v.as_array_mut()) { + if arr.len() > limit { + arr.truncate(limit); + } + } + } + value +} + +pub async fn bucket_stub_json(Extension(_session): Extension) -> Response { + Json(json!({"status": "not_implemented", "items": []})).into_response() +} + +pub async fn lifecycle_history_stub( + State(state): State, + Extension(_session): Extension, + Path(_bucket_name): Path, +) -> Response { + Json(json!({ + "enabled": state.config.lifecycle_enabled, + "executions": [], + "total": 0, + })) + .into_response() +} + +#[derive(Deserialize, Default)] +pub struct ReplicationFailuresQuery { + #[serde(default)] + pub limit: Option, + #[serde(default)] + pub offset: Option, +} + +#[derive(Deserialize)] +pub struct ReplicationObjectKeyQuery { + pub object_key: String, +} + +pub async fn replication_status( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + let Some(rule) = state.replication.get_rule(&bucket_name) else { + return json_error(StatusCode::NOT_FOUND, "No replication rule"); + }; + + let (endpoint_healthy, endpoint_error) = match state.connections.get(&rule.target_connection_id) + { + Some(conn) => { + let healthy = state.replication.check_endpoint(&conn).await; + let error = if healthy { + None + } else { + Some(format!("Cannot reach endpoint: {}", conn.endpoint_url)) + }; + (healthy, error) + } + None => (false, Some("Target connection not found".to_string())), + }; + + json_ok(json!({ + "enabled": rule.enabled, + "target_bucket": rule.target_bucket, + "target_connection_id": rule.target_connection_id, + "mode": rule.mode, + "objects_synced": rule.stats.objects_synced, + "objects_pending": rule.stats.objects_pending, + "objects_orphaned": rule.stats.objects_orphaned, + "bytes_synced": rule.stats.bytes_synced, + "last_sync_at": rule.stats.last_sync_at, + "last_sync_key": rule.stats.last_sync_key, + "endpoint_healthy": endpoint_healthy, + "endpoint_error": endpoint_error, + })) +} + +pub async fn replication_failures( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + let limit = q.limit.unwrap_or(50).clamp(1, 500); + let offset = q.offset.unwrap_or(0); + let failures = state + .replication + .get_failed_items(&bucket_name, limit, offset); + let total = state.replication.get_failure_count(&bucket_name); + json_ok(json!({ + "failures": failures, + "total": total, + "limit": limit, + "offset": offset, + })) +} + +pub async fn retry_replication_failure( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + let object_key = q.object_key.trim(); + if object_key.is_empty() { + return json_error(StatusCode::BAD_REQUEST, "object_key is required"); + } + + if state + .replication + .retry_failed(&bucket_name, object_key) + .await + { + json_ok(json!({ + "status": "submitted", + "object_key": object_key, + })) + } else { + json_error(StatusCode::BAD_REQUEST, "Failed to submit retry") + } +} + +pub async fn retry_all_replication_failures( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + let (submitted, skipped) = state.replication.retry_all(&bucket_name).await; + json_ok(json!({ + "status": "submitted", + "submitted": submitted, + "skipped": skipped, + })) +} + +pub async fn dismiss_replication_failure( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, + Query(q): Query, +) -> Response { + let object_key = q.object_key.trim(); + if object_key.is_empty() { + return json_error(StatusCode::BAD_REQUEST, "object_key is required"); + } + + if state.replication.dismiss_failure(&bucket_name, object_key) { + json_ok(json!({ + "status": "dismissed", + "object_key": object_key, + })) + } else { + json_error(StatusCode::NOT_FOUND, "Failure not found") + } +} + +pub async fn clear_replication_failures( + State(state): State, + Extension(_session): Extension, + Path(bucket_name): Path, +) -> Response { + state.replication.clear_failures(&bucket_name); + json_ok(json!({ "status": "cleared" })) +} + +static SERVER_START_TIME: std::sync::OnceLock = std::sync::OnceLock::new(); +static SYSINFO: std::sync::OnceLock> = std::sync::OnceLock::new(); + +async fn sample_system() -> (f64, u64, u64) { + let lock = SYSINFO.get_or_init(|| { + let mut system = System::new(); + system.refresh_cpu_usage(); + system.refresh_memory(); + Mutex::new(system) + }); + { + let mut system = lock.lock().unwrap(); + system.refresh_cpu_usage(); + } + tokio::time::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + let mut system = lock.lock().unwrap(); + system.refresh_cpu_usage(); + system.refresh_memory(); + let cpu_percent = system.global_cpu_usage() as f64; + let mem_total = system.total_memory(); + let mem_used = system.used_memory(); + (cpu_percent, mem_used, mem_total) +} + +fn normalize_path_for_mount(path: &FsPath) -> String { + let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + let raw = canonical.to_string_lossy().to_string(); + let stripped = raw.strip_prefix(r"\\?\").unwrap_or(&raw); + stripped.to_lowercase() +} + +fn sample_disk(path: &FsPath) -> (u64, u64) { + let disks = Disks::new_with_refreshed_list(); + let path_str = normalize_path_for_mount(path); + let mut best: Option<(usize, u64, u64)> = None; + for disk in disks.list() { + let mount_raw = disk.mount_point().to_string_lossy().to_string(); + let mount = mount_raw + .strip_prefix(r"\\?\") + .unwrap_or(&mount_raw) + .to_lowercase(); + let total = disk.total_space(); + let free = disk.available_space(); + if path_str.starts_with(&mount) { + let len = mount.len(); + match best { + Some((best_len, _, _)) if len <= best_len => {} + _ => best = Some((len, total, free)), + } + } + } + best.map(|(_, total, free)| (total, free)).unwrap_or((0, 0)) +} + +pub async fn collect_metrics(state: &AppState) -> Value { + let start_time = *SERVER_START_TIME.get_or_init(std::time::Instant::now); + let uptime_days = start_time.elapsed().as_secs_f64() / 86400.0; + + let buckets_list = state.storage.list_buckets().await.unwrap_or_default(); + let bucket_count = buckets_list.len() as u64; + + let mut total_objects: u64 = 0; + let mut total_bytes: u64 = 0; + let mut total_versions: u64 = 0; + for bucket in &buckets_list { + if let Ok(stats) = state.storage.bucket_stats(&bucket.name).await { + total_objects += stats.objects; + total_bytes += stats.bytes; + total_versions += stats.version_count; + } + } + + let (cpu_percent, mem_used, mem_total) = sample_system().await; + let mem_pct = if mem_total > 0 { + (mem_used as f64 / mem_total as f64) * 100.0 + } else { + 0.0 + }; + + let (disk_total, disk_free) = sample_disk(&state.config.storage_root); + let disk_used = disk_total.saturating_sub(disk_free); + let disk_pct = if disk_total > 0 { + (disk_used as f64 / disk_total as f64) * 100.0 + } else { + 0.0 + }; + + json!({ + "cpu_percent": cpu_percent, + "memory": { + "percent": mem_pct, + "used": human_size(mem_used), + "total": human_size(mem_total), + }, + "disk": { + "percent": disk_pct, + "free": human_size(disk_free), + "total": human_size(disk_total), + }, + "app": { + "storage_used": human_size(total_bytes), + "buckets": bucket_count, + "objects": total_objects, + "versions": total_versions, + "uptime_days": uptime_days.floor() as u64, + }, + }) +} + +pub async fn metrics_api(State(state): State) -> Response { + Json(collect_metrics(&state).await).into_response() +} + +#[derive(Deserialize, Default)] +pub struct HoursQuery { + #[serde(default)] + pub hours: Option, +} + +pub async fn metrics_history( + State(state): State, + Query(q): Query, +) -> Response { + let settings = metrics_settings_snapshot(&state); + match &state.system_metrics { + Some(metrics) => Json(json!({ + "enabled": settings.enabled, + "history": metrics.get_history(q.hours).await, + "interval_minutes": settings.interval_minutes, + "retention_hours": settings.retention_hours, + "hours_requested": q.hours.unwrap_or(settings.retention_hours), + })) + .into_response(), + None => Json(json!({ + "enabled": settings.enabled, + "history": [], + "interval_minutes": settings.interval_minutes, + "retention_hours": settings.retention_hours, + "hours_requested": q.hours.unwrap_or(settings.retention_hours), + })) + .into_response(), + } +} + +pub async fn metrics_operations(State(state): State) -> Response { + match &state.metrics { + Some(metrics) => { + let stats = metrics.get_current_stats(); + Json(json!({ + "enabled": true, + "stats": stats, + })) + .into_response() + } + None => Json(json!({ + "enabled": false, + "stats": null, + })) + .into_response(), + } +} + +pub async fn metrics_operations_history( + State(state): State, + Query(q): Query, +) -> Response { + match &state.metrics { + Some(metrics) => { + let history = metrics.get_history(q.hours.or(Some(24))); + Json(json!({ + "enabled": true, + "history": history, + "interval_minutes": 5, + })) + .into_response() + } + None => Json(json!({ + "enabled": false, + "history": [], + "interval_minutes": 5, + })) + .into_response(), + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs index 92012b5..4b300a3 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/handlers/ui_pages.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use axum::extract::{Extension, Path, Query, State}; -use axum::http::StatusCode; +use axum::extract::{Extension, Form, Path, Query, State}; +use axum::http::{header, HeaderMap, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; use serde_json::{json, Value}; use tera::Context; @@ -20,41 +20,104 @@ pub fn register_ui_endpoints(engine: &TemplateEngine) { ("ui.bucket_detail", "/ui/buckets/{bucket_name}"), ("ui.create_bucket", "/ui/buckets/create"), ("ui.delete_bucket", "/ui/buckets/{bucket_name}/delete"), - ("ui.update_bucket_versioning", "/ui/buckets/{bucket_name}/versioning"), + ( + "ui.update_bucket_versioning", + "/ui/buckets/{bucket_name}/versioning", + ), ("ui.update_bucket_quota", "/ui/buckets/{bucket_name}/quota"), - ("ui.update_bucket_encryption", "/ui/buckets/{bucket_name}/encryption"), - ("ui.update_bucket_policy", "/ui/buckets/{bucket_name}/policy"), - ("ui.update_bucket_replication", "/ui/buckets/{bucket_name}/replication"), - ("ui.update_bucket_website", "/ui/buckets/{bucket_name}/website"), + ( + "ui.update_bucket_encryption", + "/ui/buckets/{bucket_name}/encryption", + ), + ( + "ui.update_bucket_policy", + "/ui/buckets/{bucket_name}/policy", + ), + ( + "ui.update_bucket_replication", + "/ui/buckets/{bucket_name}/replication", + ), + ( + "ui.update_bucket_website", + "/ui/buckets/{bucket_name}/website", + ), ("ui.upload_object", "/ui/buckets/{bucket_name}/upload"), - ("ui.bulk_delete_objects", "/ui/buckets/{bucket_name}/bulk-delete"), - ("ui.bulk_download_objects", "/ui/buckets/{bucket_name}/bulk-download"), + ( + "ui.bulk_delete_objects", + "/ui/buckets/{bucket_name}/bulk-delete", + ), + ( + "ui.bulk_download_objects", + "/ui/buckets/{bucket_name}/bulk-download", + ), ("ui.archived_objects", "/ui/buckets/{bucket_name}/archived"), - ("ui.initiate_multipart_upload", "/ui/buckets/{bucket_name}/multipart/initiate"), - ("ui.upload_multipart_part", "/ui/buckets/{bucket_name}/multipart/{upload_id}/part/{part_number}"), - ("ui.complete_multipart_upload", "/ui/buckets/{bucket_name}/multipart/{upload_id}/complete"), - ("ui.abort_multipart_upload", "/ui/buckets/{bucket_name}/multipart/{upload_id}/abort"), - ("ui.get_lifecycle_history", "/ui/buckets/{bucket_name}/lifecycle/history"), - ("ui.get_replication_status", "/ui/buckets/{bucket_name}/replication/status"), - ("ui.get_replication_failures", "/ui/buckets/{bucket_name}/replication/failures"), - ("ui.clear_replication_failures", "/ui/buckets/{bucket_name}/replication/failures/clear"), - ("ui.retry_all_replication_failures", "/ui/buckets/{bucket_name}/replication/failures/retry-all"), - ("ui.retry_replication_failure", "/ui/buckets/{bucket_name}/replication/failures/retry"), - ("ui.dismiss_replication_failure", "/ui/buckets/{bucket_name}/replication/failures/dismiss"), + ( + "ui.initiate_multipart_upload", + "/ui/buckets/{bucket_name}/multipart/initiate", + ), + ( + "ui.upload_multipart_part", + "/ui/buckets/{bucket_name}/multipart/{upload_id}/part", + ), + ( + "ui.complete_multipart_upload", + "/ui/buckets/{bucket_name}/multipart/{upload_id}/complete", + ), + ( + "ui.abort_multipart_upload", + "/ui/buckets/{bucket_name}/multipart/{upload_id}/abort", + ), + ( + "ui.get_lifecycle_history", + "/ui/buckets/{bucket_name}/lifecycle/history", + ), + ( + "ui.get_replication_status", + "/ui/buckets/{bucket_name}/replication/status", + ), + ( + "ui.get_replication_failures", + "/ui/buckets/{bucket_name}/replication/failures", + ), + ( + "ui.clear_replication_failures", + "/ui/buckets/{bucket_name}/replication/failures/clear", + ), + ( + "ui.retry_all_replication_failures", + "/ui/buckets/{bucket_name}/replication/failures/retry-all", + ), + ( + "ui.retry_replication_failure", + "/ui/buckets/{bucket_name}/replication/failures/retry", + ), + ( + "ui.dismiss_replication_failure", + "/ui/buckets/{bucket_name}/replication/failures/dismiss", + ), ("ui.replication_wizard", "/ui/replication/new"), - ("ui.create_peer_replication_rules", "/ui/replication/create"), + ( + "ui.create_peer_replication_rules", + "/ui/sites/peers/{site_id}/replication-rules", + ), ("ui.iam_dashboard", "/ui/iam"), ("ui.create_iam_user", "/ui/iam/users"), ("ui.update_iam_user", "/ui/iam/users/{user_id}"), ("ui.delete_iam_user", "/ui/iam/users/{user_id}/delete"), ("ui.update_iam_policies", "/ui/iam/users/{user_id}/policies"), ("ui.update_iam_expiry", "/ui/iam/users/{user_id}/expiry"), - ("ui.rotate_iam_secret", "/ui/iam/users/{user_id}/rotate-secret"), + ( + "ui.rotate_iam_secret", + "/ui/iam/users/{user_id}/rotate-secret", + ), ("ui.connections_dashboard", "/ui/connections"), ("ui.create_connection", "/ui/connections/create"), ("ui.update_connection", "/ui/connections/{connection_id}"), - ("ui.delete_connection", "/ui/connections/{connection_id}/delete"), - ("ui.test_connection", "/ui/connections/{connection_id}/test"), + ( + "ui.delete_connection", + "/ui/connections/{connection_id}/delete", + ), + ("ui.test_connection", "/ui/connections/test"), ("ui.sites_dashboard", "/ui/sites"), ("ui.update_local_site", "/ui/sites/local"), ("ui.add_peer_site", "/ui/sites/peers"), @@ -65,22 +128,40 @@ pub fn register_ui_endpoints(engine: &TemplateEngine) { ("ui.system_gc_history", "/ui/system/gc/history"), ("ui.system_integrity_status", "/ui/system/integrity/status"), ("ui.system_integrity_run", "/ui/system/integrity/run"), - ("ui.system_integrity_history", "/ui/system/integrity/history"), + ( + "ui.system_integrity_history", + "/ui/system/integrity/history", + ), ("ui.website_domains_dashboard", "/ui/website-domains"), ("ui.create_website_domain", "/ui/website-domains/create"), ("ui.update_website_domain", "/ui/website-domains/{domain}"), - ("ui.delete_website_domain", "/ui/website-domains/{domain}/delete"), + ( + "ui.delete_website_domain", + "/ui/website-domains/{domain}/delete", + ), ("ui.docs_page", "/ui/docs"), ]); } -fn page_context( - state: &AppState, - session: &SessionHandle, - endpoint: &str, -) -> Context { +fn page_context(state: &AppState, session: &SessionHandle, endpoint: &str) -> Context { let mut ctx = base_context(session, Some(endpoint)); - ctx.insert("principal", &session.read(|s| s.user_id.clone())); + let principal = session.read(|s| { + s.user_id.as_ref().map(|uid| { + json!({ + "access_key": uid, + "user_id": uid, + "display_name": s + .display_name + .clone() + .unwrap_or_else(|| uid.clone()), + "is_admin": true, + }) + }) + }); + match principal { + Some(p) => ctx.insert("principal", &p), + None => ctx.insert("principal", &Value::Null), + } ctx.insert("can_manage_iam", &true); ctx.insert("can_manage_replication", &true); ctx.insert("can_manage_sites", &true); @@ -94,6 +175,112 @@ fn page_context( ctx } +fn human_size(bytes: u64) -> String { + const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = bytes as f64; + let mut idx = 0usize; + while size >= 1024.0 && idx < UNITS.len() - 1 { + size /= 1024.0; + idx += 1; + } + if idx == 0 { + format!("{} {}", bytes, UNITS[idx]) + } else { + format!("{:.1} {}", size, UNITS[idx]) + } +} + +fn wants_json(headers: &HeaderMap) -> bool { + headers + .get("x-requested-with") + .and_then(|value| value.to_str().ok()) + .map(|value| value.eq_ignore_ascii_case("xmlhttprequest")) + .unwrap_or(false) + || headers + .get(header::ACCEPT) + .and_then(|value| value.to_str().ok()) + .map(|value| value.contains("application/json")) + .unwrap_or(false) +} + +fn bucket_tab_redirect(bucket_name: &str, tab: &str) -> Response { + Redirect::to(&format!("/ui/buckets/{}?tab={}", bucket_name, tab)).into_response() +} + +fn default_public_policy(bucket_name: &str) -> String { + serde_json::to_string_pretty(&json!({ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowList", + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:ListBucket"], + "Resource": [format!("arn:aws:s3:::{}", bucket_name)], + }, + { + "Sid": "AllowRead", + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": [format!("arn:aws:s3:::{}/*", bucket_name)], + } + ] + })) + .unwrap_or_else(|_| "{}".to_string()) +} + +fn parse_api_base(state: &AppState) -> (String, String) { + let api_base = std::env::var("API_BASE_URL") + .unwrap_or_else(|_| format!("http://{}", state.config.bind_addr)) + .trim_end_matches('/') + .to_string(); + let api_host = api_base + .split("://") + .nth(1) + .unwrap_or(&api_base) + .split('/') + .next() + .unwrap_or("") + .to_string(); + (api_base, api_host) +} + +fn config_encryption_to_ui(value: Option<&Value>) -> Value { + match value { + Some(Value::Object(map)) => Value::Object(map.clone()), + Some(Value::String(s)) => { + serde_json::from_str(s).unwrap_or_else(|_| json!({ "Rules": [] })) + } + _ => json!({ "Rules": [] }), + } +} + +fn config_website_to_ui(value: Option<&Value>) -> Value { + match value { + Some(Value::Object(map)) => Value::Object(map.clone()), + Some(Value::String(s)) => serde_json::from_str(s).unwrap_or(Value::Null), + _ => Value::Null, + } +} + +fn bucket_access_descriptor( + policy: Option<&Value>, + bucket_name: &str, +) -> (&'static str, &'static str) { + let Some(policy) = policy else { + return ("Private", "bg-secondary-subtle text-secondary-emphasis"); + }; + + let default_policy = default_public_policy(bucket_name); + let default_policy_value: Value = serde_json::from_str(&default_policy).unwrap_or(Value::Null); + if *policy == default_policy_value { + return ("Public Read", "bg-warning-subtle text-warning-emphasis"); + } + + ("Custom policy", "bg-info-subtle text-info-emphasis") +} + pub async fn buckets_overview( State(state): State, Extension(session): Extension, @@ -108,24 +295,33 @@ pub async fn buckets_overview( } }; - let items: Vec = buckets - .iter() - .map(|b| { - json!({ - "meta": { - "name": b.name, - "creation_date": b.creation_date.to_rfc3339(), - }, - "summary": { - "human_size": "0 B", - "objects": 0, - }, - "detail_url": format!("/ui/buckets/{}", b.name), - "access_badge": "bg-secondary bg-opacity-10 text-secondary", - "access_label": "Private", - }) - }) - .collect(); + let mut items: Vec = Vec::with_capacity(buckets.len()); + for b in &buckets { + let stats = state.storage.bucket_stats(&b.name).await.ok(); + let total_bytes = stats.as_ref().map(|s| s.total_bytes()).unwrap_or(0); + let total_objects = stats.as_ref().map(|s| s.total_objects()).unwrap_or(0); + let policy = state + .storage + .get_bucket_config(&b.name) + .await + .ok() + .and_then(|cfg| cfg.policy); + let (access_label, access_badge) = bucket_access_descriptor(policy.as_ref(), &b.name); + + items.push(json!({ + "meta": { + "name": b.name, + "creation_date": b.creation_date.to_rfc3339(), + }, + "summary": { + "human_size": human_size(total_bytes), + "objects": total_objects, + }, + "detail_url": format!("/ui/buckets/{}", b.name), + "access_badge": access_badge, + "access_label": access_label, + })); + } ctx.insert("buckets", &items); render(&state, "buckets.html", &ctx) @@ -135,32 +331,269 @@ pub async fn bucket_detail( State(state): State, Extension(session): Extension, Path(bucket_name): Path, + Query(request_args): Query>, ) -> Response { if !matches!(state.storage.bucket_exists(&bucket_name).await, Ok(true)) { return (StatusCode::NOT_FOUND, "Bucket not found").into_response(); } let mut ctx = page_context(&state, &session, "ui.bucket_detail"); + ctx.insert("request_args", &request_args); + let bucket_meta = state + .storage + .list_buckets() + .await + .ok() + .and_then(|list| list.into_iter().find(|b| b.name == bucket_name)); + let bucket_config = state + .storage + .get_bucket_config(&bucket_name) + .await + .unwrap_or_default(); + let bucket_stats = state + .storage + .bucket_stats(&bucket_name) + .await + .unwrap_or_default(); + let replication_rule = state.replication.get_rule(&bucket_name); + let target_conn = replication_rule + .as_ref() + .and_then(|rule| state.connections.get(&rule.target_connection_id)); + let versioning_enabled = state + .storage + .is_versioning_enabled(&bucket_name) + .await + .unwrap_or(false); + let encryption_config = config_encryption_to_ui(bucket_config.encryption.as_ref()); + let website_config = config_website_to_ui(bucket_config.website.as_ref()); + let quota = bucket_config.quota.clone(); + let max_bytes = quota.as_ref().and_then(|q| q.max_bytes); + let max_objects = quota.as_ref().and_then(|q| q.max_objects); + let bucket_policy = bucket_config.policy.clone().unwrap_or(Value::Null); + let bucket_policy_text = if bucket_policy.is_null() { + String::new() + } else { + serde_json::to_string_pretty(&bucket_policy).unwrap_or_else(|_| bucket_policy.to_string()) + }; + let default_policy = default_public_policy(&bucket_name); + let default_policy_value: Value = serde_json::from_str(&default_policy).unwrap_or(Value::Null); + let preset_choice = if bucket_policy.is_null() { + "private" + } else if bucket_policy == default_policy_value { + "public" + } else { + "custom" + }; ctx.insert("bucket_name", &bucket_name); - ctx.insert("bucket", &json!({ "name": bucket_name })); + ctx.insert( + "bucket", + &json!({ + "name": bucket_name, + "creation_date": bucket_meta + .as_ref() + .map(|b| b.creation_date.to_rfc3339()) + .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()), + }), + ); ctx.insert("objects", &Vec::::new()); ctx.insert("prefixes", &Vec::::new()); - ctx.insert("total_objects", &0); - ctx.insert("total_bytes", &0); - ctx.insert("max_objects", &Value::Null); - ctx.insert("max_bytes", &Value::Null); - ctx.insert("versioning_status", &"Disabled"); - ctx.insert("encryption_config", &json!({ "Rules": [] })); - ctx.insert("replication_rules", &Vec::::new()); - ctx.insert("website_config", &Value::Null); - ctx.insert("bucket_policy", &""); - ctx.insert("connections", &Vec::::new()); + ctx.insert("total_objects", &bucket_stats.total_objects()); + ctx.insert("total_bytes", &bucket_stats.total_bytes()); + ctx.insert("current_objects", &bucket_stats.objects); + ctx.insert("current_bytes", &bucket_stats.bytes); + ctx.insert("version_count", &bucket_stats.version_count); + ctx.insert("version_bytes", &bucket_stats.version_bytes); + ctx.insert("max_objects", &max_objects); + ctx.insert("max_bytes", &max_bytes); + ctx.insert("has_max_objects", &max_objects.is_some()); + ctx.insert("has_max_bytes", &max_bytes.is_some()); + ctx.insert( + "obj_pct", + &max_objects + .map(|m| { + ((bucket_stats.total_objects() as f64 / m.max(1) as f64) * 100.0).round() as u64 + }) + .unwrap_or(0), + ); + ctx.insert( + "bytes_pct", + &max_bytes + .map(|m| ((bucket_stats.total_bytes() as f64 / m.max(1) as f64) * 100.0).round() as u64) + .unwrap_or(0), + ); + ctx.insert("has_quota", "a.is_some()); + ctx.insert("versioning_enabled", &versioning_enabled); + ctx.insert( + "versioning_status", + &(if versioning_enabled { + "Enabled" + } else { + "Disabled" + }), + ); + ctx.insert("encryption_config", &encryption_config); + ctx.insert("enc_rules", &Vec::::new()); + ctx.insert("enc_algorithm", &""); + ctx.insert("enc_kms_key", &""); + let replication_rules = replication_rule + .clone() + .and_then(|rule| serde_json::to_value(rule).ok()) + .map(|rule| vec![rule]) + .unwrap_or_default(); + ctx.insert("replication_rules", &replication_rules); + ctx.insert( + "replication_rule", + &replication_rule + .clone() + .and_then(|rule| serde_json::to_value(rule).ok()) + .unwrap_or(Value::Null), + ); + ctx.insert("website_config", &website_config); + ctx.insert("bucket_policy", &bucket_policy); + ctx.insert("bucket_policy_text", &bucket_policy_text); + ctx.insert("preset_choice", &preset_choice); + let conns: Vec = state + .connections + .list() + .into_iter() + .map(|c| { + json!({ + "id": c.id, + "name": c.name, + "endpoint_url": c.endpoint_url, + "region": c.region, + "access_key": c.access_key, + }) + }) + .collect(); + ctx.insert("connections", &conns); ctx.insert("current_prefix", &""); ctx.insert("parent_prefix", &""); ctx.insert("has_more", &false); ctx.insert("next_token", &""); - ctx.insert("active_tab", &"objects"); - ctx.insert("multipart_uploads", &Vec::::new()); + ctx.insert( + "active_tab", + &request_args + .get("tab") + .cloned() + .unwrap_or_else(|| "objects".to_string()), + ); + let multipart_uploads: Vec = state + .storage + .list_multipart_uploads(&bucket_name) + .await + .unwrap_or_default() + .into_iter() + .map(|u| { + json!({ + "upload_id": u.upload_id, + "key": u.key, + "initiated": u.initiated.to_rfc3339(), + }) + }) + .collect(); + ctx.insert("multipart_uploads", &multipart_uploads); + ctx.insert( + "target_conn", + &target_conn + .as_ref() + .and_then(|conn| serde_json::to_value(conn).ok()) + .unwrap_or(Value::Null), + ); + ctx.insert( + "target_conn_name", + &target_conn + .as_ref() + .map(|conn| conn.name.clone()) + .unwrap_or_default(), + ); + ctx.insert("default_policy", &default_policy); + ctx.insert("can_manage_cors", &true); + ctx.insert("can_manage_lifecycle", &true); + ctx.insert("can_manage_quota", &true); + ctx.insert("can_manage_versioning", &true); + ctx.insert("can_manage_website", &true); + ctx.insert("can_edit_policy", &true); + ctx.insert("is_replication_admin", &true); + ctx.insert("lifecycle_enabled", &state.config.lifecycle_enabled); + ctx.insert("site_sync_enabled", &state.config.site_sync_enabled); + ctx.insert( + "website_hosting_enabled", + &state.config.website_hosting_enabled, + ); + let website_domains: Vec = state + .website_domains + .as_ref() + .map(|store| { + store + .list_all() + .into_iter() + .filter_map(|entry| { + if entry.get("bucket").and_then(|v| v.as_str()) == Some(bucket_name.as_str()) { + entry + .get("domain") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + ctx.insert("website_domains", &website_domains); + let kms_keys: Vec = if let Some(kms) = &state.kms { + kms.list_keys() + .await + .into_iter() + .map(|key| { + json!({ + "key_id": key.key_id, + "description": key.description, + }) + }) + .collect() + } else { + Vec::new() + }; + ctx.insert("kms_keys", &kms_keys); + ctx.insert( + "bucket_stats", + &json!({ + "bytes": bucket_stats.bytes, + "objects": bucket_stats.objects, + "total_bytes": bucket_stats.total_bytes(), + "total_objects": bucket_stats.total_objects(), + "version_bytes": bucket_stats.version_bytes, + "version_count": bucket_stats.version_count + }), + ); + ctx.insert( + "bucket_quota", + &json!({ "max_bytes": max_bytes, "max_objects": max_objects }), + ); + ctx.insert( + "buckets_for_copy_url", + &format!("/ui/buckets/{}/copy-targets", bucket_name), + ); + ctx.insert("acl_url", &format!("/ui/buckets/{}/acl", bucket_name)); + ctx.insert("cors_url", &format!("/ui/buckets/{}/cors", bucket_name)); + ctx.insert( + "folders_url", + &format!("/ui/buckets/{}/folders", bucket_name), + ); + ctx.insert( + "lifecycle_url", + &format!("/ui/buckets/{}/lifecycle", bucket_name), + ); + ctx.insert( + "objects_api_url", + &format!("/ui/buckets/{}/objects", bucket_name), + ); + ctx.insert( + "objects_stream_url", + &format!("/ui/buckets/{}/objects/stream", bucket_name), + ); render(&state, "bucket_detail.html", &ctx) } @@ -169,43 +602,936 @@ pub async fn iam_dashboard( Extension(session): Extension, ) -> Response { let mut ctx = page_context(&state, &session, "ui.iam_dashboard"); - let users: Vec = state - .iam - .list_users() + let now = chrono::Utc::now(); + let soon = now + chrono::Duration::days(7); + let raw_users = state.iam.list_users().await; + let mut users: Vec = Vec::with_capacity(raw_users.len()); + for u in raw_users.iter() { + let user_id = u + .get("user_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let display_name = u + .get("display_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let enabled = u.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true); + let access_key = u + .get("access_keys") + .and_then(|v| v.as_array()) + .and_then(|arr| { + arr.iter().find_map(|k| { + k.get("access_key") + .and_then(|x| x.as_str()) + .map(|s| s.to_string()) + }) + }) + .unwrap_or_default(); + + let detail = state.iam.get_user(&user_id).await; + let policies = detail + .as_ref() + .and_then(|d| d.get("policies").cloned()) + .unwrap_or(Value::Array(Vec::new())); + let expires_at = detail + .as_ref() + .and_then(|d| d.get("expires_at").cloned()) + .unwrap_or(Value::Null); + let is_admin = policies + .as_array() + .map(|items| { + items.iter().any(|policy| { + policy + .get("actions") + .and_then(|value| value.as_array()) + .map(|actions| { + actions + .iter() + .any(|action| matches!(action.as_str(), Some("*") | Some("iam:*"))) + }) + .unwrap_or(false) + }) + }) + .unwrap_or(false); + let expires_dt = expires_at.as_str().and_then(|value| { + chrono::DateTime::parse_from_rfc3339(value) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) + }); + let is_expired = expires_dt.map(|dt| dt <= now).unwrap_or(false); + let is_expiring_soon = expires_dt.map(|dt| dt > now && dt <= soon).unwrap_or(false); + let access_keys = u + .get("access_keys") + .cloned() + .unwrap_or(Value::Array(Vec::new())); + + users.push(json!({ + "user_id": user_id, + "access_key": access_key, + "display_name": display_name, + "enabled": enabled, + "is_enabled": enabled, + "expires_at": expires_at, + "is_admin": is_admin, + "is_expired": is_expired, + "is_expiring_soon": is_expiring_soon, + "access_keys": access_keys, + "policies": policies, + "policy_count": u.get("policy_count").cloned().unwrap_or(Value::from(0)), + })); + } + let all_buckets: Vec = state + .storage + .list_buckets() .await - .into_iter() - .map(|u| { - let mut map = u.as_object().cloned().unwrap_or_default(); - map.entry("policies".to_string()).or_insert(Value::Array(Vec::new())); - map.entry("expires_at".to_string()).or_insert(Value::Null); - map.entry("is_enabled".to_string()).or_insert(Value::Bool(true)); - map.entry("display_name".to_string()) - .or_insert_with(|| Value::String(String::new())); - Value::Object(map) - }) - .collect(); + .map(|list| list.into_iter().map(|b| b.name).collect()) + .unwrap_or_default(); ctx.insert("users", &users); ctx.insert("iam_locked", &false); - ctx.insert("now_iso", &chrono::Utc::now().to_rfc3339()); - ctx.insert( - "soon_iso", - &(chrono::Utc::now() + chrono::Duration::days(7)).to_rfc3339(), - ); - ctx.insert("all_buckets", &Vec::::new()); + ctx.insert("locked_reason", &""); + ctx.insert("iam_disabled", &false); + ctx.insert("all_buckets", &all_buckets); + ctx.insert("disclosed_secret", &Value::Null); + let config_doc = + serde_json::to_string_pretty(&state.iam.export_config(true)).unwrap_or_default(); + ctx.insert("config_document", &config_doc); + ctx.insert("config_summary", &json!({ "user_count": users.len() })); render(&state, "iam.html", &ctx) } +#[derive(serde::Deserialize)] +pub struct CreateIamUserForm { + pub display_name: Option, + pub access_key: Option, + pub secret_key: Option, + pub policies: Option, + pub expires_at: Option, + #[serde(default)] + pub csrf_token: String, +} + +fn parse_policies(raw: &str) -> Result, String> { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Ok(vec![]); + } + serde_json::from_str::>(trimmed) + .map_err(|e| format!("Invalid policies JSON: {}", e)) +} + +fn normalize_expires_at(raw: Option) -> Result, String> { + let Some(value) = raw else { + return Ok(None); + }; + let trimmed = value.trim(); + if trimmed.is_empty() { + return Ok(None); + } + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(trimmed) { + return Ok(Some(dt.with_timezone(&chrono::Utc).to_rfc3339())); + } + if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M") { + return Ok(Some(naive.and_utc().to_rfc3339())); + } + if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S") { + return Ok(Some(naive.and_utc().to_rfc3339())); + } + Err("Invalid expiry date format".to_string()) +} + +pub async fn create_iam_user( + State(state): State, + Extension(session): Extension, + headers: HeaderMap, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let wants_json = wants_json(&headers); + let display_name = form + .display_name + .as_deref() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "Unnamed".to_string()); + + if display_name.len() > 64 { + let message = "Display name must be 64 characters or fewer".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/iam").into_response(); + } + + let policies = match form.policies.as_deref().map(parse_policies) { + Some(Ok(p)) if !p.is_empty() => Some(p), + Some(Ok(_)) | None => None, + Some(Err(e)) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + return Redirect::to("/ui/iam").into_response(); + } + }; + + let expires_at = match normalize_expires_at(form.expires_at) { + Ok(v) => v, + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + return Redirect::to("/ui/iam").into_response(); + } + }; + + let custom_access_key = form + .access_key + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + let custom_secret_key = form + .secret_key + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + + match state.iam.create_user( + &display_name, + policies.clone(), + custom_access_key, + custom_secret_key, + expires_at, + ) { + Ok(created) => { + let user_id = created + .get("user_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let access_key = created + .get("access_key") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let secret_key = created + .get("secret_key") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let message = format!("Created user {}", access_key); + if wants_json { + return axum::Json(json!({ + "success": true, + "message": message, + "user_id": user_id, + "access_key": access_key, + "secret_key": secret_key, + "display_name": display_name, + "expires_at": created.get("expires_at").cloned().unwrap_or(Value::Null), + "policies": policies.unwrap_or_default(), + })) + .into_response(); + } + session + .write(|s| s.push_flash("success", format!("{}. Copy the secret now.", message))); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateIamUserForm { + pub display_name: Option, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_iam_user( + State(state): State, + Extension(session): Extension, + Path(user_id): Path, + headers: HeaderMap, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let wants_json = wants_json(&headers); + let display_name = form + .display_name + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + + match state.iam.update_user(&user_id, display_name, None) { + Ok(()) => { + if wants_json { + let display_name = state + .iam + .get_user(&user_id) + .await + .and_then(|user| { + user.get("display_name") + .and_then(|value| value.as_str()) + .map(ToString::to_string) + }) + .unwrap_or_default(); + return axum::Json(json!({ + "success": true, + "user_id": user_id, + "display_name": display_name, + })) + .into_response(); + } + session.write(|s| s.push_flash("success", "User updated.")); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + +pub async fn delete_iam_user( + State(state): State, + Extension(session): Extension, + Path(user_id): Path, + headers: HeaderMap, +) -> Response { + let wants_json = wants_json(&headers); + match state.iam.delete_user(&user_id) { + Ok(()) => { + if wants_json { + return axum::Json(json!({ "success": true })).into_response(); + } + session.write(|s| s.push_flash("success", "User deleted.")); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateIamPoliciesForm { + pub policies: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_iam_policies( + State(state): State, + Extension(session): Extension, + Path(user_id): Path, + headers: HeaderMap, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let wants_json = wants_json(&headers); + let policies = match parse_policies(&form.policies) { + Ok(p) => p, + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + return Redirect::to("/ui/iam").into_response(); + } + }; + + match state.iam.update_user_policies(&user_id, policies) { + Ok(()) => { + if wants_json { + let policies = state + .iam + .get_user(&user_id) + .await + .and_then(|user| user.get("policies").cloned()) + .unwrap_or_else(|| Value::Array(Vec::new())); + return axum::Json(json!({ + "success": true, + "user_id": user_id, + "policies": policies, + })) + .into_response(); + } + session.write(|s| s.push_flash("success", "Policies updated.")); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateIamExpiryForm { + pub expires_at: Option, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_iam_expiry( + State(state): State, + Extension(session): Extension, + Path(user_id): Path, + headers: HeaderMap, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let wants_json = wants_json(&headers); + let expires_at = match normalize_expires_at(form.expires_at) { + Ok(v) => v, + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + return Redirect::to("/ui/iam").into_response(); + } + }; + + match state.iam.update_user(&user_id, None, Some(expires_at)) { + Ok(()) => { + if wants_json { + return axum::Json(json!({ "success": true })).into_response(); + } + session.write(|s| s.push_flash("success", "Expiry updated.")); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + +pub async fn rotate_iam_secret( + State(state): State, + Extension(session): Extension, + Path(user_id): Path, + headers: HeaderMap, +) -> Response { + let wants_json = wants_json(&headers); + match state.iam.rotate_secret(&user_id) { + Ok(result) => { + let access_key = result + .get("access_key") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let secret_key = result + .get("secret_key") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + if wants_json { + return axum::Json(json!({ + "success": true, + "access_key": access_key, + "secret_key": secret_key, + })) + .into_response(); + } + session + .write(|s| s.push_flash("success", format!("Secret rotated for {}.", access_key))); + Redirect::to("/ui/iam").into_response() + } + Err(e) => { + if wants_json { + return (StatusCode::BAD_REQUEST, axum::Json(json!({ "error": e }))) + .into_response(); + } + session.write(|s| s.push_flash("danger", e)); + Redirect::to("/ui/iam").into_response() + } + } +} + pub async fn sites_dashboard( State(state): State, Extension(session): Extension, ) -> Response { let mut ctx = page_context(&state, &session, "ui.sites_dashboard"); - ctx.insert("local_site", &Value::Null); - ctx.insert("peers", &Vec::::new()); + + let local_site = state + .site_registry + .as_ref() + .and_then(|reg| reg.get_local_site()) + .map(|s| { + json!({ + "site_id": s.site_id, + "display_name": s.display_name, + "endpoint": s.endpoint, + "region": s.region, + "priority": s.priority, + }) + }) + .unwrap_or(Value::Null); + + let peers: Vec = state + .site_registry + .as_ref() + .map(|reg| { + reg.list_peers() + .into_iter() + .map(|p| { + json!({ + "site_id": p.site_id, + "display_name": p.display_name, + "endpoint": p.endpoint, + "region": p.region, + "priority": p.priority, + "connection_id": p.connection_id, + "is_healthy": p.is_healthy, + "last_health_check": p.last_health_check, + }) + }) + .collect() + }) + .unwrap_or_default(); + + let peers_with_stats: Vec = peers + .iter() + .cloned() + .map(|peer| { + let has_connection = peer + .get("connection_id") + .and_then(|value| value.as_str()) + .map(|value| !value.is_empty()) + .unwrap_or(false); + json!({ + "peer": peer, + "has_connection": has_connection, + "buckets_syncing": 0, + "has_bidirectional": false, + }) + }) + .collect(); + + let conns: Vec = state + .connections + .list() + .into_iter() + .map(|c| { + json!({ + "id": c.id, + "name": c.name, + "endpoint_url": c.endpoint_url, + "region": c.region, + "access_key": c.access_key, + }) + }) + .collect(); + + ctx.insert("local_site", &local_site); + ctx.insert("peers", &peers); + ctx.insert("peers_with_stats", &peers_with_stats); + ctx.insert("connections", &conns); + ctx.insert( + "config_site_id", + &std::env::var("SITE_ID").unwrap_or_default(), + ); + ctx.insert( + "config_site_endpoint", + &std::env::var("SITE_ENDPOINT").unwrap_or_default(), + ); + ctx.insert( + "config_site_region", + &std::env::var("SITE_REGION").unwrap_or_else(|_| state.config.region.clone()), + ); ctx.insert("topology", &json!({"sites": [], "connections": []})); render(&state, "sites.html", &ctx) } +#[derive(serde::Deserialize)] +pub struct LocalSiteForm { + pub site_id: String, + #[serde(default)] + pub endpoint: String, + #[serde(default = "default_site_region")] + pub region: String, + #[serde(default = "default_site_priority")] + pub priority: i32, + #[serde(default)] + pub display_name: String, + #[serde(default)] + pub csrf_token: String, +} + +#[derive(serde::Deserialize)] +pub struct PeerSiteForm { + pub site_id: String, + pub endpoint: String, + #[serde(default = "default_site_region")] + pub region: String, + #[serde(default = "default_site_priority")] + pub priority: i32, + #[serde(default)] + pub display_name: String, + #[serde(default)] + pub connection_id: String, + #[serde(default)] + pub csrf_token: String, +} + +#[derive(serde::Deserialize, Default)] +pub struct DeletePeerSiteForm { + #[serde(default)] + pub csrf_token: String, +} + +fn default_site_region() -> String { + "us-east-1".to_string() +} + +fn default_site_priority() -> i32 { + 100 +} + +pub async fn update_local_site( + State(state): State, + Extension(session): Extension, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let site_id = form.site_id.trim().to_string(); + if site_id.is_empty() { + let message = "Site ID is required.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + + let Some(registry) = &state.site_registry else { + let message = "Site registry is not available.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + }; + + let existing = registry.get_local_site(); + let site = crate::services::site_registry::SiteInfo { + site_id: site_id.clone(), + endpoint: form.endpoint.trim().to_string(), + region: form.region.trim().to_string(), + priority: form.priority, + display_name: { + let display_name = form.display_name.trim(); + if display_name.is_empty() { + site_id.clone() + } else { + display_name.to_string() + } + }, + created_at: existing.and_then(|site| site.created_at), + }; + registry.set_local_site(site); + + let message = "Local site configuration updated".to_string(); + if wants_json { + return axum::Json(json!({ "ok": true, "message": message })).into_response(); + } + session.write(|s| s.push_flash("success", message)); + Redirect::to("/ui/sites").into_response() +} + +pub async fn add_peer_site( + State(state): State, + Extension(session): Extension, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let site_id = form.site_id.trim().to_string(); + let endpoint = form.endpoint.trim().to_string(); + if site_id.is_empty() { + let message = "Site ID is required.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + if endpoint.is_empty() { + let message = "Endpoint is required.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + + let Some(registry) = &state.site_registry else { + let message = "Site registry is not available.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + }; + + if registry.get_peer(&site_id).is_some() { + let message = format!("Peer site '{}' already exists.", site_id); + if wants_json { + return ( + StatusCode::CONFLICT, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + + let connection_id = { + let value = form.connection_id.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } + }; + if let Some(connection_id) = connection_id.as_deref() { + if state.connections.get(connection_id).is_none() { + let message = format!("Connection '{}' not found.", connection_id); + if wants_json { + return ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + } + + let has_connection = connection_id.is_some(); + let peer = crate::services::site_registry::PeerSite { + site_id: site_id.clone(), + endpoint, + region: form.region.trim().to_string(), + priority: form.priority, + display_name: { + let display_name = form.display_name.trim(); + if display_name.is_empty() { + site_id.clone() + } else { + display_name.to_string() + } + }, + connection_id: connection_id.clone(), + created_at: None, + is_healthy: false, + last_health_check: None, + }; + registry.add_peer(peer); + + let message = format!("Peer site '{}' added.", site_id); + if wants_json { + let redirect = if has_connection { + Some(format!("/ui/replication/new?site_id={}", site_id)) + } else { + None + }; + return axum::Json(json!({ + "ok": true, + "message": message, + "redirect": redirect, + })) + .into_response(); + } + session.write(|s| s.push_flash("success", message)); + if has_connection { + return Redirect::to(&format!("/ui/replication/new?site_id={}", site_id)).into_response(); + } + Redirect::to("/ui/sites").into_response() +} + +pub async fn update_peer_site( + State(state): State, + Extension(session): Extension, + Path(site_id): Path, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let Some(registry) = &state.site_registry else { + let message = "Site registry is not available.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + }; + + let Some(existing) = registry.get_peer(&site_id) else { + let message = format!("Peer site '{}' not found.", site_id); + if wants_json { + return ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + }; + + let connection_id = { + let value = form.connection_id.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } + }; + if let Some(connection_id) = connection_id.as_deref() { + if state.connections.get(connection_id).is_none() { + let message = format!("Connection '{}' not found.", connection_id); + if wants_json { + return ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + } + } + + let peer = crate::services::site_registry::PeerSite { + site_id: site_id.clone(), + endpoint: form.endpoint.trim().to_string(), + region: form.region.trim().to_string(), + priority: form.priority, + display_name: { + let display_name = form.display_name.trim(); + if display_name.is_empty() { + site_id.clone() + } else { + display_name.to_string() + } + }, + connection_id, + created_at: existing.created_at, + is_healthy: existing.is_healthy, + last_health_check: existing.last_health_check, + }; + registry.update_peer(peer); + + let message = format!("Peer site '{}' updated.", site_id); + if wants_json { + return axum::Json(json!({ "ok": true, "message": message })).into_response(); + } + session.write(|s| s.push_flash("success", message)); + Redirect::to("/ui/sites").into_response() +} + +pub async fn delete_peer_site( + State(state): State, + Extension(session): Extension, + Path(site_id): Path, + headers: HeaderMap, + Form(_form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let Some(registry) = &state.site_registry else { + let message = "Site registry is not available.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/sites").into_response(); + }; + + if registry.delete_peer(&site_id) { + let message = format!("Peer site '{}' deleted.", site_id); + if wants_json { + return axum::Json(json!({ "ok": true, "message": message })).into_response(); + } + session.write(|s| s.push_flash("success", message)); + } else { + let message = format!("Peer site '{}' not found.", site_id); + if wants_json { + return ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + } + + Redirect::to("/ui/sites").into_response() +} + pub async fn connections_dashboard( State(state): State, Extension(session): Extension, @@ -233,24 +1559,211 @@ pub async fn metrics_dashboard( Extension(session): Extension, ) -> Response { let mut ctx = page_context(&state, &session, "ui.metrics_dashboard"); - ctx.insert("metrics_enabled", &state.config.metrics_enabled); + ctx.insert( + "metrics_enabled", + &(state.config.metrics_enabled || state.config.metrics_history_enabled), + ); + ctx.insert( + "metrics_history_enabled", + &state.config.metrics_history_enabled, + ); + ctx.insert("operation_metrics_enabled", &state.config.metrics_enabled); ctx.insert("history", &Vec::::new()); ctx.insert("operation_metrics", &Vec::::new()); - ctx.insert("summary", &json!({})); + + let metrics = crate::handlers::ui_api::collect_metrics(&state).await; + let cpu_percent = metrics + .get("cpu_percent") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let memory = metrics + .get("memory") + .cloned() + .unwrap_or_else(|| json!({ "percent": 0, "total": "0 B", "used": "0 B" })); + let disk = metrics + .get("disk") + .cloned() + .unwrap_or_else(|| json!({ "percent": 0, "free": "0 B", "total": "0 B" })); + let app = metrics.get("app").cloned().unwrap_or_else(|| { + json!({ + "buckets": 0, "objects": 0, "storage_used": "0 B", + "uptime_days": 0, "versions": 0, + }) + }); + let mem_pct = memory + .get("percent") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let disk_pct = disk.get("percent").and_then(|v| v.as_f64()).unwrap_or(0.0); + let has_issues = cpu_percent > 80.0 || mem_pct > 85.0 || disk_pct > 90.0; + + ctx.insert("cpu_percent", &cpu_percent); + ctx.insert("memory", &memory); + ctx.insert("disk", &disk); + ctx.insert("app", &app); + ctx.insert("has_issues", &has_issues); + ctx.insert( + "summary", + &json!({ + "app": app, + "cpu_percent": cpu_percent, + "disk": disk, + "memory": memory, + "has_issues": has_issues, + }), + ); render(&state, "metrics.html", &ctx) } +fn format_history_timestamp(timestamp: Option) -> String { + let Some(timestamp) = timestamp else { + return "-".to_string(); + }; + let millis = (timestamp * 1000.0).round() as i64; + chrono::DateTime::::from_timestamp_millis(millis) + .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()) + .unwrap_or_else(|| "-".to_string()) +} + +fn format_byte_count(bytes: u64) -> String { + const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"]; + let mut value = bytes as f64; + let mut unit = 0usize; + while value >= 1024.0 && unit < UNITS.len() - 1 { + value /= 1024.0; + unit += 1; + } + if unit == 0 { + format!("{} {}", bytes, UNITS[unit]) + } else { + format!("{value:.1} {}", UNITS[unit]) + } +} + +fn decorate_gc_history(executions: &[Value]) -> Vec { + executions + .iter() + .cloned() + .map(|mut execution| { + let timestamp = execution.get("timestamp").and_then(|value| value.as_f64()); + let bytes_freed = execution + .get("result") + .and_then(|value| value.get("temp_bytes_freed")) + .and_then(|value| value.as_u64()) + .unwrap_or(0); + if let Some(obj) = execution.as_object_mut() { + obj.insert( + "timestamp_display".to_string(), + Value::String(format_history_timestamp(timestamp)), + ); + obj.insert( + "bytes_freed_display".to_string(), + Value::String(format_byte_count(bytes_freed)), + ); + } + execution + }) + .collect() +} + +fn decorate_integrity_history(executions: &[Value]) -> Vec { + executions + .iter() + .cloned() + .map(|mut execution| { + let timestamp = execution.get("timestamp").and_then(|value| value.as_f64()); + if let Some(obj) = execution.as_object_mut() { + obj.insert( + "timestamp_display".to_string(), + Value::String(format_history_timestamp(timestamp)), + ); + } + execution + }) + .collect() +} + pub async fn system_dashboard( State(state): State, Extension(session): Extension, ) -> Response { let mut ctx = page_context(&state, &session, "ui.system_dashboard"); + + let gc_status = match &state.gc { + Some(gc) => gc.status().await, + None => json!({ + "dry_run": false, + "enabled": false, + "interval_hours": 6, + "lock_file_max_age_hours": 1, + "multipart_max_age_days": 7, + "running": false, + "scanning": false, + "scan_elapsed_seconds": Value::Null, + "temp_file_max_age_hours": 24, + }), + }; + let gc_history = match &state.gc { + Some(gc) => gc + .history() + .await + .get("executions") + .and_then(|value| value.as_array()) + .map(|values| decorate_gc_history(values)) + .unwrap_or_default(), + None => Vec::new(), + }; + + let integrity_status = match &state.integrity { + Some(checker) => checker.status().await, + None => json!({ + "auto_heal": false, + "batch_size": 100, + "dry_run": false, + "enabled": false, + "interval_hours": 24, + "running": false, + "scanning": false, + "scan_elapsed_seconds": Value::Null, + }), + }; + let integrity_history = match &state.integrity { + Some(checker) => checker + .history() + .await + .get("executions") + .and_then(|value| value.as_array()) + .map(|values| decorate_integrity_history(values)) + .unwrap_or_default(), + None => Vec::new(), + }; + ctx.insert("gc_enabled", &state.config.gc_enabled); ctx.insert("integrity_enabled", &state.config.integrity_enabled); - ctx.insert("gc_history", &Vec::::new()); - ctx.insert("integrity_history", &Vec::::new()); - ctx.insert("gc_status", &json!({"running": false})); - ctx.insert("integrity_status", &json!({"running": false})); + ctx.insert("gc_history", &gc_history); + ctx.insert("integrity_history", &integrity_history); + ctx.insert("gc_status", &gc_status); + ctx.insert("integrity_status", &integrity_status); + ctx.insert("app_version", &env!("CARGO_PKG_VERSION")); + ctx.insert("display_timezone", &"UTC"); + ctx.insert("platform", &std::env::consts::OS); + ctx.insert( + "storage_root", + &state.config.storage_root.display().to_string(), + ); + ctx.insert("total_issues", &0); + let features = vec![ + json!({"label": "Encryption (SSE-S3)", "enabled": state.config.encryption_enabled}), + json!({"label": "KMS", "enabled": state.config.kms_enabled}), + json!({"label": "Versioning Lifecycle", "enabled": state.config.lifecycle_enabled}), + json!({"label": "Metrics History", "enabled": state.config.metrics_history_enabled}), + json!({"label": "Operation Metrics", "enabled": state.config.metrics_enabled}), + json!({"label": "Site Sync", "enabled": state.config.site_sync_enabled}), + json!({"label": "Website Hosting", "enabled": state.config.website_hosting_enabled}), + json!({"label": "Garbage Collection", "enabled": state.config.gc_enabled}), + json!({"label": "Integrity Scanner", "enabled": state.config.integrity_enabled}), + ]; + ctx.insert("features", &features); render(&state, "system.html", &ctx) } @@ -259,26 +1772,327 @@ pub async fn website_domains_dashboard( Extension(session): Extension, ) -> Response { let mut ctx = page_context(&state, &session, "ui.website_domains_dashboard"); - ctx.insert("domains", &Vec::::new()); + let buckets: Vec = state + .storage + .list_buckets() + .await + .map(|list| list.into_iter().map(|b| b.name).collect()) + .unwrap_or_default(); + let mappings = state + .website_domains + .as_ref() + .map(|store| { + let mut mappings = store.list_all(); + mappings.sort_by(|a, b| { + let a_domain = a + .get("domain") + .and_then(|value| value.as_str()) + .unwrap_or(""); + let b_domain = b + .get("domain") + .and_then(|value| value.as_str()) + .unwrap_or(""); + a_domain.cmp(b_domain) + }); + mappings + }) + .unwrap_or_default(); + ctx.insert("domains", &mappings); + ctx.insert("mappings", &mappings); + ctx.insert("buckets", &buckets); render(&state, "website_domains.html", &ctx) } pub async fn replication_wizard( State(state): State, Extension(session): Extension, + Query(q): Query>, ) -> Response { let mut ctx = page_context(&state, &session, "ui.replication_wizard"); - ctx.insert("connections", &Vec::::new()); - ctx.insert("local_site", &Value::Null); - ctx.insert("peers", &Vec::::new()); + + let site_id = q.get("site_id").cloned().unwrap_or_default(); + let peer_record = state + .site_registry + .as_ref() + .and_then(|reg| { + if site_id.is_empty() { + reg.list_peers().into_iter().next() + } else { + reg.get_peer(&site_id) + } + }) + .map(|p| { + json!({ + "site_id": p.site_id, + "display_name": p.display_name, + "endpoint": p.endpoint, + "region": p.region, + "connection_id": p.connection_id, + }) + }) + .unwrap_or_else(|| { + json!({ + "site_id": site_id, + "display_name": "", + "endpoint": "", + "region": "us-east-1", + }) + }); + let peer_connection_id = peer_record + .get("connection_id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + + let local_site = state + .site_registry + .as_ref() + .and_then(|reg| reg.get_local_site()) + .map(|s| { + json!({ + "site_id": s.site_id, + "display_name": s.display_name, + "endpoint": s.endpoint, + "region": s.region, + }) + }) + .unwrap_or(Value::Null); + + let peers: Vec = state + .site_registry + .as_ref() + .map(|reg| { + reg.list_peers() + .into_iter() + .map(|p| { + json!({ + "site_id": p.site_id, + "display_name": p.display_name, + "endpoint": p.endpoint, + "region": p.region, + "connection_id": p.connection_id, + }) + }) + .collect() + }) + .unwrap_or_default(); + + let all_rules = state.replication.list_rules(); + let bucket_names: Vec = state + .storage + .list_buckets() + .await + .map(|list| list.into_iter().map(|b| b.name).collect()) + .unwrap_or_default(); + let buckets: Vec = bucket_names + .into_iter() + .map(|bucket_name| { + let existing_rule = all_rules + .iter() + .find(|rule| rule.bucket_name == bucket_name); + let has_rule_for_peer = existing_rule + .map(|rule| rule.target_connection_id == peer_connection_id) + .unwrap_or(false); + json!({ + "name": bucket_name, + "has_rule": has_rule_for_peer, + "existing_mode": if has_rule_for_peer { + existing_rule.map(|rule| rule.mode.clone()) + } else { + None:: + }, + "existing_target": if has_rule_for_peer { + existing_rule.map(|rule| rule.target_bucket.clone()) + } else { + None:: + }, + }) + }) + .collect(); + + let conns: Vec = state + .connections + .list() + .into_iter() + .map(|c| { + json!({ + "id": c.id, + "name": c.name, + "endpoint_url": c.endpoint_url, + "region": c.region, + "access_key": c.access_key, + }) + }) + .collect(); + + let connection = conns + .iter() + .find(|conn| { + conn.get("id") + .and_then(|value| value.as_str()) + .map(|id| id == peer_connection_id) + .unwrap_or(false) + }) + .cloned() + .or_else(|| conns.first().cloned()) + .unwrap_or_else( + || json!({ "id": "", "name": "", "endpoint_url": "", "region": "", "access_key": "" }), + ); + + ctx.insert("peer", &peer_record); + ctx.insert("peers", &peers); + ctx.insert("local_site", &local_site); + ctx.insert("connections", &conns); + ctx.insert("connection", &connection); + ctx.insert("buckets", &buckets); render(&state, "replication_wizard.html", &ctx) } +#[derive(serde::Deserialize)] +pub struct CreatePeerReplicationRulesForm { + #[serde(default)] + pub mode: String, + #[serde(default)] + pub buckets: Vec, + #[serde(default)] + pub csrf_token: String, + #[serde(flatten)] + pub extras: HashMap, +} + +pub async fn create_peer_replication_rules( + State(state): State, + Extension(session): Extension, + Path(site_id): Path, + Form(form): Form, +) -> Response { + create_peer_replication_rules_impl(state, session, site_id, form).await +} + +pub async fn create_peer_replication_rules_from_query( + State(state): State, + Extension(session): Extension, + Query(q): Query>, + Form(form): Form, +) -> Response { + let site_id = q.get("site_id").cloned().unwrap_or_default(); + create_peer_replication_rules_impl(state, session, site_id, form).await +} + +async fn create_peer_replication_rules_impl( + state: AppState, + session: SessionHandle, + site_id: String, + form: CreatePeerReplicationRulesForm, +) -> Response { + let Some(registry) = &state.site_registry else { + session.write(|s| s.push_flash("danger", "Site registry is not available.")); + return Redirect::to("/ui/sites").into_response(); + }; + let Some(peer) = registry.get_peer(&site_id) else { + session.write(|s| s.push_flash("danger", format!("Peer site '{}' not found.", site_id))); + return Redirect::to("/ui/sites").into_response(); + }; + let Some(connection_id) = peer.connection_id.clone() else { + session.write(|s| { + s.push_flash( + "danger", + "This peer has no connection configured. Add a connection first.", + ) + }); + return Redirect::to("/ui/sites").into_response(); + }; + if state.connections.get(&connection_id).is_none() { + session.write(|s| { + s.push_flash( + "danger", + format!("Connection '{}' was not found.", connection_id), + ) + }); + return Redirect::to("/ui/sites").into_response(); + } + + let mode = match form.mode.trim() { + crate::services::replication::MODE_ALL => crate::services::replication::MODE_ALL, + crate::services::replication::MODE_BIDIRECTIONAL => { + crate::services::replication::MODE_BIDIRECTIONAL + } + _ => crate::services::replication::MODE_NEW_ONLY, + } + .to_string(); + + if form.buckets.is_empty() { + session.write(|s| s.push_flash("warning", "No buckets selected.")); + return Redirect::to("/ui/sites").into_response(); + } + + let mut created = 0usize; + let mut created_existing = Vec::new(); + + for bucket_name in form.buckets { + let target_key = format!("target_{}", bucket_name); + let target_bucket = form + .extras + .get(&target_key) + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + .unwrap_or(bucket_name.as_str()) + .to_string(); + + let rule = crate::services::replication::ReplicationRule { + bucket_name: bucket_name.clone(), + target_connection_id: connection_id.clone(), + target_bucket, + enabled: true, + mode: mode.clone(), + created_at: Some(chrono::Utc::now().timestamp_millis() as f64 / 1000.0), + stats: Default::default(), + sync_deletions: true, + last_pull_at: None, + filter_prefix: None, + }; + + state.replication.set_rule(rule); + created += 1; + if mode == crate::services::replication::MODE_ALL { + created_existing.push(bucket_name); + } + } + + for bucket_name in created_existing { + state + .replication + .clone() + .schedule_existing_objects_sync(bucket_name); + } + + if created > 0 { + session.write(|s| { + s.push_flash( + "success", + format!( + "Created {} replication rule(s) for {}.", + created, + if peer.display_name.is_empty() { + peer.site_id.as_str() + } else { + peer.display_name.as_str() + } + ), + ) + }); + } + Redirect::to("/ui/sites").into_response() +} + pub async fn docs_page( State(state): State, Extension(session): Extension, ) -> Response { - let ctx = page_context(&state, &session, "ui.docs_page"); + let mut ctx = page_context(&state, &session, "ui.docs_page"); + let (api_base, api_host) = parse_api_base(&state); + ctx.insert("api_base", &api_base); + ctx.insert("api_host", &api_host); render(&state, "docs.html", &ctx) } @@ -292,22 +2106,905 @@ pub struct CreateBucketForm { pub async fn create_bucket( State(state): State, Extension(session): Extension, + headers: HeaderMap, axum::extract::Form(form): axum::extract::Form, ) -> Response { - match state.storage.create_bucket(form.bucket_name.trim()).await { + let wants_json = wants_json(&headers); + let bucket_name = form.bucket_name.trim().to_string(); + + if bucket_name.is_empty() { + let message = "Bucket name is required".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/buckets").into_response(); + } + + match state.storage.create_bucket(&bucket_name).await { Ok(()) => { - session.write(|s| s.push_flash("success", format!("Bucket '{}' created.", form.bucket_name))); + let message = format!("Bucket '{}' created.", bucket_name); + if wants_json { + return axum::Json(json!({ + "success": true, + "message": message, + "bucket_name": bucket_name, + })) + .into_response(); + } + session.write(|s| s.push_flash("success", message)); } Err(e) => { - session.write(|s| s.push_flash("danger", format!("Failed to create bucket: {}", e))); + let message = format!("Failed to create bucket: {}", e); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); } } Redirect::to("/ui/buckets").into_response() } -pub async fn stub_post( - Extension(session): Extension, +#[derive(serde::Deserialize)] +pub struct UpdateBucketVersioningForm { + pub state: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn delete_bucket( + State(state): State, + Path(bucket_name): Path, ) -> Response { + match state.storage.delete_bucket(&bucket_name).await { + Ok(()) => axum::Json(json!({ + "ok": true, + "message": format!("Bucket '{}' deleted.", bucket_name), + })) + .into_response(), + Err(e) => ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(), + } +} + +pub async fn update_bucket_versioning( + State(state): State, + Path(bucket_name): Path, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let enabled = form.state.eq_ignore_ascii_case("enable"); + match state.storage.set_versioning(&bucket_name, enabled).await { + Ok(()) => axum::Json(json!({ + "ok": true, + "enabled": enabled, + "message": if enabled { "Versioning enabled." } else { "Versioning suspended." }, + })) + .into_response(), + Err(e) => ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(), + } +} + +fn empty_string_as_none<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + use serde::Deserialize; + let opt = Option::::deserialize(deserializer)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => s.parse::().map(Some).map_err(serde::de::Error::custom), + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateBucketQuotaForm { + pub action: String, + #[serde(default, deserialize_with = "empty_string_as_none")] + pub max_mb: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] + pub max_objects: Option, + #[serde(default)] + pub csrf_token: String, +} + +#[derive(serde::Deserialize)] +pub struct UpdateBucketReplicationForm { + pub action: String, + #[serde(default)] + pub target_connection_id: String, + #[serde(default)] + pub target_bucket: String, + #[serde(default)] + pub replication_mode: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_bucket_replication( + State(state): State, + Extension(session): Extension, + Path(bucket_name): Path, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + + let respond = |ok: bool, status: StatusCode, message: String, extra: Value| -> Response { + if wants_json { + let mut payload = json!({ + "ok": ok, + "message": message, + }); + if let Some(obj) = payload.as_object_mut() { + if let Some(extra_obj) = extra.as_object() { + for (key, value) in extra_obj { + obj.insert(key.clone(), value.clone()); + } + } + } + return (status, axum::Json(payload)).into_response(); + } + + session.write(|s| s.push_flash(if ok { "success" } else { "danger" }, message)); + bucket_tab_redirect(&bucket_name, "replication") + }; + + match form.action.as_str() { + "delete" => { + state.replication.delete_rule(&bucket_name); + respond( + true, + StatusCode::OK, + "Replication configuration removed.".to_string(), + json!({ "action": "delete", "enabled": false }), + ) + } + "pause" => { + let Some(mut rule) = state.replication.get_rule(&bucket_name) else { + return respond( + false, + StatusCode::NOT_FOUND, + "No replication configuration to pause.".to_string(), + json!({ "error": "No replication configuration to pause" }), + ); + }; + rule.enabled = false; + state.replication.set_rule(rule); + respond( + true, + StatusCode::OK, + "Replication paused.".to_string(), + json!({ "action": "pause", "enabled": false }), + ) + } + "resume" => { + let Some(mut rule) = state.replication.get_rule(&bucket_name) else { + return respond( + false, + StatusCode::NOT_FOUND, + "No replication configuration to resume.".to_string(), + json!({ "error": "No replication configuration to resume" }), + ); + }; + rule.enabled = true; + let mode = rule.mode.clone(); + state.replication.set_rule(rule); + + let message = if mode == crate::services::replication::MODE_ALL { + state + .replication + .clone() + .schedule_existing_objects_sync(bucket_name.clone()); + "Replication resumed. Existing object sync will continue in the background." + .to_string() + } else { + "Replication resumed.".to_string() + }; + + respond( + true, + StatusCode::OK, + message, + json!({ "action": "resume", "enabled": true, "mode": mode }), + ) + } + "create" => { + let target_connection_id = form.target_connection_id.trim(); + let target_bucket = form.target_bucket.trim(); + if target_connection_id.is_empty() || target_bucket.is_empty() { + return respond( + false, + StatusCode::BAD_REQUEST, + "Target connection and bucket are required.".to_string(), + json!({ "error": "Target connection and bucket are required" }), + ); + } + if state.connections.get(target_connection_id).is_none() { + return respond( + false, + StatusCode::BAD_REQUEST, + "Target connection was not found.".to_string(), + json!({ "error": "Target connection was not found" }), + ); + } + + let mode = match form.replication_mode.trim() { + crate::services::replication::MODE_ALL => crate::services::replication::MODE_ALL, + crate::services::replication::MODE_BIDIRECTIONAL => { + crate::services::replication::MODE_BIDIRECTIONAL + } + _ => crate::services::replication::MODE_NEW_ONLY, + }; + + state + .replication + .set_rule(crate::services::replication::ReplicationRule { + bucket_name: bucket_name.clone(), + target_connection_id: target_connection_id.to_string(), + target_bucket: target_bucket.to_string(), + enabled: true, + mode: mode.to_string(), + created_at: Some( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs_f64()) + .unwrap_or(0.0), + ), + stats: crate::services::replication::ReplicationStats::default(), + sync_deletions: true, + last_pull_at: None, + filter_prefix: None, + }); + + let message = if mode == crate::services::replication::MODE_ALL { + state + .replication + .clone() + .schedule_existing_objects_sync(bucket_name.clone()); + "Replication configured. Existing object sync will continue in the background." + .to_string() + } else { + "Replication configured. New uploads will be replicated.".to_string() + }; + + respond( + true, + StatusCode::OK, + message, + json!({ + "action": "create", + "enabled": true, + "mode": mode, + "target_connection_id": target_connection_id, + "target_bucket": target_bucket, + }), + ) + } + _ => respond( + false, + StatusCode::BAD_REQUEST, + "Invalid replication action.".to_string(), + json!({ "error": "Invalid action" }), + ), + } +} + +#[derive(serde::Deserialize)] +pub struct ConnectionForm { + pub name: String, + pub endpoint_url: String, + pub access_key: String, + #[serde(default)] + pub secret_key: String, + #[serde(default = "default_connection_region")] + pub region: String, + #[serde(default)] + pub csrf_token: String, +} + +fn default_connection_region() -> String { + "us-east-1".to_string() +} + +pub async fn create_connection( + State(state): State, + Extension(session): Extension, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let name = form.name.trim(); + let endpoint = form.endpoint_url.trim(); + let access_key = form.access_key.trim(); + let secret_key = form.secret_key.trim(); + let region = form.region.trim(); + + if name.is_empty() || endpoint.is_empty() || access_key.is_empty() || secret_key.is_empty() { + let message = "All connection fields are required.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/connections").into_response(); + } + + let connection = crate::stores::connections::RemoteConnection { + id: uuid::Uuid::new_v4().to_string(), + name: name.to_string(), + endpoint_url: endpoint.to_string(), + access_key: access_key.to_string(), + secret_key: secret_key.to_string(), + region: if region.is_empty() { + default_connection_region() + } else { + region.to_string() + }, + }; + + match state.connections.add(connection.clone()) { + Ok(()) => { + let message = format!("Connection '{}' created.", connection.name); + if wants_json { + axum::Json(json!({ + "ok": true, + "message": message, + "connection": { + "id": connection.id, + "name": connection.name, + "endpoint_url": connection.endpoint_url, + "access_key": connection.access_key, + "region": connection.region, + } + })) + .into_response() + } else { + session.write(|s| s.push_flash("success", message)); + Redirect::to("/ui/connections").into_response() + } + } + Err(err) => { + let message = format!("Failed to create connection: {}", err); + if wants_json { + ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response() + } else { + session.write(|s| s.push_flash("danger", message)); + Redirect::to("/ui/connections").into_response() + } + } + } +} + +pub async fn update_connection( + State(state): State, + Extension(session): Extension, + Path(connection_id): Path, + headers: HeaderMap, + Form(form): Form, +) -> Response { + let wants_json = wants_json(&headers); + let Some(mut connection) = state.connections.get(&connection_id) else { + let message = "Connection not found.".to_string(); + if wants_json { + return ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/connections").into_response(); + }; + + let name = form.name.trim(); + let endpoint = form.endpoint_url.trim(); + let access_key = form.access_key.trim(); + let secret_key = form.secret_key.trim(); + let region = form.region.trim(); + + if name.is_empty() || endpoint.is_empty() || access_key.is_empty() { + let message = "Name, endpoint, and access key are required.".to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to("/ui/connections").into_response(); + } + + connection.name = name.to_string(); + connection.endpoint_url = endpoint.to_string(); + connection.access_key = access_key.to_string(); + if !secret_key.is_empty() { + connection.secret_key = secret_key.to_string(); + } + connection.region = if region.is_empty() { + default_connection_region() + } else { + region.to_string() + }; + + match state.connections.add(connection.clone()) { + Ok(()) => { + let message = format!("Connection '{}' updated.", connection.name); + if wants_json { + axum::Json(json!({ + "ok": true, + "message": message, + "connection": { + "id": connection.id, + "name": connection.name, + "endpoint_url": connection.endpoint_url, + "access_key": connection.access_key, + "region": connection.region, + } + })) + .into_response() + } else { + session.write(|s| s.push_flash("success", message)); + Redirect::to("/ui/connections").into_response() + } + } + Err(err) => { + let message = format!("Failed to update connection: {}", err); + if wants_json { + ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response() + } else { + session.write(|s| s.push_flash("danger", message)); + Redirect::to("/ui/connections").into_response() + } + } + } +} + +#[derive(serde::Deserialize, Default)] +pub struct DeleteConnectionForm { + #[serde(default)] + pub csrf_token: String, +} + +pub async fn delete_connection( + State(state): State, + Extension(session): Extension, + Path(connection_id): Path, + headers: HeaderMap, + Form(_form): Form, +) -> Response { + let wants_json = wants_json(&headers); + match state.connections.delete(&connection_id) { + Ok(true) => { + let message = "Connection deleted.".to_string(); + if wants_json { + axum::Json(json!({ "ok": true, "message": message })).into_response() + } else { + session.write(|s| s.push_flash("success", message)); + Redirect::to("/ui/connections").into_response() + } + } + Ok(false) => { + let message = "Connection not found.".to_string(); + if wants_json { + ( + StatusCode::NOT_FOUND, + axum::Json(json!({ "error": message })), + ) + .into_response() + } else { + session.write(|s| s.push_flash("danger", message)); + Redirect::to("/ui/connections").into_response() + } + } + Err(err) => { + let message = format!("Failed to delete connection: {}", err); + if wants_json { + ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response() + } else { + session.write(|s| s.push_flash("danger", message)); + Redirect::to("/ui/connections").into_response() + } + } + } +} + +#[derive(serde::Deserialize)] +pub struct WebsiteDomainForm { + pub bucket: String, + #[serde(default)] + pub domain: String, + #[serde(default)] + pub csrf_token: String, +} + +#[derive(serde::Deserialize, Default)] +pub struct WebsiteDomainDeleteForm { + #[serde(default)] + pub csrf_token: String, +} + +pub async fn create_website_domain( + State(state): State, + Extension(session): Extension, + Form(form): Form, +) -> Response { + let Some(store) = &state.website_domains else { + session.write(|s| s.push_flash("danger", "Website hosting is not enabled.")); + return Redirect::to("/ui/website-domains").into_response(); + }; + + let domain = crate::services::website_domains::normalize_domain(&form.domain); + let bucket = form.bucket.trim().to_string(); + if !crate::services::website_domains::is_valid_domain(&domain) { + session.write(|s| s.push_flash("danger", "Enter a valid domain name.")); + return Redirect::to("/ui/website-domains").into_response(); + } + match state.storage.bucket_exists(&bucket).await { + Ok(true) => {} + _ => { + session + .write(|s| s.push_flash("danger", format!("Bucket '{}' does not exist.", bucket))); + return Redirect::to("/ui/website-domains").into_response(); + } + } + store.set_mapping(&domain, &bucket); + session.write(|s| { + s.push_flash( + "success", + format!("Domain '{}' mapped to '{}'.", domain, bucket), + ) + }); + Redirect::to("/ui/website-domains").into_response() +} + +pub async fn update_website_domain( + State(state): State, + Extension(session): Extension, + Path(domain): Path, + Form(form): Form, +) -> Response { + let Some(store) = &state.website_domains else { + session.write(|s| s.push_flash("danger", "Website hosting is not enabled.")); + return Redirect::to("/ui/website-domains").into_response(); + }; + + let domain = crate::services::website_domains::normalize_domain(&domain); + let bucket = form.bucket.trim().to_string(); + match state.storage.bucket_exists(&bucket).await { + Ok(true) => {} + _ => { + session + .write(|s| s.push_flash("danger", format!("Bucket '{}' does not exist.", bucket))); + return Redirect::to("/ui/website-domains").into_response(); + } + } + if store.get_bucket(&domain).is_none() { + session.write(|s| s.push_flash("danger", format!("Domain '{}' was not found.", domain))); + return Redirect::to("/ui/website-domains").into_response(); + } + store.set_mapping(&domain, &bucket); + session.write(|s| s.push_flash("success", format!("Domain '{}' updated.", domain))); + Redirect::to("/ui/website-domains").into_response() +} + +pub async fn delete_website_domain( + State(state): State, + Extension(session): Extension, + Path(domain): Path, + Form(_form): Form, +) -> Response { + let Some(store) = &state.website_domains else { + session.write(|s| s.push_flash("danger", "Website hosting is not enabled.")); + return Redirect::to("/ui/website-domains").into_response(); + }; + + let domain = crate::services::website_domains::normalize_domain(&domain); + if store.delete_mapping(&domain) { + session.write(|s| s.push_flash("success", format!("Domain '{}' removed.", domain))); + } else { + session.write(|s| s.push_flash("danger", format!("Domain '{}' was not found.", domain))); + } + Redirect::to("/ui/website-domains").into_response() +} + +pub async fn update_bucket_quota( + State(state): State, + Path(bucket_name): Path, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let mut config = match state.storage.get_bucket_config(&bucket_name).await { + Ok(cfg) => cfg, + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(); + } + }; + + if form.action.eq_ignore_ascii_case("remove") { + config.quota = None; + } else { + config.quota = Some(myfsio_common::types::QuotaConfig { + max_bytes: form.max_mb.map(|mb| mb.saturating_mul(1024 * 1024)), + max_objects: form.max_objects, + }); + } + + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => axum::Json(json!({ + "ok": true, + "has_quota": config.quota.is_some(), + "max_bytes": config.quota.as_ref().and_then(|q| q.max_bytes), + "max_objects": config.quota.as_ref().and_then(|q| q.max_objects), + "message": if config.quota.is_some() { "Quota settings saved." } else { "Quota removed." }, + })) + .into_response(), + Err(e) => ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(), + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateBucketEncryptionForm { + pub action: String, + #[serde(default)] + pub algorithm: String, + #[serde(default)] + pub kms_key_id: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_bucket_encryption( + State(state): State, + Path(bucket_name): Path, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let mut config = match state.storage.get_bucket_config(&bucket_name).await { + Ok(cfg) => cfg, + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(); + } + }; + + if form.action.eq_ignore_ascii_case("disable") { + config.encryption = None; + } else { + let mut inner = json!({ + "SSEAlgorithm": if form.algorithm == "aws:kms" { "aws:kms" } else { "AES256" } + }); + if form.algorithm == "aws:kms" && !form.kms_key_id.trim().is_empty() { + inner["KMSMasterKeyID"] = Value::String(form.kms_key_id.trim().to_string()); + } + config.encryption = Some(json!({ + "Rules": [{ + "ApplyServerSideEncryptionByDefault": inner + }] + })); + } + + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => { + let algorithm = config + .encryption + .as_ref() + .and_then(|value| value.get("Rules")) + .and_then(|rules| rules.as_array()) + .and_then(|rules| rules.first()) + .and_then(|rule| rule.get("ApplyServerSideEncryptionByDefault")) + .and_then(|inner| inner.get("SSEAlgorithm")) + .and_then(|v| v.as_str()) + .unwrap_or("AES256"); + axum::Json(json!({ + "ok": true, + "enabled": config.encryption.is_some(), + "algorithm": algorithm, + "message": if config.encryption.is_some() { "Encryption settings saved." } else { "Encryption disabled." }, + })) + .into_response() + } + Err(e) => ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(), + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateBucketPolicyForm { + pub mode: String, + #[serde(default)] + pub policy_document: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_bucket_policy( + State(state): State, + Extension(session): Extension, + Path(bucket_name): Path, + headers: HeaderMap, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let wants_json = wants_json(&headers); + let redirect_url = format!("/ui/buckets/{}?tab=permissions", bucket_name); + let mut config = match state.storage.get_bucket_config(&bucket_name).await { + Ok(cfg) => cfg, + Err(e) => { + let message = e.to_string(); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to(&redirect_url).into_response(); + } + }; + + if form.mode.eq_ignore_ascii_case("delete") { + config.policy = None; + } else { + let policy: Value = match serde_json::from_str(&form.policy_document) { + Ok(value) => value, + Err(e) => { + let message = format!("Invalid policy JSON: {}", e); + if wants_json { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response(); + } + session.write(|s| s.push_flash("danger", message)); + return Redirect::to(&redirect_url).into_response(); + } + }; + config.policy = Some(policy); + } + + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => { + let message = if config.policy.is_some() { + "Bucket policy saved." + } else { + "Bucket policy deleted." + }; + if wants_json { + axum::Json(json!({ + "ok": true, + "message": message, + })) + .into_response() + } else { + session.write(|s| s.push_flash("success", message)); + Redirect::to(&redirect_url).into_response() + } + } + Err(e) => { + let message = e.to_string(); + if wants_json { + ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": message })), + ) + .into_response() + } else { + session.write(|s| s.push_flash("danger", message)); + Redirect::to(&redirect_url).into_response() + } + } + } +} + +#[derive(serde::Deserialize)] +pub struct UpdateBucketWebsiteForm { + pub action: String, + #[serde(default)] + pub index_document: String, + #[serde(default)] + pub error_document: String, + #[serde(default)] + pub csrf_token: String, +} + +pub async fn update_bucket_website( + State(state): State, + Path(bucket_name): Path, + axum::extract::Form(form): axum::extract::Form, +) -> Response { + let mut config = match state.storage.get_bucket_config(&bucket_name).await { + Ok(cfg) => cfg, + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(); + } + }; + + if form.action.eq_ignore_ascii_case("disable") { + config.website = None; + } else { + let index_document = if form.index_document.trim().is_empty() { + "index.html".to_string() + } else { + form.index_document.trim().to_string() + }; + let error_document = form.error_document.trim().to_string(); + config.website = Some(json!({ + "index_document": index_document, + "error_document": if error_document.is_empty() { Value::Null } else { Value::String(error_document) } + })); + } + + match state.storage.set_bucket_config(&bucket_name, &config).await { + Ok(()) => { + let website = config.website.clone().unwrap_or(Value::Null); + axum::Json(json!({ + "ok": true, + "enabled": !website.is_null(), + "index_document": website.get("index_document").and_then(|v| v.as_str()).unwrap_or("index.html"), + "error_document": website.get("error_document").and_then(|v| v.as_str()).unwrap_or(""), + "message": if website.is_null() { "Website hosting disabled." } else { "Website settings saved." }, + })) + .into_response() + } + Err(e) => ( + StatusCode::BAD_REQUEST, + axum::Json(json!({ "error": e.to_string() })), + ) + .into_response(), + } +} + +pub async fn stub_post(Extension(session): Extension) -> Response { session.write(|s| s.push_flash("info", "This action is not yet implemented in the Rust UI.")); Redirect::to("/ui/buckets").into_response() } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/lib.rs b/rust/myfsio-engine/crates/myfsio-server/src/lib.rs index 34f6ebf..93ee066 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/lib.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/lib.rs @@ -9,24 +9,249 @@ pub mod templates; use axum::Router; -pub const SERVER_HEADER: &str = concat!("MyFSIO-Rust/", env!("CARGO_PKG_VERSION")); +pub const SERVER_HEADER: &str = "MyFSIO"; pub fn create_ui_router(state: state::AppState) -> Router { - use axum::routing::{get, post}; + use axum::routing::{delete, get, post, put}; use handlers::ui; + use handlers::ui_api; use handlers::ui_pages; let protected = Router::new() + .route("/", get(ui::root_redirect)) + .route("/ui", get(ui::root_redirect)) + .route("/ui/", get(ui::root_redirect)) .route("/ui/buckets", get(ui_pages::buckets_overview)) .route("/ui/buckets/create", post(ui_pages::create_bucket)) .route("/ui/buckets/{bucket_name}", get(ui_pages::bucket_detail)) + .route( + "/ui/buckets/{bucket_name}/delete", + post(ui_pages::delete_bucket), + ) + .route( + "/ui/buckets/{bucket_name}/versioning", + post(ui_pages::update_bucket_versioning), + ) + .route( + "/ui/buckets/{bucket_name}/quota", + post(ui_pages::update_bucket_quota), + ) + .route( + "/ui/buckets/{bucket_name}/encryption", + post(ui_pages::update_bucket_encryption), + ) + .route( + "/ui/buckets/{bucket_name}/policy", + post(ui_pages::update_bucket_policy), + ) + .route( + "/ui/buckets/{bucket_name}/replication", + post(ui_pages::update_bucket_replication), + ) + .route( + "/ui/buckets/{bucket_name}/website", + post(ui_pages::update_bucket_website), + ) + .route( + "/ui/buckets/{bucket_name}/upload", + post(ui_api::upload_object), + ) + .route( + "/ui/buckets/{bucket_name}/multipart/initiate", + post(ui_api::initiate_multipart_upload), + ) + .route( + "/ui/buckets/{bucket_name}/multipart/{upload_id}/part", + put(ui_api::upload_multipart_part), + ) + .route( + "/ui/buckets/{bucket_name}/multipart/{upload_id}/complete", + post(ui_api::complete_multipart_upload), + ) + .route( + "/ui/buckets/{bucket_name}/multipart/{upload_id}/abort", + delete(ui_api::abort_multipart_upload), + ) + .route( + "/ui/buckets/{bucket_name}/objects", + get(ui_api::list_bucket_objects), + ) + .route( + "/ui/buckets/{bucket_name}/objects/stream", + get(ui_api::stream_bucket_objects), + ) + .route( + "/ui/buckets/{bucket_name}/folders", + get(ui_api::list_bucket_folders), + ) + .route( + "/ui/buckets/{bucket_name}/copy-targets", + get(ui_api::list_copy_targets), + ) + .route( + "/ui/buckets/{bucket_name}/objects/{*rest}", + get(ui_api::object_get_dispatch).post(ui_api::object_post_dispatch), + ) + .route( + "/ui/buckets/{bucket_name}/acl", + get(ui_api::bucket_acl).post(ui_api::update_bucket_acl), + ) + .route( + "/ui/buckets/{bucket_name}/cors", + get(ui_api::bucket_cors).post(ui_api::update_bucket_cors), + ) + .route( + "/ui/buckets/{bucket_name}/lifecycle", + get(ui_api::bucket_lifecycle).post(ui_api::update_bucket_lifecycle), + ) + .route( + "/ui/buckets/{bucket_name}/lifecycle/history", + get(ui_api::lifecycle_history_stub), + ) + .route( + "/ui/buckets/{bucket_name}/replication/status", + get(ui_api::replication_status), + ) + .route( + "/ui/buckets/{bucket_name}/replication/failures", + get(ui_api::replication_failures).delete(ui_api::clear_replication_failures), + ) + .route( + "/ui/buckets/{bucket_name}/replication/failures/retry", + post(ui_api::retry_replication_failure), + ) + .route( + "/ui/buckets/{bucket_name}/replication/failures/retry-all", + post(ui_api::retry_all_replication_failures), + ) + .route( + "/ui/buckets/{bucket_name}/replication/failures/dismiss", + delete(ui_api::dismiss_replication_failure), + ) + .route( + "/ui/buckets/{bucket_name}/replication/failures/clear", + delete(ui_api::clear_replication_failures), + ) + .route( + "/ui/buckets/{bucket_name}/bulk-delete", + post(ui_api::bulk_delete_objects), + ) + .route( + "/ui/buckets/{bucket_name}/bulk-download", + post(ui_api::bulk_download_objects), + ) + .route( + "/ui/buckets/{bucket_name}/archived", + get(ui_api::archived_objects), + ) + .route( + "/ui/buckets/{bucket_name}/archived/{*rest}", + post(ui_api::archived_post_dispatch), + ) .route("/ui/iam", get(ui_pages::iam_dashboard)) + .route("/ui/iam/users", post(ui_pages::create_iam_user)) + .route("/ui/iam/users/{user_id}", post(ui_pages::update_iam_user)) + .route( + "/ui/iam/users/{user_id}/delete", + post(ui_pages::delete_iam_user), + ) + .route( + "/ui/iam/users/{user_id}/policies", + post(ui_pages::update_iam_policies), + ) + .route( + "/ui/iam/users/{user_id}/expiry", + post(ui_pages::update_iam_expiry), + ) + .route( + "/ui/iam/users/{user_id}/rotate-secret", + post(ui_pages::rotate_iam_secret), + ) + .route("/ui/connections/create", post(ui_pages::create_connection)) + .route("/ui/connections/test", post(ui_api::test_connection)) + .route( + "/ui/connections/{connection_id}", + post(ui_pages::update_connection), + ) + .route( + "/ui/connections/{connection_id}/delete", + post(ui_pages::delete_connection), + ) + .route( + "/ui/connections/{connection_id}/health", + get(ui_api::connection_health), + ) .route("/ui/sites", get(ui_pages::sites_dashboard)) + .route("/ui/sites/local", post(ui_pages::update_local_site)) + .route("/ui/sites/peers", post(ui_pages::add_peer_site)) + .route( + "/ui/sites/peers/{site_id}/update", + post(ui_pages::update_peer_site), + ) + .route( + "/ui/sites/peers/{site_id}/delete", + post(ui_pages::delete_peer_site), + ) + .route("/ui/sites/peers/{site_id}/health", get(ui_api::peer_health)) + .route( + "/ui/sites/peers/{site_id}/sync-stats", + get(ui_api::peer_sync_stats), + ) + .route( + "/ui/sites/peers/{site_id}/bidirectional-status", + get(ui_api::peer_bidirectional_status), + ) .route("/ui/connections", get(ui_pages::connections_dashboard)) .route("/ui/metrics", get(ui_pages::metrics_dashboard)) + .route( + "/ui/metrics/settings", + get(ui_api::metrics_settings).put(ui_api::update_metrics_settings), + ) + .route("/ui/metrics/api", get(ui_api::metrics_api)) + .route("/ui/metrics/history", get(ui_api::metrics_history)) + .route("/ui/metrics/operations", get(ui_api::metrics_operations)) + .route( + "/ui/metrics/operations/history", + get(ui_api::metrics_operations_history), + ) .route("/ui/system", get(ui_pages::system_dashboard)) - .route("/ui/website-domains", get(ui_pages::website_domains_dashboard)) + .route("/ui/system/gc/status", get(ui_api::gc_status_ui)) + .route("/ui/system/gc/run", post(ui_api::gc_run_ui)) + .route("/ui/system/gc/history", get(ui_api::gc_history_ui)) + .route( + "/ui/system/integrity/status", + get(ui_api::integrity_status_ui), + ) + .route("/ui/system/integrity/run", post(ui_api::integrity_run_ui)) + .route( + "/ui/system/integrity/history", + get(ui_api::integrity_history_ui), + ) + .route( + "/ui/website-domains", + get(ui_pages::website_domains_dashboard), + ) + .route( + "/ui/website-domains/create", + post(ui_pages::create_website_domain), + ) + .route( + "/ui/website-domains/{domain}", + post(ui_pages::update_website_domain), + ) + .route( + "/ui/website-domains/{domain}/delete", + post(ui_pages::delete_website_domain), + ) .route("/ui/replication/new", get(ui_pages::replication_wizard)) + .route( + "/ui/replication/create", + post(ui_pages::create_peer_replication_rules_from_query), + ) + .route( + "/ui/sites/peers/{site_id}/replication-rules", + post(ui_pages::create_peer_replication_rules), + ) .route("/ui/docs", get(ui_pages::docs_page)) .layer(axum::middleware::from_fn(ui::require_login)); @@ -40,18 +265,29 @@ pub fn create_ui_router(state: state::AppState) -> Router { secure: false, }; + let static_service = tower_http::services::ServeDir::new(&state.config.static_dir); + protected .merge(public) + .fallback(ui::not_found_page) .layer(axum::middleware::from_fn(middleware::csrf_layer)) .layer(axum::middleware::from_fn_with_state( session_state, middleware::session_layer, )) + .layer(axum::middleware::from_fn_with_state( + state.clone(), + middleware::ui_metrics_layer, + )) .with_state(state) + .nest_service("/static", static_service) + .layer(axum::middleware::from_fn(middleware::server_header)) + .layer(tower_http::compression::CompressionLayer::new()) } pub fn create_router(state: state::AppState) -> Router { let mut router = Router::new() + .route("/myfsio/health", axum::routing::get(handlers::health_check)) .route("/", axum::routing::get(handlers::list_buckets)) .route( "/{bucket}", @@ -61,6 +297,14 @@ pub fn create_router(state: state::AppState) -> Router { .head(handlers::head_bucket) .post(handlers::post_bucket), ) + .route( + "/{bucket}/", + axum::routing::put(handlers::create_bucket) + .get(handlers::get_bucket) + .delete(handlers::delete_bucket) + .head(handlers::head_bucket) + .post(handlers::post_bucket), + ) .route( "/{bucket}/{*key}", axum::routing::put(handlers::put_object) @@ -72,53 +316,189 @@ pub fn create_router(state: state::AppState) -> Router { if state.config.kms_enabled { router = router - .route("/kms/keys", axum::routing::get(handlers::kms::list_keys).post(handlers::kms::create_key)) - .route("/kms/keys/{key_id}", axum::routing::get(handlers::kms::get_key).delete(handlers::kms::delete_key)) - .route("/kms/keys/{key_id}/enable", axum::routing::post(handlers::kms::enable_key)) - .route("/kms/keys/{key_id}/disable", axum::routing::post(handlers::kms::disable_key)) + .route( + "/kms/keys", + axum::routing::get(handlers::kms::list_keys).post(handlers::kms::create_key), + ) + .route( + "/kms/keys/{key_id}", + axum::routing::get(handlers::kms::get_key).delete(handlers::kms::delete_key), + ) + .route( + "/kms/keys/{key_id}/enable", + axum::routing::post(handlers::kms::enable_key), + ) + .route( + "/kms/keys/{key_id}/disable", + axum::routing::post(handlers::kms::disable_key), + ) .route("/kms/encrypt", axum::routing::post(handlers::kms::encrypt)) .route("/kms/decrypt", axum::routing::post(handlers::kms::decrypt)) - .route("/kms/generate-data-key", axum::routing::post(handlers::kms::generate_data_key)); + .route( + "/kms/generate-data-key", + axum::routing::post(handlers::kms::generate_data_key), + ) + .route( + "/kms/generate-data-key-without-plaintext", + axum::routing::post(handlers::kms::generate_data_key_without_plaintext), + ) + .route( + "/kms/re-encrypt", + axum::routing::post(handlers::kms::re_encrypt), + ) + .route( + "/kms/generate-random", + axum::routing::post(handlers::kms::generate_random), + ) + .route( + "/kms/client/generate-key", + axum::routing::post(handlers::kms::client_generate_key), + ) + .route( + "/kms/client/encrypt", + axum::routing::post(handlers::kms::client_encrypt), + ) + .route( + "/kms/client/decrypt", + axum::routing::post(handlers::kms::client_decrypt), + ) + .route( + "/kms/materials/{key_id}", + axum::routing::post(handlers::kms::materials), + ); } router = router - .route("/admin/site/local", axum::routing::get(handlers::admin::get_local_site).put(handlers::admin::update_local_site)) - .route("/admin/site/all", axum::routing::get(handlers::admin::list_all_sites)) - .route("/admin/site/peers", axum::routing::post(handlers::admin::register_peer_site)) - .route("/admin/site/peers/{site_id}", axum::routing::get(handlers::admin::get_peer_site).put(handlers::admin::update_peer_site).delete(handlers::admin::delete_peer_site)) - .route("/admin/site/peers/{site_id}/health", axum::routing::post(handlers::admin::check_peer_health)) - .route("/admin/site/topology", axum::routing::get(handlers::admin::get_topology)) - .route("/admin/site/peers/{site_id}/bidirectional-status", axum::routing::get(handlers::admin::check_bidirectional_status)) - .route("/admin/iam/users", axum::routing::get(handlers::admin::iam_list_users)) - .route("/admin/iam/users/{identifier}", axum::routing::get(handlers::admin::iam_get_user)) - .route("/admin/iam/users/{identifier}/policies", axum::routing::get(handlers::admin::iam_get_user_policies)) - .route("/admin/iam/users/{identifier}/access-keys", axum::routing::post(handlers::admin::iam_create_access_key)) - .route("/admin/iam/users/{identifier}/access-keys/{access_key}", axum::routing::delete(handlers::admin::iam_delete_access_key)) - .route("/admin/iam/users/{identifier}/disable", axum::routing::post(handlers::admin::iam_disable_user)) - .route("/admin/iam/users/{identifier}/enable", axum::routing::post(handlers::admin::iam_enable_user)) - .route("/admin/website-domains", axum::routing::get(handlers::admin::list_website_domains).post(handlers::admin::create_website_domain)) - .route("/admin/website-domains/{domain}", axum::routing::get(handlers::admin::get_website_domain).put(handlers::admin::update_website_domain).delete(handlers::admin::delete_website_domain)) - .route("/admin/gc/status", axum::routing::get(handlers::admin::gc_status)) - .route("/admin/gc/run", axum::routing::post(handlers::admin::gc_run)) - .route("/admin/gc/history", axum::routing::get(handlers::admin::gc_history)) - .route("/admin/integrity/status", axum::routing::get(handlers::admin::integrity_status)) - .route("/admin/integrity/run", axum::routing::post(handlers::admin::integrity_run)) - .route("/admin/integrity/history", axum::routing::get(handlers::admin::integrity_history)); + .route( + "/admin/site", + axum::routing::get(handlers::admin::get_local_site) + .put(handlers::admin::update_local_site), + ) + .route( + "/admin/sites", + axum::routing::get(handlers::admin::list_all_sites) + .post(handlers::admin::register_peer_site), + ) + .route( + "/admin/sites/{site_id}", + axum::routing::get(handlers::admin::get_peer_site) + .put(handlers::admin::update_peer_site) + .delete(handlers::admin::delete_peer_site), + ) + .route( + "/admin/sites/{site_id}/health", + axum::routing::get(handlers::admin::check_peer_health) + .post(handlers::admin::check_peer_health), + ) + .route( + "/admin/sites/{site_id}/bidirectional-status", + axum::routing::get(handlers::admin::check_bidirectional_status), + ) + .route( + "/admin/topology", + axum::routing::get(handlers::admin::get_topology), + ) + .route( + "/admin/site/local", + axum::routing::get(handlers::admin::get_local_site) + .put(handlers::admin::update_local_site), + ) + .route( + "/admin/site/all", + axum::routing::get(handlers::admin::list_all_sites), + ) + .route( + "/admin/site/peers", + axum::routing::post(handlers::admin::register_peer_site), + ) + .route( + "/admin/site/peers/{site_id}", + axum::routing::get(handlers::admin::get_peer_site) + .put(handlers::admin::update_peer_site) + .delete(handlers::admin::delete_peer_site), + ) + .route( + "/admin/site/peers/{site_id}/health", + axum::routing::post(handlers::admin::check_peer_health), + ) + .route( + "/admin/site/topology", + axum::routing::get(handlers::admin::get_topology), + ) + .route( + "/admin/site/peers/{site_id}/bidirectional-status", + axum::routing::get(handlers::admin::check_bidirectional_status), + ) + .route( + "/admin/iam/users", + axum::routing::get(handlers::admin::iam_list_users), + ) + .route( + "/admin/iam/users/{identifier}", + axum::routing::get(handlers::admin::iam_get_user), + ) + .route( + "/admin/iam/users/{identifier}/policies", + axum::routing::get(handlers::admin::iam_get_user_policies), + ) + .route( + "/admin/iam/users/{identifier}/access-keys", + axum::routing::post(handlers::admin::iam_create_access_key), + ) + .route( + "/admin/iam/users/{identifier}/access-keys/{access_key}", + axum::routing::delete(handlers::admin::iam_delete_access_key), + ) + .route( + "/admin/iam/users/{identifier}/disable", + axum::routing::post(handlers::admin::iam_disable_user), + ) + .route( + "/admin/iam/users/{identifier}/enable", + axum::routing::post(handlers::admin::iam_enable_user), + ) + .route( + "/admin/website-domains", + axum::routing::get(handlers::admin::list_website_domains) + .post(handlers::admin::create_website_domain), + ) + .route( + "/admin/website-domains/{domain}", + axum::routing::get(handlers::admin::get_website_domain) + .put(handlers::admin::update_website_domain) + .delete(handlers::admin::delete_website_domain), + ) + .route( + "/admin/gc/status", + axum::routing::get(handlers::admin::gc_status), + ) + .route( + "/admin/gc/run", + axum::routing::post(handlers::admin::gc_run), + ) + .route( + "/admin/gc/history", + axum::routing::get(handlers::admin::gc_history), + ) + .route( + "/admin/integrity/status", + axum::routing::get(handlers::admin::integrity_status), + ) + .route( + "/admin/integrity/run", + axum::routing::post(handlers::admin::integrity_run), + ) + .route( + "/admin/integrity/history", + axum::routing::get(handlers::admin::integrity_history), + ); - let mut router = router + router .layer(axum::middleware::from_fn_with_state( state.clone(), middleware::auth_layer, )) .layer(axum::middleware::from_fn(middleware::server_header)) - .with_state(state.clone()); - - if state.config.ui_enabled { - let static_service = tower_http::services::ServeDir::new(&state.config.static_dir); - router = router - .nest_service("/static", static_service) - .merge(create_ui_router(state)); - } - - router + .layer(tower_http::compression::CompressionLayer::new()) + .with_state(state) } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/main.rs b/rust/myfsio-engine/crates/myfsio-server/src/main.rs index ece0c93..204511b 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/main.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/main.rs @@ -3,8 +3,18 @@ use myfsio_server::config::ServerConfig; use myfsio_server::state::AppState; #[derive(Parser)] -#[command(name = "myfsio", version, about = "MyFSIO S3-compatible storage engine")] +#[command( + name = "myfsio", + version, + about = "MyFSIO S3-compatible storage engine" +)] struct Cli { + #[arg(long, help = "Validate configuration and exit")] + check_config: bool, + #[arg(long, help = "Show configuration summary and exit")] + show_config: bool, + #[arg(long, help = "Reset admin credentials and exit")] + reset_cred: bool, #[command(subcommand)] command: Option, } @@ -17,9 +27,30 @@ enum Command { #[tokio::main] async fn main() { + load_env_files(); tracing_subscriber::fmt::init(); let cli = Cli::parse(); + let config = ServerConfig::from_env(); + + if cli.reset_cred { + reset_admin_credentials(&config); + return; + } + if cli.check_config || cli.show_config { + print_config_summary(&config); + if cli.check_config { + let issues = validate_config(&config); + for issue in &issues { + println!("{issue}"); + } + if issues.iter().any(|issue| issue.starts_with("CRITICAL:")) { + std::process::exit(1); + } + } + return; + } + match cli.command.unwrap_or(Command::Serve) { Command::Version => { println!("myfsio {}", env!("CARGO_PKG_VERSION")); @@ -28,19 +59,24 @@ async fn main() { Command::Serve => {} } - let config = ServerConfig::from_env(); + ensure_iam_bootstrap(&config); let bind_addr = config.bind_addr; + let ui_bind_addr = config.ui_bind_addr; - tracing::info!("MyFSIO Rust Engine starting on {}", bind_addr); + tracing::info!("MyFSIO Rust Engine starting — API on {}", bind_addr); + if config.ui_enabled { + tracing::info!("UI will bind on {}", ui_bind_addr); + } tracing::info!("Storage root: {}", config.storage_root.display()); tracing::info!("Region: {}", config.region); tracing::info!( - "Encryption: {}, KMS: {}, GC: {}, Lifecycle: {}, Integrity: {}, Metrics: {}, UI: {}", + "Encryption: {}, KMS: {}, GC: {}, Lifecycle: {}, Integrity: {}, Metrics History: {}, Operation Metrics: {}, UI: {}", config.encryption_enabled, config.kms_enabled, config.gc_enabled, config.lifecycle_enabled, config.integrity_enabled, + config.metrics_history_enabled, config.metrics_enabled, config.ui_enabled ); @@ -68,13 +104,17 @@ async fn main() { tracing::info!("Metrics collector background service started"); } + if let Some(ref system_metrics) = state.system_metrics { + bg_handles.push(system_metrics.clone().start_background()); + tracing::info!("System metrics history collector started"); + } + if config.lifecycle_enabled { - let lifecycle = std::sync::Arc::new( - myfsio_server::services::lifecycle::LifecycleService::new( + let lifecycle = + std::sync::Arc::new(myfsio_server::services::lifecycle::LifecycleService::new( state.storage.clone(), myfsio_server::services::lifecycle::LifecycleConfig::default(), - ), - ); + )); bg_handles.push(lifecycle.start_background()); tracing::info!("Lifecycle manager background service started"); } @@ -87,15 +127,21 @@ async fn main() { tracing::info!("Site sync worker started"); } - let app = myfsio_server::create_router(state); + let ui_enabled = config.ui_enabled; + let api_app = myfsio_server::create_router(state.clone()); + let ui_app = if ui_enabled { + Some(myfsio_server::create_ui_router(state.clone())) + } else { + None + }; - let listener = match tokio::net::TcpListener::bind(bind_addr).await { + let api_listener = match tokio::net::TcpListener::bind(bind_addr).await { Ok(listener) => listener, Err(err) => { if err.kind() == std::io::ErrorKind::AddrInUse { - tracing::error!("Port already in use: {}", bind_addr); + tracing::error!("API port already in use: {}", bind_addr); } else { - tracing::error!("Failed to bind {}: {}", bind_addr, err); + tracing::error!("Failed to bind API {}: {}", bind_addr, err); } for handle in bg_handles { handle.abort(); @@ -103,17 +149,67 @@ async fn main() { std::process::exit(1); } }; - tracing::info!("Listening on {}", bind_addr); + tracing::info!("API listening on {}", bind_addr); - if let Err(err) = axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await - { - tracing::error!("Server exited with error: {}", err); - for handle in bg_handles { - handle.abort(); + let ui_listener = if let Some(ref app) = ui_app { + let _ = app; + match tokio::net::TcpListener::bind(ui_bind_addr).await { + Ok(listener) => { + tracing::info!("UI listening on {}", ui_bind_addr); + Some(listener) + } + Err(err) => { + if err.kind() == std::io::ErrorKind::AddrInUse { + tracing::error!("UI port already in use: {}", ui_bind_addr); + } else { + tracing::error!("Failed to bind UI {}: {}", ui_bind_addr, err); + } + for handle in bg_handles { + handle.abort(); + } + std::process::exit(1); + } + } + } else { + None + }; + + let shutdown = shutdown_signal_shared(); + let api_shutdown = shutdown.clone(); + let api_task = tokio::spawn(async move { + axum::serve(api_listener, api_app) + .with_graceful_shutdown(async move { + api_shutdown.notified().await; + }) + .await + }); + + let ui_task = if let (Some(listener), Some(app)) = (ui_listener, ui_app) { + let ui_shutdown = shutdown.clone(); + Some(tokio::spawn(async move { + axum::serve(listener, app) + .with_graceful_shutdown(async move { + ui_shutdown.notified().await; + }) + .await + })) + } else { + None + }; + + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for Ctrl+C"); + tracing::info!("Shutdown signal received"); + shutdown.notify_waiters(); + + if let Err(err) = api_task.await.unwrap_or(Ok(())) { + tracing::error!("API server exited with error: {}", err); + } + if let Some(task) = ui_task { + if let Err(err) = task.await.unwrap_or(Ok(())) { + tracing::error!("UI server exited with error: {}", err); } - std::process::exit(1); } for handle in bg_handles { @@ -121,9 +217,209 @@ async fn main() { } } -async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("Failed to listen for Ctrl+C"); - tracing::info!("Shutdown signal received"); +fn print_config_summary(config: &ServerConfig) { + println!("MyFSIO Rust Configuration"); + println!("Version: {}", env!("CARGO_PKG_VERSION")); + println!("API bind: {}", config.bind_addr); + println!("UI bind: {}", config.ui_bind_addr); + println!("UI enabled: {}", config.ui_enabled); + println!("Storage root: {}", config.storage_root.display()); + println!("IAM config: {}", config.iam_config_path.display()); + println!("Region: {}", config.region); + println!("Encryption enabled: {}", config.encryption_enabled); + println!("KMS enabled: {}", config.kms_enabled); + println!("GC enabled: {}", config.gc_enabled); + println!("Integrity enabled: {}", config.integrity_enabled); + println!("Lifecycle enabled: {}", config.lifecycle_enabled); + println!( + "Website hosting enabled: {}", + config.website_hosting_enabled + ); + println!("Site sync enabled: {}", config.site_sync_enabled); + println!( + "Metrics history enabled: {}", + config.metrics_history_enabled + ); + println!("Operation metrics enabled: {}", config.metrics_enabled); +} + +fn validate_config(config: &ServerConfig) -> Vec { + let mut issues = Vec::new(); + + if config.ui_enabled && config.bind_addr == config.ui_bind_addr { + issues.push( + "CRITICAL: API and UI bind addresses cannot be identical when UI is enabled." + .to_string(), + ); + } + if config.presigned_url_min_expiry > config.presigned_url_max_expiry { + issues.push("CRITICAL: PRESIGNED_URL_MIN_EXPIRY_SECONDS cannot exceed PRESIGNED_URL_MAX_EXPIRY_SECONDS.".to_string()); + } + if let Err(err) = std::fs::create_dir_all(&config.storage_root) { + issues.push(format!( + "CRITICAL: Cannot create storage root {}: {}", + config.storage_root.display(), + err + )); + } + if let Some(parent) = config.iam_config_path.parent() { + if let Err(err) = std::fs::create_dir_all(parent) { + issues.push(format!( + "CRITICAL: Cannot create IAM config directory {}: {}", + parent.display(), + err + )); + } + } + if config.encryption_enabled && config.secret_key.is_none() { + issues.push( + "WARNING: ENCRYPTION_ENABLED=true but SECRET_KEY is not configured; secure-at-rest config encryption is unavailable.".to_string(), + ); + } + if config.site_sync_enabled && !config.website_hosting_enabled { + issues.push( + "INFO: SITE_SYNC_ENABLED=true without WEBSITE_HOSTING_ENABLED; this is valid but unrelated.".to_string(), + ); + } + + issues +} + +fn shutdown_signal_shared() -> std::sync::Arc { + std::sync::Arc::new(tokio::sync::Notify::new()) +} + +fn load_env_files() { + let cwd = std::env::current_dir().ok(); + let mut candidates: Vec = Vec::new(); + candidates.push(std::path::PathBuf::from("/opt/myfsio/myfsio.env")); + if let Some(ref dir) = cwd { + candidates.push(dir.join(".env")); + candidates.push(dir.join("myfsio.env")); + for ancestor in dir.ancestors().skip(1).take(4) { + candidates.push(ancestor.join(".env")); + candidates.push(ancestor.join("myfsio.env")); + } + } + + let mut seen = std::collections::HashSet::new(); + for path in candidates { + if !seen.insert(path.clone()) { + continue; + } + if path.is_file() { + match dotenvy::from_path_override(&path) { + Ok(()) => eprintln!("Loaded env file: {}", path.display()), + Err(e) => eprintln!("Failed to load env file {}: {}", path.display(), e), + } + } + } +} + +fn ensure_iam_bootstrap(config: &ServerConfig) { + let iam_path = &config.iam_config_path; + if iam_path.exists() { + return; + } + + let access_key = std::env::var("ADMIN_ACCESS_KEY") + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| format!("AK{}", uuid::Uuid::new_v4().simple())); + let secret_key = std::env::var("ADMIN_SECRET_KEY") + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| format!("SK{}", uuid::Uuid::new_v4().simple())); + + let user_id = format!("u-{}", &uuid::Uuid::new_v4().simple().to_string()[..16]); + let created_at = chrono::Utc::now().to_rfc3339(); + + let body = serde_json::json!({ + "version": 2, + "users": [{ + "user_id": user_id, + "display_name": "Local Admin", + "enabled": true, + "access_keys": [{ + "access_key": access_key, + "secret_key": secret_key, + "status": "active", + "created_at": created_at, + }], + "policies": [{ + "bucket": "*", + "actions": ["*"], + "prefix": "*", + }] + }] + }); + + let json = match serde_json::to_string_pretty(&body) { + Ok(s) => s, + Err(e) => { + tracing::error!("Failed to serialize IAM bootstrap config: {}", e); + return; + } + }; + + if let Some(parent) = iam_path.parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + tracing::error!( + "Failed to create IAM config dir {}: {}", + parent.display(), + e + ); + return; + } + } + + if let Err(e) = std::fs::write(iam_path, json) { + tracing::error!( + "Failed to write IAM bootstrap config {}: {}", + iam_path.display(), + e + ); + return; + } + + tracing::info!("============================================================"); + tracing::info!("MYFSIO - ADMIN CREDENTIALS INITIALIZED"); + tracing::info!("============================================================"); + tracing::info!("Access Key: {}", access_key); + tracing::info!("Secret Key: {}", secret_key); + tracing::info!("Saved to: {}", iam_path.display()); + tracing::info!("============================================================"); +} + +fn reset_admin_credentials(config: &ServerConfig) { + if let Some(parent) = config.iam_config_path.parent() { + if let Err(err) = std::fs::create_dir_all(parent) { + eprintln!( + "Failed to create IAM config directory {}: {}", + parent.display(), + err + ); + std::process::exit(1); + } + } + + if config.iam_config_path.exists() { + let backup = config + .iam_config_path + .with_extension(format!("bak-{}", chrono::Utc::now().timestamp())); + if let Err(err) = std::fs::rename(&config.iam_config_path, &backup) { + eprintln!( + "Failed to back up existing IAM config {}: {}", + config.iam_config_path.display(), + err + ); + std::process::exit(1); + } + println!("Backed up existing IAM config to {}", backup.display()); + } + + ensure_iam_bootstrap(config); + println!("Admin credentials reset."); } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/middleware/auth.rs b/rust/myfsio-engine/crates/myfsio-server/src/middleware/auth.rs index 5dfa0d5..db2283d 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/middleware/auth.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/middleware/auth.rs @@ -1,5 +1,5 @@ use axum::extract::{Request, State}; -use axum::http::{Method, StatusCode}; +use axum::http::{header, HeaderMap, Method, StatusCode}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; @@ -7,51 +7,470 @@ use chrono::{NaiveDateTime, Utc}; use myfsio_auth::sigv4; use myfsio_common::error::{S3Error, S3ErrorCode}; use myfsio_common::types::Principal; +use myfsio_storage::traits::StorageEngine; +use serde_json::Value; +use std::time::Instant; +use tokio::io::AsyncReadExt; use crate::state::AppState; -pub async fn auth_layer( - State(state): State, - mut req: Request, - next: Next, +fn website_error_response( + status: StatusCode, + body: Option>, + content_type: &str, ) -> Response { - let uri = req.uri().clone(); - let path = uri.path().to_string(); + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap()); + if let Some(ref body) = body { + headers.insert( + header::CONTENT_LENGTH, + body.len().to_string().parse().unwrap(), + ); + (status, headers, body.clone()).into_response() + } else { + (status, headers).into_response() + } +} - if path == "/" && req.method() == axum::http::Method::GET { - match try_auth(&state, &req) { - AuthResult::Ok(principal) => { - if let Err(err) = authorize_request(&state, &principal, &req) { - return error_response(err, &path); - } - req.extensions_mut().insert(principal); - } - AuthResult::Denied(err) => return error_response(err, &path), - AuthResult::NoAuth => { - return error_response( - S3Error::new(S3ErrorCode::AccessDenied, "Missing credentials"), - &path, - ); - } +fn parse_range_header(range_header: &str, total_size: u64) -> Option<(u64, u64)> { + let range_spec = range_header.strip_prefix("bytes=")?; + if let Some(suffix) = range_spec.strip_prefix('-') { + let suffix_len: u64 = suffix.parse().ok()?; + if suffix_len == 0 || suffix_len > total_size { + return None; } - return next.run(req).await; + return Some((total_size - suffix_len, total_size - 1)); } - match try_auth(&state, &req) { - AuthResult::Ok(principal) => { - if let Err(err) = authorize_request(&state, &principal, &req) { - return error_response(err, &path); + let (start_str, end_str) = range_spec.split_once('-')?; + let start: u64 = start_str.parse().ok()?; + let end = if end_str.is_empty() { + total_size.saturating_sub(1) + } else { + end_str + .parse::() + .ok()? + .min(total_size.saturating_sub(1)) + }; + + if start > end || start >= total_size { + return None; + } + Some((start, end)) +} + +fn website_content_type(key: &str, metadata: &std::collections::HashMap) -> String { + metadata + .get("__content_type__") + .filter(|value| !value.trim().is_empty()) + .cloned() + .unwrap_or_else(|| { + mime_guess::from_path(key) + .first_raw() + .unwrap_or("application/octet-stream") + .to_string() + }) +} + +fn parse_website_config(value: &Value) -> Option<(String, Option)> { + match value { + Value::Object(map) => { + let index_document = map + .get("index_document") + .or_else(|| map.get("IndexDocument")) + .and_then(|v| v.as_str()) + .unwrap_or("index.html") + .to_string(); + let error_document = map + .get("error_document") + .or_else(|| map.get("ErrorDocument")) + .and_then(|v| v.as_str()) + .map(|v| v.to_string()); + Some((index_document, error_document)) + } + Value::String(raw) => { + if let Ok(json) = serde_json::from_str::(raw) { + return parse_website_config(&json); } - req.extensions_mut().insert(principal); - next.run(req).await + let doc = roxmltree::Document::parse(raw).ok()?; + let index_document = doc + .descendants() + .find(|node| node.is_element() && node.tag_name().name() == "Suffix") + .and_then(|node| node.text()) + .map(|text| text.trim().to_string()) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| "index.html".to_string()); + let error_document = doc + .descendants() + .find(|node| node.is_element() && node.tag_name().name() == "Key") + .and_then(|node| node.text()) + .map(|text| text.trim().to_string()) + .filter(|text| !text.is_empty()); + Some((index_document, error_document)) } - AuthResult::Denied(err) => error_response(err, &path), - AuthResult::NoAuth => { - error_response( - S3Error::new(S3ErrorCode::AccessDenied, "Missing credentials"), - &path, + _ => None, + } +} + +async fn serve_website_document( + state: &AppState, + bucket: &str, + key: &str, + method: &axum::http::Method, + range_header: Option<&str>, + status: StatusCode, +) -> Option { + let metadata = state.storage.get_object_metadata(bucket, key).await.ok()?; + let (meta, mut reader) = state.storage.get_object(bucket, key).await.ok()?; + let content_type = website_content_type(key, &metadata); + + if method == axum::http::Method::HEAD { + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert( + header::CONTENT_LENGTH, + meta.size.to_string().parse().unwrap(), + ); + headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap()); + return Some((status, headers).into_response()); + } + + let mut bytes = Vec::new(); + if reader.read_to_end(&mut bytes).await.is_err() { + return None; + } + + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap()); + + if status == StatusCode::OK { + if let Some(range_header) = range_header { + let Some((start, end)) = parse_range_header(range_header, bytes.len() as u64) else { + let mut range_headers = HeaderMap::new(); + range_headers.insert( + header::CONTENT_RANGE, + format!("bytes */{}", bytes.len()).parse().unwrap(), + ); + return Some((StatusCode::RANGE_NOT_SATISFIABLE, range_headers).into_response()); + }; + let body = bytes[start as usize..=end as usize].to_vec(); + headers.insert( + header::CONTENT_RANGE, + format!("bytes {}-{}/{}", start, end, bytes.len()) + .parse() + .unwrap(), + ); + headers.insert( + header::CONTENT_LENGTH, + body.len().to_string().parse().unwrap(), + ); + return Some((StatusCode::PARTIAL_CONTENT, headers, body).into_response()); + } + } + + headers.insert( + header::CONTENT_LENGTH, + bytes.len().to_string().parse().unwrap(), + ); + Some((status, headers, bytes).into_response()) +} + +async fn maybe_serve_website( + state: &AppState, + method: Method, + host: String, + uri_path: String, + range_header: Option, +) -> Option { + if !state.config.website_hosting_enabled { + return None; + } + if method != axum::http::Method::GET && method != axum::http::Method::HEAD { + return None; + } + let request_path = uri_path.trim_start_matches('/').to_string(); + let store = state.website_domains.as_ref()?; + let bucket = store.get_bucket(&host)?; + if !matches!(state.storage.bucket_exists(&bucket).await, Ok(true)) { + return Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )); + } + + let bucket_config = state.storage.get_bucket_config(&bucket).await.ok()?; + let Some(website_config) = bucket_config.website.as_ref() else { + return Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )); + }; + let Some((index_document, error_document)) = parse_website_config(website_config) else { + return Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )); + }; + + let mut object_key = if request_path.is_empty() || uri_path.ends_with('/') { + if request_path.is_empty() { + index_document.clone() + } else { + format!("{}{}", request_path, index_document) + } + } else { + request_path.clone() + }; + + let exists = state + .storage + .head_object(&bucket, &object_key) + .await + .is_ok(); + if !exists && !request_path.is_empty() && !request_path.ends_with('/') { + let alternate = format!("{}/{}", request_path, index_document); + if state.storage.head_object(&bucket, &alternate).await.is_ok() { + object_key = alternate; + } else if let Some(error_key) = error_document.as_deref() { + return serve_website_document( + state, + &bucket, + error_key, + &method, + range_header.as_deref(), + StatusCode::NOT_FOUND, ) + .await + .or_else(|| { + Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )) + }); + } else { + return Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )); } + } else if !exists { + if let Some(error_key) = error_document.as_deref() { + return serve_website_document( + state, + &bucket, + error_key, + &method, + range_header.as_deref(), + StatusCode::NOT_FOUND, + ) + .await + .or_else(|| { + Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )) + }); + } + return Some(website_error_response( + StatusCode::NOT_FOUND, + None, + "text/plain; charset=utf-8", + )); + } + + serve_website_document( + state, + &bucket, + &object_key, + &method, + range_header.as_deref(), + StatusCode::OK, + ) + .await +} + +pub async fn auth_layer(State(state): State, mut req: Request, next: Next) -> Response { + let start = Instant::now(); + let uri = req.uri().clone(); + let path = uri.path().to_string(); + let method = req.method().clone(); + let query = uri.query().unwrap_or("").to_string(); + let copy_source = req + .headers() + .get("x-amz-copy-source") + .and_then(|v| v.to_str().ok()) + .map(|value| value.to_string()); + let endpoint_type = classify_endpoint(&path, &query); + let bytes_in = req + .headers() + .get(axum::http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + let host = req + .headers() + .get(header::HOST) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split(':').next()) + .map(|value| value.trim().to_ascii_lowercase()); + let range_header = req + .headers() + .get(header::RANGE) + .and_then(|value| value.to_str().ok()) + .map(|value| value.to_string()); + + let response = if path == "/myfsio/health" || path == "/health" { + next.run(req).await + } else if let Some(response) = maybe_serve_website( + &state, + method.clone(), + host.unwrap_or_default(), + path.clone(), + range_header, + ) + .await + { + response + } else { + match try_auth(&state, &req) { + AuthResult::NoAuth => match authorize_request( + &state, + None, + &method, + &path, + &query, + copy_source.as_deref(), + ) + .await + { + Ok(()) => next.run(req).await, + Err(err) => error_response(err, &path), + }, + AuthResult::Ok(principal) => { + if let Err(err) = authorize_request( + &state, + Some(&principal), + &method, + &path, + &query, + copy_source.as_deref(), + ) + .await + { + error_response(err, &path) + } else { + req.extensions_mut().insert(principal); + next.run(req).await + } + } + AuthResult::Denied(err) => error_response(err, &path), + } + }; + + if let Some(metrics) = &state.metrics { + let latency_ms = start.elapsed().as_secs_f64() * 1000.0; + let status = response.status().as_u16(); + let bytes_out = response + .headers() + .get(axum::http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + let error_code = if status >= 400 { + Some(s3_code_for_status(status)) + } else { + None + }; + metrics.record_request( + method.as_str(), + endpoint_type, + status, + latency_ms, + bytes_in, + bytes_out, + error_code, + ); + } + + response +} + +fn classify_endpoint(path: &str, query: &str) -> &'static str { + if path == "/" { + return "list_buckets"; + } + let segments: Vec<&str> = path + .trim_start_matches('/') + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + if segments.is_empty() { + return "other"; + } + if segments.len() == 1 { + if query.contains("uploads") { + return "list_multipart_uploads"; + } + if query.contains("versioning") { + return "bucket_versioning"; + } + if query.contains("lifecycle") { + return "bucket_lifecycle"; + } + if query.contains("policy") { + return "bucket_policy"; + } + if query.contains("website") { + return "bucket_website"; + } + if query.contains("encryption") { + return "bucket_encryption"; + } + if query.contains("replication") { + return "bucket_replication"; + } + return "bucket"; + } + if query.contains("uploadId") { + return "multipart_part"; + } + if query.contains("uploads") { + return "multipart_init"; + } + if query.contains("tagging") { + return "object_tagging"; + } + if query.contains("acl") { + return "object_acl"; + } + "object" +} + +fn s3_code_for_status(status: u16) -> &'static str { + match status { + 400 => "BadRequest", + 401 => "Unauthorized", + 403 => "AccessDenied", + 404 => "NotFound", + 405 => "MethodNotAllowed", + 409 => "Conflict", + 411 => "MissingContentLength", + 412 => "PreconditionFailed", + 413 => "EntityTooLarge", + 416 => "InvalidRange", + 500 => "InternalError", + 501 => "NotImplemented", + 503 => "ServiceUnavailable", + _ => "Other", } } @@ -61,20 +480,45 @@ enum AuthResult { NoAuth, } -fn authorize_request(state: &AppState, principal: &Principal, req: &Request) -> Result<(), S3Error> { - let path = req.uri().path(); +async fn authorize_request( + state: &AppState, + principal: Option<&Principal>, + method: &Method, + path: &str, + query: &str, + copy_source: Option<&str>, +) -> Result<(), S3Error> { + if path == "/myfsio/health" || path == "/health" { + return Ok(()); + } if path == "/" { - if state.iam.authorize(principal, None, "list", None) { - return Ok(()); + if let Some(principal) = principal { + if state.iam.authorize(principal, None, "list", None) { + return Ok(()); + } + return Err(S3Error::new(S3ErrorCode::AccessDenied, "Access denied")); } - return Err(S3Error::new(S3ErrorCode::AccessDenied, "Access denied")); + return Err(S3Error::new( + S3ErrorCode::AccessDenied, + "Missing credentials", + )); } if path.starts_with("/admin/") || path.starts_with("/kms/") { - return Ok(()); + return if principal.is_some() { + Ok(()) + } else { + Err(S3Error::new( + S3ErrorCode::AccessDenied, + "Missing credentials", + )) + }; } - let mut segments = path.trim_start_matches('/').split('/').filter(|s| !s.is_empty()); + let mut segments = path + .trim_start_matches('/') + .split('/') + .filter(|s| !s.is_empty()); let bucket = match segments.next() { Some(b) => b, None => { @@ -82,29 +526,25 @@ fn authorize_request(state: &AppState, principal: &Principal, req: &Request) -> } }; let remaining: Vec<&str> = segments.collect(); - let query = req.uri().query().unwrap_or(""); if remaining.is_empty() { - let action = resolve_bucket_action(req.method(), query); - if state.iam.authorize(principal, Some(bucket), action, None) { - return Ok(()); - } - return Err(S3Error::new(S3ErrorCode::AccessDenied, "Access denied")); + let action = resolve_bucket_action(method, query); + return authorize_action(state, principal, bucket, action, None).await; } let object_key = remaining.join("/"); - if req.method() == Method::PUT { - if let Some(copy_source) = req - .headers() - .get("x-amz-copy-source") - .and_then(|v| v.to_str().ok()) - { + if *method == Method::PUT { + if let Some(copy_source) = copy_source { let source = copy_source.strip_prefix('/').unwrap_or(copy_source); if let Some((src_bucket, src_key)) = source.split_once('/') { let source_allowed = - state.iam.authorize(principal, Some(src_bucket), "read", Some(src_key)); + authorize_action(state, principal, src_bucket, "read", Some(src_key)) + .await + .is_ok(); let dest_allowed = - state.iam.authorize(principal, Some(bucket), "write", Some(&object_key)); + authorize_action(state, principal, bucket, "write", Some(&object_key)) + .await + .is_ok(); if source_allowed && dest_allowed { return Ok(()); } @@ -113,15 +553,270 @@ fn authorize_request(state: &AppState, principal: &Principal, req: &Request) -> } } - let action = resolve_object_action(req.method(), query); - if state - .iam - .authorize(principal, Some(bucket), action, Some(&object_key)) - { + let action = resolve_object_action(method, query); + authorize_action(state, principal, bucket, action, Some(&object_key)).await +} + +async fn authorize_action( + state: &AppState, + principal: Option<&Principal>, + bucket: &str, + action: &str, + object_key: Option<&str>, +) -> Result<(), S3Error> { + let iam_allowed = principal + .map(|principal| { + state + .iam + .authorize(principal, Some(bucket), action, object_key) + }) + .unwrap_or(false); + let policy_decision = evaluate_bucket_policy( + state, + principal.map(|principal| principal.access_key.as_str()), + bucket, + action, + object_key, + ) + .await; + + if matches!(policy_decision, PolicyDecision::Deny) { + return Err(S3Error::new( + S3ErrorCode::AccessDenied, + "Access denied by bucket policy", + )); + } + if iam_allowed || matches!(policy_decision, PolicyDecision::Allow) { return Ok(()); } - Err(S3Error::new(S3ErrorCode::AccessDenied, "Access denied")) + if principal.is_some() { + Err(S3Error::new(S3ErrorCode::AccessDenied, "Access denied")) + } else { + Err(S3Error::new( + S3ErrorCode::AccessDenied, + "Missing credentials", + )) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PolicyDecision { + Allow, + Deny, + Neutral, +} + +async fn evaluate_bucket_policy( + state: &AppState, + access_key: Option<&str>, + bucket: &str, + action: &str, + object_key: Option<&str>, +) -> PolicyDecision { + let config = match state.storage.get_bucket_config(bucket).await { + Ok(config) => config, + Err(_) => return PolicyDecision::Neutral, + }; + let policy: &Value = match config.policy.as_ref() { + Some(policy) => policy, + None => return PolicyDecision::Neutral, + }; + let mut decision = PolicyDecision::Neutral; + + match policy.get("Statement") { + Some(Value::Array(items)) => { + for statement in items.iter() { + match evaluate_policy_statement(statement, access_key, bucket, action, object_key) { + PolicyDecision::Deny => return PolicyDecision::Deny, + PolicyDecision::Allow => decision = PolicyDecision::Allow, + PolicyDecision::Neutral => {} + } + } + } + Some(statement) => { + return evaluate_policy_statement(statement, access_key, bucket, action, object_key); + } + None => return PolicyDecision::Neutral, + } + + decision +} + +fn evaluate_policy_statement( + statement: &Value, + access_key: Option<&str>, + bucket: &str, + action: &str, + object_key: Option<&str>, +) -> PolicyDecision { + if !statement_matches_principal(statement, access_key) + || !statement_matches_action(statement, action) + || !statement_matches_resource(statement, bucket, object_key) + { + return PolicyDecision::Neutral; + } + + match statement + .get("Effect") + .and_then(|value| value.as_str()) + .map(|value| value.to_ascii_lowercase()) + .as_deref() + { + Some("deny") => PolicyDecision::Deny, + Some("allow") => PolicyDecision::Allow, + _ => PolicyDecision::Neutral, + } +} + +fn statement_matches_principal(statement: &Value, access_key: Option<&str>) -> bool { + match statement.get("Principal") { + Some(principal) => principal_value_matches(principal, access_key), + None => false, + } +} + +fn principal_value_matches(value: &Value, access_key: Option<&str>) -> bool { + match value { + Value::String(token) => token == "*" || access_key == Some(token.as_str()), + Value::Array(items) => items + .iter() + .any(|item| principal_value_matches(item, access_key)), + Value::Object(map) => map + .values() + .any(|item| principal_value_matches(item, access_key)), + _ => false, + } +} + +fn statement_matches_action(statement: &Value, action: &str) -> bool { + match statement.get("Action") { + Some(Value::String(value)) => policy_action_matches(value, action), + Some(Value::Array(items)) => items.iter().any(|item| { + item.as_str() + .map(|value| policy_action_matches(value, action)) + .unwrap_or(false) + }), + _ => false, + } +} + +fn policy_action_matches(policy_action: &str, requested_action: &str) -> bool { + let normalized_policy_action = normalize_policy_action(policy_action); + normalized_policy_action == "*" || normalized_policy_action == requested_action +} + +fn normalize_policy_action(action: &str) -> String { + let normalized = action.trim().to_ascii_lowercase(); + if normalized == "*" { + return normalized; + } + match normalized.as_str() { + "s3:listbucket" + | "s3:listallmybuckets" + | "s3:listbucketversions" + | "s3:listmultipartuploads" + | "s3:listparts" => "list".to_string(), + "s3:getobject" + | "s3:getobjectversion" + | "s3:getobjecttagging" + | "s3:getobjectversiontagging" + | "s3:getobjectacl" + | "s3:getbucketversioning" + | "s3:headobject" + | "s3:headbucket" => "read".to_string(), + "s3:putobject" + | "s3:createbucket" + | "s3:putobjecttagging" + | "s3:putbucketversioning" + | "s3:createmultipartupload" + | "s3:uploadpart" + | "s3:completemultipartupload" + | "s3:abortmultipartupload" + | "s3:copyobject" => "write".to_string(), + "s3:deleteobject" + | "s3:deleteobjectversion" + | "s3:deletebucket" + | "s3:deleteobjecttagging" => "delete".to_string(), + "s3:putobjectacl" | "s3:putbucketacl" | "s3:getbucketacl" => "share".to_string(), + "s3:putbucketpolicy" | "s3:getbucketpolicy" | "s3:deletebucketpolicy" => { + "policy".to_string() + } + "s3:getreplicationconfiguration" + | "s3:putreplicationconfiguration" + | "s3:deletereplicationconfiguration" + | "s3:replicateobject" + | "s3:replicatetags" + | "s3:replicatedelete" => "replication".to_string(), + "s3:getlifecycleconfiguration" + | "s3:putlifecycleconfiguration" + | "s3:deletelifecycleconfiguration" + | "s3:getbucketlifecycle" + | "s3:putbucketlifecycle" => "lifecycle".to_string(), + "s3:getbucketcors" | "s3:putbucketcors" | "s3:deletebucketcors" => "cors".to_string(), + other => other.to_string(), + } +} + +fn statement_matches_resource(statement: &Value, bucket: &str, object_key: Option<&str>) -> bool { + match statement.get("Resource") { + Some(Value::String(resource)) => resource_matches(resource, bucket, object_key), + Some(Value::Array(items)) => items.iter().any(|item| { + item.as_str() + .map(|resource| resource_matches(resource, bucket, object_key)) + .unwrap_or(false) + }), + _ => false, + } +} + +fn resource_matches(resource: &str, bucket: &str, object_key: Option<&str>) -> bool { + let remainder = match resource.strip_prefix("arn:aws:s3:::") { + Some(value) => value, + None => return false, + }; + + match remainder.split_once('/') { + Some((resource_bucket, resource_key)) => object_key + .map(|key| wildcard_match(bucket, resource_bucket) && wildcard_match(key, resource_key)) + .unwrap_or(false), + None => object_key.is_none() && wildcard_match(bucket, remainder), + } +} + +fn wildcard_match(value: &str, pattern: &str) -> bool { + let value = value.as_bytes(); + let pattern = pattern.as_bytes(); + let mut value_idx = 0usize; + let mut pattern_idx = 0usize; + let mut star_idx: Option = None; + let mut match_idx = 0usize; + + while value_idx < value.len() { + if pattern_idx < pattern.len() + && (pattern[pattern_idx] == b'?' + || pattern[pattern_idx].eq_ignore_ascii_case(&value[value_idx])) + { + value_idx += 1; + pattern_idx += 1; + } else if pattern_idx < pattern.len() && pattern[pattern_idx] == b'*' { + star_idx = Some(pattern_idx); + pattern_idx += 1; + match_idx = value_idx; + } else if let Some(star) = star_idx { + pattern_idx = star + 1; + match_idx += 1; + value_idx = match_idx; + } else { + return false; + } + } + + while pattern_idx < pattern.len() && pattern[pattern_idx] == b'*' { + pattern_idx += 1; + } + + pattern_idx == pattern.len() } fn resolve_bucket_action(method: &Method, query: &str) -> &'static str { @@ -186,10 +881,18 @@ fn resolve_bucket_action(method: &Method, query: &str) -> &'static str { fn resolve_object_action(method: &Method, query: &str) -> &'static str { if has_query_key(query, "tagging") { - return if *method == Method::GET { "read" } else { "write" }; + return if *method == Method::GET { + "read" + } else { + "write" + }; } if has_query_key(query, "acl") { - return if *method == Method::GET { "read" } else { "write" }; + return if *method == Method::GET { + "read" + } else { + "write" + }; } if has_query_key(query, "retention") || has_query_key(query, "legal-hold") { return "object_lock"; @@ -241,14 +944,16 @@ fn try_auth(state: &AppState, req: &Request) -> AuthResult { } if let (Some(ak), Some(sk)) = ( - req.headers().get("x-access-key").and_then(|v| v.to_str().ok()), - req.headers().get("x-secret-key").and_then(|v| v.to_str().ok()), + req.headers() + .get("x-access-key") + .and_then(|v| v.to_str().ok()), + req.headers() + .get("x-secret-key") + .and_then(|v| v.to_str().ok()), ) { return match state.iam.authenticate(ak, sk) { Some(principal) => AuthResult::Ok(principal), - None => AuthResult::Denied( - S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch), - ), + None => AuthResult::Denied(S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch)), }; } @@ -263,9 +968,10 @@ fn verify_sigv4_header(state: &AppState, req: &Request, auth_str: &str) -> AuthR .collect(); if parts.len() != 3 { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Malformed Authorization header"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Malformed Authorization header", + )); } let credential = parts[0].strip_prefix("Credential=").unwrap_or(""); @@ -274,9 +980,10 @@ fn verify_sigv4_header(state: &AppState, req: &Request, auth_str: &str) -> AuthR let cred_parts: Vec<&str> = credential.split('/').collect(); if cred_parts.len() != 5 { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Malformed credential"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Malformed credential", + )); } let access_key = cred_parts[0]; @@ -292,21 +999,22 @@ fn verify_sigv4_header(state: &AppState, req: &Request, auth_str: &str) -> AuthR .unwrap_or(""); if amz_date.is_empty() { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::AccessDenied, "Missing Date header"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::AccessDenied, + "Missing Date header", + )); } - if let Some(err) = check_timestamp_freshness(amz_date, state.config.sigv4_timestamp_tolerance_secs) { + if let Some(err) = + check_timestamp_freshness(amz_date, state.config.sigv4_timestamp_tolerance_secs) + { return AuthResult::Denied(err); } let secret_key = match state.iam.get_secret_key(access_key) { Some(sk) => sk, None => { - return AuthResult::Denied( - S3Error::from_code(S3ErrorCode::InvalidAccessKeyId), - ); + return AuthResult::Denied(S3Error::from_code(S3ErrorCode::InvalidAccessKeyId)); } }; @@ -350,16 +1058,12 @@ fn verify_sigv4_header(state: &AppState, req: &Request, auth_str: &str) -> AuthR ); if !verified { - return AuthResult::Denied( - S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch), - ); + return AuthResult::Denied(S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch)); } match state.iam.get_principal(access_key) { Some(p) => AuthResult::Ok(p), - None => AuthResult::Denied( - S3Error::from_code(S3ErrorCode::InvalidAccessKeyId), - ), + None => AuthResult::Denied(S3Error::from_code(S3ErrorCode::InvalidAccessKeyId)), } } @@ -374,9 +1078,10 @@ fn verify_sigv4_query(state: &AppState, req: &Request) -> AuthResult { let credential = match param_map.get("X-Amz-Credential") { Some(c) => *c, None => { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Missing X-Amz-Credential"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing X-Amz-Credential", + )); } }; @@ -387,33 +1092,37 @@ fn verify_sigv4_query(state: &AppState, req: &Request) -> AuthResult { let provided_signature = match param_map.get("X-Amz-Signature") { Some(s) => *s, None => { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Missing X-Amz-Signature"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing X-Amz-Signature", + )); } }; let amz_date = match param_map.get("X-Amz-Date") { Some(d) => *d, None => { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Missing X-Amz-Date"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing X-Amz-Date", + )); } }; let expires_str = match param_map.get("X-Amz-Expires") { Some(e) => *e, None => { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Missing X-Amz-Expires"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Missing X-Amz-Expires", + )); } }; let cred_parts: Vec<&str> = credential.split('/').collect(); if cred_parts.len() != 5 { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Malformed credential"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Malformed credential", + )); } let access_key = cred_parts[0]; @@ -424,44 +1133,44 @@ fn verify_sigv4_query(state: &AppState, req: &Request) -> AuthResult { let expires: u64 = match expires_str.parse() { Ok(e) => e, Err(_) => { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "Invalid X-Amz-Expires"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "Invalid X-Amz-Expires", + )); } }; if expires < state.config.presigned_url_min_expiry || expires > state.config.presigned_url_max_expiry { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::InvalidArgument, "X-Amz-Expires out of range"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::InvalidArgument, + "X-Amz-Expires out of range", + )); } - if let Ok(request_time) = - NaiveDateTime::parse_from_str(amz_date, "%Y%m%dT%H%M%SZ") - { + if let Ok(request_time) = NaiveDateTime::parse_from_str(amz_date, "%Y%m%dT%H%M%SZ") { let request_utc = request_time.and_utc(); let now = Utc::now(); let elapsed = (now - request_utc).num_seconds(); if elapsed > expires as i64 { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::AccessDenied, "Request has expired"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::AccessDenied, + "Request has expired", + )); } if elapsed < -(state.config.sigv4_timestamp_tolerance_secs as i64) { - return AuthResult::Denied( - S3Error::new(S3ErrorCode::AccessDenied, "Request is too far in the future"), - ); + return AuthResult::Denied(S3Error::new( + S3ErrorCode::AccessDenied, + "Request is too far in the future", + )); } } let secret_key = match state.iam.get_secret_key(access_key) { Some(sk) => sk, None => { - return AuthResult::Denied( - S3Error::from_code(S3ErrorCode::InvalidAccessKeyId), - ); + return AuthResult::Denied(S3Error::from_code(S3ErrorCode::InvalidAccessKeyId)); } }; @@ -505,16 +1214,12 @@ fn verify_sigv4_query(state: &AppState, req: &Request) -> AuthResult { ); if !verified { - return AuthResult::Denied( - S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch), - ); + return AuthResult::Denied(S3Error::from_code(S3ErrorCode::SignatureDoesNotMatch)); } match state.iam.get_principal(access_key) { Some(p) => AuthResult::Ok(p), - None => AuthResult::Denied( - S3Error::from_code(S3ErrorCode::InvalidAccessKeyId), - ), + None => AuthResult::Denied(S3Error::from_code(S3ErrorCode::InvalidAccessKeyId)), } } @@ -543,10 +1248,7 @@ fn parse_query_params(query: &str) -> Vec<(String, String)> { let mut parts = pair.splitn(2, '='); let key = parts.next()?; let value = parts.next().unwrap_or(""); - Some(( - urlencoding_decode(key), - urlencoding_decode(value), - )) + Some((urlencoding_decode(key), urlencoding_decode(value))) }) .collect() } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/middleware/mod.rs b/rust/myfsio-engine/crates/myfsio-server/src/middleware/mod.rs index a7e6347..b8ab0ef 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/middleware/mod.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/middleware/mod.rs @@ -4,15 +4,82 @@ pub mod session; pub use auth::auth_layer; pub use session::{csrf_layer, session_layer, SessionHandle, SessionLayerState}; -use axum::extract::Request; +use axum::extract::{Request, State}; use axum::middleware::Next; use axum::response::Response; +use std::time::Instant; + +use crate::state::AppState; pub async fn server_header(req: Request, next: Next) -> Response { let mut resp = next.run(req).await; - resp.headers_mut().insert( - "server", - crate::SERVER_HEADER.parse().unwrap(), - ); + resp.headers_mut() + .insert("server", crate::SERVER_HEADER.parse().unwrap()); resp } + +pub async fn ui_metrics_layer(State(state): State, req: Request, next: Next) -> Response { + let metrics = match state.metrics.clone() { + Some(m) => m, + None => return next.run(req).await, + }; + let start = Instant::now(); + let method = req.method().clone(); + let path = req.uri().path().to_string(); + let endpoint_type = classify_ui_endpoint(&path); + let bytes_in = req + .headers() + .get(axum::http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + let response = next.run(req).await; + + let latency_ms = start.elapsed().as_secs_f64() * 1000.0; + let status = response.status().as_u16(); + let bytes_out = response + .headers() + .get(axum::http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + let error_code = if status >= 400 { Some("UIError") } else { None }; + metrics.record_request( + method.as_str(), + endpoint_type, + status, + latency_ms, + bytes_in, + bytes_out, + error_code, + ); + + response +} + +fn classify_ui_endpoint(path: &str) -> &'static str { + if path.contains("/upload") { + "ui_upload" + } else if path.starts_with("/ui/buckets/") { + "ui_bucket" + } else if path.starts_with("/ui/iam") { + "ui_iam" + } else if path.starts_with("/ui/sites") { + "ui_sites" + } else if path.starts_with("/ui/connections") { + "ui_connections" + } else if path.starts_with("/ui/metrics") { + "ui_metrics" + } else if path.starts_with("/ui/system") { + "ui_system" + } else if path.starts_with("/ui/website-domains") { + "ui_website_domains" + } else if path.starts_with("/ui/replication") { + "ui_replication" + } else if path.starts_with("/login") || path.starts_with("/logout") { + "ui_auth" + } else { + "ui_other" + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/middleware/session.rs b/rust/myfsio-engine/crates/myfsio-server/src/middleware/session.rs index 3fc5347..60618bc 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/middleware/session.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/middleware/session.rs @@ -62,18 +62,14 @@ pub async fn session_layer( ) -> Response { let cookie_id = extract_session_cookie(&req); - let (session_id, session_data, is_new) = match cookie_id.and_then(|id| { - state - .store - .get(&id) - .map(|data| (id.clone(), data)) - }) { - Some((id, data)) => (id, data, false), - None => { - let (id, data) = state.store.create(); - (id, data, true) - } - }; + let (session_id, session_data, is_new) = + match cookie_id.and_then(|id| state.store.get(&id).map(|data| (id.clone(), data))) { + Some((id, data)) => (id, data, false), + None => { + let (id, data) = state.store.create(); + (id, data, true) + } + }; let handle = SessionHandle::new(session_id.clone(), session_data); req.extensions_mut().insert(handle.clone()); @@ -95,6 +91,8 @@ pub async fn session_layer( } pub async fn csrf_layer(req: Request, next: Next) -> Response { + const CSRF_HEADER_ALIAS: &str = "x-csrftoken"; + let method = req.method().clone(); let needs_check = matches!( method, @@ -126,29 +124,33 @@ pub async fn csrf_layer(req: Request, next: Next) -> Response { let header_token = req .headers() .get(CSRF_HEADER_NAME) + .or_else(|| req.headers().get(CSRF_HEADER_ALIAS)) .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); - if let Some(token) = header_token { - if csrf_tokens_match(&expected, &token) { + if let Some(token) = header_token.as_deref() { + if csrf_tokens_match(&expected, token) { return next.run(req).await; } } + let content_type = req + .headers() + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + let (parts, body) = req.into_parts(); let bytes = match axum::body::to_bytes(body, usize::MAX).await { Ok(b) => b, Err(_) => return (StatusCode::BAD_REQUEST, "Body read failed").into_response(), }; - let content_type = parts - .headers - .get(header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - let form_token = if content_type.starts_with("application/x-www-form-urlencoded") { extract_form_token(&bytes) + } else if content_type.starts_with("multipart/form-data") { + extract_multipart_token(&content_type, &bytes) } else { None }; @@ -160,9 +162,32 @@ pub async fn csrf_layer(req: Request, next: Next) -> Response { } } + tracing::warn!( + path = %parts.uri.path(), + content_type = %content_type, + expected_len = expected.len(), + header_present = header_token.is_some(), + "CSRF token mismatch" + ); (StatusCode::FORBIDDEN, "Invalid CSRF token").into_response() } +fn extract_multipart_token(content_type: &str, body: &[u8]) -> Option { + let boundary = multer::parse_boundary(content_type).ok()?; + let prefix = format!("--{}", boundary); + let text = std::str::from_utf8(body).ok()?; + let needle = "name=\"csrf_token\""; + let idx = text.find(needle)?; + let after = &text[idx + needle.len()..]; + let body_start = after.find("\r\n\r\n")? + 4; + let tail = &after[body_start..]; + let end = tail + .find(&format!("\r\n--{}", prefix.trim_start_matches("--"))) + .or_else(|| tail.find("\r\n--")) + .unwrap_or(tail.len()); + Some(tail[..end].trim().to_string()) +} + fn extract_session_cookie(req: &Request) -> Option { let raw = req.headers().get(header::COOKIE)?.to_str().ok()?; for pair in raw.split(';') { diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/access_logging.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/access_logging.rs new file mode 100644 index 0000000..0bc03bc --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/access_logging.rs @@ -0,0 +1,105 @@ +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoggingConfiguration { + pub target_bucket: String, + #[serde(default)] + pub target_prefix: String, + #[serde(default = "default_enabled")] + pub enabled: bool, +} + +fn default_enabled() -> bool { + true +} + +#[derive(Serialize, Deserialize)] +struct StoredLoggingFile { + #[serde(rename = "LoggingEnabled")] + logging_enabled: Option, +} + +#[derive(Serialize, Deserialize)] +struct StoredLoggingEnabled { + #[serde(rename = "TargetBucket")] + target_bucket: String, + #[serde(rename = "TargetPrefix", default)] + target_prefix: String, +} + +pub struct AccessLoggingService { + storage_root: PathBuf, + cache: RwLock>>, +} + +impl AccessLoggingService { + pub fn new(storage_root: &Path) -> Self { + Self { + storage_root: storage_root.to_path_buf(), + cache: RwLock::new(HashMap::new()), + } + } + + fn config_path(&self, bucket: &str) -> PathBuf { + self.storage_root + .join(".myfsio.sys") + .join("buckets") + .join(bucket) + .join("logging.json") + } + + pub fn get(&self, bucket: &str) -> Option { + if let Some(cached) = self.cache.read().get(bucket).cloned() { + return cached; + } + + let path = self.config_path(bucket); + let config = if path.exists() { + std::fs::read_to_string(&path) + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .and_then(|f| f.logging_enabled) + .map(|e| LoggingConfiguration { + target_bucket: e.target_bucket, + target_prefix: e.target_prefix, + enabled: true, + }) + } else { + None + }; + + self.cache + .write() + .insert(bucket.to_string(), config.clone()); + config + } + + pub fn set(&self, bucket: &str, config: LoggingConfiguration) -> std::io::Result<()> { + let path = self.config_path(bucket); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let stored = StoredLoggingFile { + logging_enabled: Some(StoredLoggingEnabled { + target_bucket: config.target_bucket.clone(), + target_prefix: config.target_prefix.clone(), + }), + }; + let json = serde_json::to_string_pretty(&stored) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + std::fs::write(&path, json)?; + self.cache.write().insert(bucket.to_string(), Some(config)); + Ok(()) + } + + pub fn delete(&self, bucket: &str) { + let path = self.config_path(bucket); + if path.exists() { + let _ = std::fs::remove_file(&path); + } + self.cache.write().insert(bucket.to_string(), None); + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/gc.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/gc.rs index e1d930f..37bb402 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/gc.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/gc.rs @@ -28,6 +28,7 @@ pub struct GcService { storage_root: PathBuf, config: GcConfig, running: Arc>, + started_at: Arc>>, history: Arc>>, history_path: PathBuf, } @@ -53,6 +54,7 @@ impl GcService { storage_root, config, running: Arc::new(RwLock::new(false)), + started_at: Arc::new(RwLock::new(None)), history: Arc::new(RwLock::new(history)), history_path, } @@ -60,9 +62,17 @@ impl GcService { pub async fn status(&self) -> Value { let running = *self.running.read().await; + let scan_elapsed_seconds = self + .started_at + .read() + .await + .as_ref() + .map(|started| started.elapsed().as_secs_f64()); json!({ "enabled": true, "running": running, + "scanning": running, + "scan_elapsed_seconds": scan_elapsed_seconds, "interval_hours": self.config.interval_hours, "temp_file_max_age_hours": self.config.temp_file_max_age_hours, "multipart_max_age_days": self.config.multipart_max_age_days, @@ -73,7 +83,9 @@ impl GcService { pub async fn history(&self) -> Value { let history = self.history.read().await; - json!({ "executions": *history }) + let mut executions: Vec = history.iter().cloned().collect(); + executions.reverse(); + json!({ "executions": executions }) } pub async fn run_now(&self, dry_run: bool) -> Result { @@ -84,12 +96,14 @@ impl GcService { } *running = true; } + *self.started_at.write().await = Some(Instant::now()); let start = Instant::now(); let result = self.execute_gc(dry_run || self.config.dry_run).await; let elapsed = start.elapsed().as_secs_f64(); *self.running.write().await = false; + *self.started_at.write().await = None; let mut result_json = result.clone(); if let Some(obj) = result_json.as_object_mut() { @@ -124,9 +138,12 @@ impl GcService { let mut errors: Vec = Vec::new(); let now = std::time::SystemTime::now(); - let temp_max_age = std::time::Duration::from_secs_f64(self.config.temp_file_max_age_hours * 3600.0); - let multipart_max_age = std::time::Duration::from_secs(self.config.multipart_max_age_days * 86400); - let lock_max_age = std::time::Duration::from_secs_f64(self.config.lock_file_max_age_hours * 3600.0); + let temp_max_age = + std::time::Duration::from_secs_f64(self.config.temp_file_max_age_hours * 3600.0); + let multipart_max_age = + std::time::Duration::from_secs(self.config.multipart_max_age_days * 86400); + let lock_max_age = + std::time::Duration::from_secs_f64(self.config.lock_file_max_age_hours * 3600.0); let tmp_dir = self.storage_root.join(".myfsio.sys").join("tmp"); if tmp_dir.exists() { @@ -140,7 +157,10 @@ impl GcService { let size = metadata.len(); if !dry_run { if let Err(e) = std::fs::remove_file(entry.path()) { - errors.push(format!("Failed to remove temp file: {}", e)); + errors.push(format!( + "Failed to remove temp file: {}", + e + )); continue; } } @@ -242,7 +262,10 @@ impl GcService { if let Some(parent) = self.history_path.parent() { let _ = std::fs::create_dir_all(parent); } - let _ = std::fs::write(&self.history_path, serde_json::to_string_pretty(&data).unwrap_or_default()); + let _ = std::fs::write( + &self.history_path, + serde_json::to_string_pretty(&data).unwrap_or_default(), + ); } pub fn start_background(self: Arc) -> tokio::task::JoinHandle<()> { diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/integrity.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/integrity.rs index 7cf2f03..0d7ef03 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/integrity.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/integrity.rs @@ -1,11 +1,17 @@ +use myfsio_common::constants::{ + BUCKET_META_DIR, BUCKET_VERSIONS_DIR, INDEX_FILE, SYSTEM_BUCKETS_DIR, SYSTEM_ROOT, +}; use myfsio_storage::fs_backend::FsStorageBackend; -use myfsio_storage::traits::StorageEngine; -use serde_json::{json, Value}; -use std::path::PathBuf; +use serde_json::{json, Map, Value}; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; use tokio::sync::RwLock; +const MAX_ISSUES: usize = 500; +const INTERNAL_FOLDERS: &[&str] = &[".meta", ".versions", ".multipart"]; + pub struct IntegrityConfig { pub interval_hours: f64, pub batch_size: usize, @@ -17,7 +23,7 @@ impl Default for IntegrityConfig { fn default() -> Self { Self { interval_hours: 24.0, - batch_size: 1000, + batch_size: 10_000, auto_heal: false, dry_run: false, } @@ -25,21 +31,70 @@ impl Default for IntegrityConfig { } pub struct IntegrityService { + #[allow(dead_code)] storage: Arc, + storage_root: PathBuf, config: IntegrityConfig, running: Arc>, + started_at: Arc>>, history: Arc>>, history_path: PathBuf, } +#[derive(Default)] +struct ScanState { + objects_scanned: u64, + buckets_scanned: u64, + corrupted_objects: u64, + orphaned_objects: u64, + phantom_metadata: u64, + stale_versions: u64, + etag_cache_inconsistencies: u64, + issues: Vec, + errors: Vec, +} + +impl ScanState { + fn batch_exhausted(&self, batch_size: usize) -> bool { + self.objects_scanned >= batch_size as u64 + } + + fn push_issue(&mut self, issue_type: &str, bucket: &str, key: &str, detail: String) { + if self.issues.len() < MAX_ISSUES { + self.issues.push(json!({ + "issue_type": issue_type, + "bucket": bucket, + "key": key, + "detail": detail, + })); + } + } + + fn into_json(self, elapsed: f64) -> Value { + json!({ + "objects_scanned": self.objects_scanned, + "buckets_scanned": self.buckets_scanned, + "corrupted_objects": self.corrupted_objects, + "orphaned_objects": self.orphaned_objects, + "phantom_metadata": self.phantom_metadata, + "stale_versions": self.stale_versions, + "etag_cache_inconsistencies": self.etag_cache_inconsistencies, + "issues_healed": 0, + "issues": self.issues, + "errors": self.errors, + "execution_time_seconds": elapsed, + }) + } +} + impl IntegrityService { pub fn new( storage: Arc, - storage_root: &std::path::Path, + storage_root: &Path, config: IntegrityConfig, ) -> Self { let history_path = storage_root - .join(".myfsio.sys") + .join(SYSTEM_ROOT) .join("config") .join("integrity_history.json"); @@ -55,8 +110,10 @@ impl IntegrityService { Self { storage, + storage_root: storage_root.to_path_buf(), config, running: Arc::new(RwLock::new(false)), + started_at: Arc::new(RwLock::new(None)), history: Arc::new(RwLock::new(history)), history_path, } @@ -64,9 +121,17 @@ impl IntegrityService { pub async fn status(&self) -> Value { let running = *self.running.read().await; + let scan_elapsed_seconds = self + .started_at + .read() + .await + .as_ref() + .map(|started| started.elapsed().as_secs_f64()); json!({ "enabled": true, "running": running, + "scanning": running, + "scan_elapsed_seconds": scan_elapsed_seconds, "interval_hours": self.config.interval_hours, "batch_size": self.config.batch_size, "auto_heal": self.config.auto_heal, @@ -76,7 +141,9 @@ impl IntegrityService { pub async fn history(&self) -> Value { let history = self.history.read().await; - json!({ "executions": *history }) + let mut executions: Vec = history.iter().cloned().collect(); + executions.reverse(); + json!({ "executions": executions }) } pub async fn run_now(&self, dry_run: bool, auto_heal: bool) -> Result { @@ -87,23 +154,31 @@ impl IntegrityService { } *running = true; } + *self.started_at.write().await = Some(Instant::now()); let start = Instant::now(); - let result = self.check_integrity(dry_run, auto_heal).await; + let storage_root = self.storage_root.clone(); + let batch_size = self.config.batch_size; + let result = + tokio::task::spawn_blocking(move || scan_all_buckets(&storage_root, batch_size)) + .await + .unwrap_or_else(|e| { + let mut st = ScanState::default(); + st.errors.push(format!("scan task failed: {}", e)); + st + }); let elapsed = start.elapsed().as_secs_f64(); *self.running.write().await = false; + *self.started_at.write().await = None; - let mut result_json = result.clone(); - if let Some(obj) = result_json.as_object_mut() { - obj.insert("execution_time_seconds".to_string(), json!(elapsed)); - } + let result_json = result.into_json(elapsed); let record = json!({ "timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0, "dry_run": dry_run, "auto_heal": auto_heal, - "result": result_json, + "result": result_json.clone(), }); { @@ -116,62 +191,7 @@ impl IntegrityService { } self.save_history().await; - Ok(result) - } - - async fn check_integrity(&self, _dry_run: bool, _auto_heal: bool) -> Value { - let buckets = match self.storage.list_buckets().await { - Ok(b) => b, - Err(e) => return json!({"error": e.to_string()}), - }; - - let mut objects_scanned = 0u64; - let mut corrupted = 0u64; - let mut phantom_metadata = 0u64; - let mut errors: Vec = Vec::new(); - - for bucket in &buckets { - let params = myfsio_common::types::ListParams { - max_keys: self.config.batch_size, - ..Default::default() - }; - let objects = match self.storage.list_objects(&bucket.name, ¶ms).await { - Ok(r) => r.objects, - Err(e) => { - errors.push(format!("{}: {}", bucket.name, e)); - continue; - } - }; - - for obj in &objects { - objects_scanned += 1; - match self.storage.get_object_path(&bucket.name, &obj.key).await { - Ok(path) => { - if !path.exists() { - phantom_metadata += 1; - } else if let Some(ref expected_etag) = obj.etag { - match myfsio_crypto::hashing::md5_file(&path) { - Ok(actual_etag) => { - if &actual_etag != expected_etag { - corrupted += 1; - } - } - Err(e) => errors.push(format!("{}:{}: {}", bucket.name, obj.key, e)), - } - } - } - Err(e) => errors.push(format!("{}:{}: {}", bucket.name, obj.key, e)), - } - } - } - - json!({ - "objects_scanned": objects_scanned, - "buckets_scanned": buckets.len(), - "corrupted_objects": corrupted, - "phantom_metadata": phantom_metadata, - "errors": errors, - }) + Ok(result_json) } async fn save_history(&self) { @@ -202,3 +222,511 @@ impl IntegrityService { }) } } + +fn scan_all_buckets(storage_root: &Path, batch_size: usize) -> ScanState { + let mut state = ScanState::default(); + let buckets = match list_bucket_names(storage_root) { + Ok(b) => b, + Err(e) => { + state.errors.push(format!("list buckets: {}", e)); + return state; + } + }; + + for bucket in &buckets { + if state.batch_exhausted(batch_size) { + break; + } + state.buckets_scanned += 1; + + let bucket_path = storage_root.join(bucket); + let meta_root = storage_root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join(BUCKET_META_DIR); + + let index_entries = collect_index_entries(&meta_root); + + check_corrupted(&mut state, bucket, &bucket_path, &index_entries, batch_size); + check_phantom(&mut state, bucket, &bucket_path, &index_entries, batch_size); + check_orphaned(&mut state, bucket, &bucket_path, &index_entries, batch_size); + check_stale_versions(&mut state, storage_root, bucket, batch_size); + check_etag_cache(&mut state, storage_root, bucket, &index_entries, batch_size); + } + + state +} + +fn list_bucket_names(storage_root: &Path) -> std::io::Result> { + let mut names = Vec::new(); + if !storage_root.exists() { + return Ok(names); + } + for entry in std::fs::read_dir(storage_root)? { + let entry = entry?; + let name = entry.file_name().to_string_lossy().to_string(); + if name == SYSTEM_ROOT { + continue; + } + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + names.push(name); + } + } + Ok(names) +} + +#[allow(dead_code)] +struct IndexEntryInfo { + entry: Value, + index_file: PathBuf, + key_name: String, +} + +fn collect_index_entries(meta_root: &Path) -> HashMap { + let mut out: HashMap = HashMap::new(); + if !meta_root.exists() { + return out; + } + + let mut stack: Vec = vec![meta_root.to_path_buf()]; + while let Some(dir) = stack.pop() { + let rd = match std::fs::read_dir(&dir) { + Ok(r) => r, + Err(_) => continue, + }; + for entry in rd.flatten() { + let path = entry.path(); + let ft = match entry.file_type() { + Ok(t) => t, + Err(_) => continue, + }; + if ft.is_dir() { + stack.push(path); + continue; + } + if entry.file_name().to_string_lossy() != INDEX_FILE { + continue; + } + let rel_dir = match path.parent().and_then(|p| p.strip_prefix(meta_root).ok()) { + Some(p) => p.to_path_buf(), + None => continue, + }; + let dir_prefix = if rel_dir.as_os_str().is_empty() { + String::new() + } else { + rel_dir + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/") + }; + + let content = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => continue, + }; + let index_data: Map = match serde_json::from_str(&content) { + Ok(Value::Object(m)) => m, + _ => continue, + }; + + for (key_name, entry_val) in index_data { + let full_key = if dir_prefix.is_empty() { + key_name.clone() + } else { + format!("{}/{}", dir_prefix, key_name) + }; + out.insert( + full_key, + IndexEntryInfo { + entry: entry_val, + index_file: path.clone(), + key_name, + }, + ); + } + } + } + out +} + +fn stored_etag(entry: &Value) -> Option { + entry + .get("metadata") + .and_then(|m| m.get("__etag__")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + +fn check_corrupted( + state: &mut ScanState, + bucket: &str, + bucket_path: &Path, + entries: &HashMap, + batch_size: usize, +) { + let mut keys: Vec<&String> = entries.keys().collect(); + keys.sort(); + + for full_key in keys { + if state.batch_exhausted(batch_size) { + return; + } + let info = &entries[full_key]; + let object_path = bucket_path.join(full_key); + if !object_path.exists() { + continue; + } + state.objects_scanned += 1; + + let Some(stored) = stored_etag(&info.entry) else { + continue; + }; + + match myfsio_crypto::hashing::md5_file(&object_path) { + Ok(actual) => { + if actual != stored { + state.corrupted_objects += 1; + state.push_issue( + "corrupted_object", + bucket, + full_key, + format!("stored_etag={} actual_etag={}", stored, actual), + ); + } + } + Err(e) => state + .errors + .push(format!("hash {}/{}: {}", bucket, full_key, e)), + } + } +} + +fn check_phantom( + state: &mut ScanState, + bucket: &str, + bucket_path: &Path, + entries: &HashMap, + batch_size: usize, +) { + let mut keys: Vec<&String> = entries.keys().collect(); + keys.sort(); + + for full_key in keys { + if state.batch_exhausted(batch_size) { + return; + } + state.objects_scanned += 1; + let object_path = bucket_path.join(full_key); + if !object_path.exists() { + state.phantom_metadata += 1; + state.push_issue( + "phantom_metadata", + bucket, + full_key, + "metadata entry without file on disk".to_string(), + ); + } + } +} + +fn check_orphaned( + state: &mut ScanState, + bucket: &str, + bucket_path: &Path, + entries: &HashMap, + batch_size: usize, +) { + let indexed: HashSet<&String> = entries.keys().collect(); + let mut stack: Vec<(PathBuf, String)> = vec![(bucket_path.to_path_buf(), String::new())]; + + while let Some((dir, prefix)) = stack.pop() { + if state.batch_exhausted(batch_size) { + return; + } + let rd = match std::fs::read_dir(&dir) { + Ok(r) => r, + Err(_) => continue, + }; + for entry in rd.flatten() { + if state.batch_exhausted(batch_size) { + return; + } + let name = entry.file_name().to_string_lossy().to_string(); + let ft = match entry.file_type() { + Ok(t) => t, + Err(_) => continue, + }; + if ft.is_dir() { + if prefix.is_empty() && INTERNAL_FOLDERS.contains(&name.as_str()) { + continue; + } + let new_prefix = if prefix.is_empty() { + name + } else { + format!("{}/{}", prefix, name) + }; + stack.push((entry.path(), new_prefix)); + } else if ft.is_file() { + let full_key = if prefix.is_empty() { + name + } else { + format!("{}/{}", prefix, name) + }; + state.objects_scanned += 1; + if !indexed.contains(&full_key) { + state.orphaned_objects += 1; + state.push_issue( + "orphaned_object", + bucket, + &full_key, + "file exists without metadata entry".to_string(), + ); + } + } + } + } +} + +fn check_stale_versions( + state: &mut ScanState, + storage_root: &Path, + bucket: &str, + batch_size: usize, +) { + let versions_root = storage_root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join(BUCKET_VERSIONS_DIR); + if !versions_root.exists() { + return; + } + + let mut stack: Vec = vec![versions_root.clone()]; + while let Some(dir) = stack.pop() { + if state.batch_exhausted(batch_size) { + return; + } + let rd = match std::fs::read_dir(&dir) { + Ok(r) => r, + Err(_) => continue, + }; + + let mut bin_stems: HashMap = HashMap::new(); + let mut json_stems: HashMap = HashMap::new(); + let mut subdirs: Vec = Vec::new(); + + for entry in rd.flatten() { + let ft = match entry.file_type() { + Ok(t) => t, + Err(_) => continue, + }; + let path = entry.path(); + if ft.is_dir() { + subdirs.push(path); + continue; + } + let name = entry.file_name().to_string_lossy().to_string(); + if let Some(stem) = name.strip_suffix(".bin") { + bin_stems.insert(stem.to_string(), path); + } else if let Some(stem) = name.strip_suffix(".json") { + json_stems.insert(stem.to_string(), path); + } + } + + for (stem, path) in &bin_stems { + if state.batch_exhausted(batch_size) { + return; + } + state.objects_scanned += 1; + if !json_stems.contains_key(stem) { + state.stale_versions += 1; + let key = path + .strip_prefix(&versions_root) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| path.display().to_string()); + state.push_issue( + "stale_version", + bucket, + &key, + "version data without manifest".to_string(), + ); + } + } + + for (stem, path) in &json_stems { + if state.batch_exhausted(batch_size) { + return; + } + state.objects_scanned += 1; + if !bin_stems.contains_key(stem) { + state.stale_versions += 1; + let key = path + .strip_prefix(&versions_root) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| path.display().to_string()); + state.push_issue( + "stale_version", + bucket, + &key, + "version manifest without data".to_string(), + ); + } + } + + stack.extend(subdirs); + } +} + +fn check_etag_cache( + state: &mut ScanState, + storage_root: &Path, + bucket: &str, + entries: &HashMap, + batch_size: usize, +) { + let etag_index_path = storage_root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join("etag_index.json"); + if !etag_index_path.exists() { + return; + } + + let cache: HashMap = match std::fs::read_to_string(&etag_index_path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + { + Some(Value::Object(m)) => m.into_iter().collect(), + _ => return, + }; + + for (full_key, cached_val) in cache { + if state.batch_exhausted(batch_size) { + return; + } + state.objects_scanned += 1; + let Some(cached_etag) = cached_val.as_str() else { + continue; + }; + let Some(info) = entries.get(&full_key) else { + continue; + }; + let Some(stored) = stored_etag(&info.entry) else { + continue; + }; + if cached_etag != stored { + state.etag_cache_inconsistencies += 1; + state.push_issue( + "etag_cache_inconsistency", + bucket, + &full_key, + format!("cached_etag={} index_etag={}", cached_etag, stored), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn md5_hex(bytes: &[u8]) -> String { + myfsio_crypto::hashing::md5_bytes(bytes) + } + + fn write_index(meta_dir: &Path, entries: &[(&str, &str)]) { + fs::create_dir_all(meta_dir).unwrap(); + let mut map = Map::new(); + for (name, etag) in entries { + map.insert( + name.to_string(), + json!({ "metadata": { "__etag__": etag } }), + ); + } + fs::write( + meta_dir.join(INDEX_FILE), + serde_json::to_string(&Value::Object(map)).unwrap(), + ) + .unwrap(); + } + + #[test] + fn scan_detects_each_issue_type() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let bucket = "testbucket"; + let bucket_path = root.join(bucket); + let meta_root = root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join(BUCKET_META_DIR); + fs::create_dir_all(&bucket_path).unwrap(); + + let clean_bytes = b"clean file contents"; + let clean_etag = md5_hex(clean_bytes); + fs::write(bucket_path.join("clean.txt"), clean_bytes).unwrap(); + + let corrupted_bytes = b"actual content"; + fs::write(bucket_path.join("corrupted.txt"), corrupted_bytes).unwrap(); + + fs::write(bucket_path.join("orphan.txt"), b"no metadata").unwrap(); + + write_index( + &meta_root, + &[ + ("clean.txt", &clean_etag), + ("corrupted.txt", "00000000000000000000000000000000"), + ("phantom.txt", "deadbeefdeadbeefdeadbeefdeadbeef"), + ], + ); + + let versions_root = root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join(BUCKET_VERSIONS_DIR) + .join("someobject"); + fs::create_dir_all(&versions_root).unwrap(); + fs::write(versions_root.join("v1.bin"), b"orphan bin").unwrap(); + fs::write(versions_root.join("v2.json"), b"{}").unwrap(); + + let etag_index = root + .join(SYSTEM_ROOT) + .join(SYSTEM_BUCKETS_DIR) + .join(bucket) + .join("etag_index.json"); + fs::write( + &etag_index, + serde_json::to_string(&json!({ "clean.txt": "stale-cached-etag" })).unwrap(), + ) + .unwrap(); + + let state = scan_all_buckets(root, 10_000); + + assert_eq!(state.corrupted_objects, 1, "corrupted"); + assert_eq!(state.phantom_metadata, 1, "phantom"); + assert_eq!(state.orphaned_objects, 1, "orphaned"); + assert_eq!(state.stale_versions, 2, "stale versions"); + assert_eq!(state.etag_cache_inconsistencies, 1, "etag cache"); + assert_eq!(state.buckets_scanned, 1); + assert!( + state.errors.is_empty(), + "unexpected errors: {:?}", + state.errors + ); + } + + #[test] + fn skips_system_root_as_bucket() { + let tmp = tempfile::tempdir().unwrap(); + fs::create_dir_all(tmp.path().join(SYSTEM_ROOT).join("config")).unwrap(); + let state = scan_all_buckets(tmp.path(), 100); + assert_eq!(state.buckets_scanned, 0); + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/lifecycle.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/lifecycle.rs index b68706a..071e6ab 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/lifecycle.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/lifecycle.rs @@ -66,7 +66,10 @@ impl LifecycleService { None => continue, }; - let rules = match lifecycle.as_str().and_then(|s| serde_json::from_str::(s).ok()) { + let rules = match lifecycle + .as_str() + .and_then(|s| serde_json::from_str::(s).ok()) + { Some(v) => v, None => continue, }; @@ -93,7 +96,11 @@ impl LifecycleService { let cutoff = chrono::Utc::now() - chrono::Duration::days(days as i64); let params = myfsio_common::types::ListParams { max_keys: 1000, - prefix: if prefix.is_empty() { None } else { Some(prefix.to_string()) }, + prefix: if prefix.is_empty() { + None + } else { + Some(prefix.to_string()) + }, ..Default::default() }; if let Ok(result) = self.storage.list_objects(&bucket.name, ¶ms).await { @@ -101,7 +108,8 @@ impl LifecycleService { if obj.last_modified < cutoff { match self.storage.delete_object(&bucket.name, &obj.key).await { Ok(()) => total_expired += 1, - Err(e) => errors.push(format!("{}:{}: {}", bucket.name, obj.key, e)), + Err(e) => errors + .push(format!("{}:{}: {}", bucket.name, obj.key, e)), } } } @@ -112,12 +120,18 @@ impl LifecycleService { if let Some(abort) = rule.get("AbortIncompleteMultipartUpload") { if let Some(days) = abort.get("DaysAfterInitiation").and_then(|d| d.as_u64()) { let cutoff = chrono::Utc::now() - chrono::Duration::days(days as i64); - if let Ok(uploads) = self.storage.list_multipart_uploads(&bucket.name).await { + if let Ok(uploads) = self.storage.list_multipart_uploads(&bucket.name).await + { for upload in &uploads { if upload.initiated < cutoff { - match self.storage.abort_multipart(&bucket.name, &upload.upload_id).await { + match self + .storage + .abort_multipart(&bucket.name, &upload.upload_id) + .await + { Ok(()) => total_multipart_aborted += 1, - Err(e) => errors.push(format!("abort {}: {}", upload.upload_id, e)), + Err(e) => errors + .push(format!("abort {}: {}", upload.upload_id, e)), } } } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/metrics.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/metrics.rs index 84e08e2..59314bc 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/metrics.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/metrics.rs @@ -165,8 +165,9 @@ impl MetricsService { .ok() .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| { - v.get("snapshots") - .and_then(|s| serde_json::from_value::>(s.clone()).ok()) + v.get("snapshots").and_then(|s| { + serde_json::from_value::>(s.clone()).ok() + }) }) .unwrap_or_default() } else { @@ -218,7 +219,9 @@ impl MetricsService { if let Some(code) = error_code { *inner.error_codes.entry(code.to_string()).or_insert(0) += 1; } - inner.totals.record(latency_ms, success, bytes_in, bytes_out); + inner + .totals + .record(latency_ms, success, bytes_in, bytes_out); } pub fn get_current_stats(&self) -> Value { diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/mod.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/mod.rs index 9612243..9da9f01 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/mod.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/mod.rs @@ -1,9 +1,11 @@ +pub mod access_logging; pub mod gc; -pub mod lifecycle; pub mod integrity; +pub mod lifecycle; pub mod metrics; pub mod replication; pub mod s3_client; pub mod site_registry; pub mod site_sync; +pub mod system_metrics; pub mod website_domains; diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/replication.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/replication.rs index 9a1e7ca..922174f 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/replication.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/replication.rs @@ -8,6 +8,7 @@ use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use tokio::sync::Semaphore; +use myfsio_common::types::ListParams; use myfsio_storage::fs_backend::FsStorageBackend; use myfsio_storage::traits::StorageEngine; @@ -124,7 +125,10 @@ impl ReplicationFailureStore { } let trimmed = &failures[..failures.len().min(self.max_failures_per_bucket)]; let data = serde_json::json!({ "failures": trimmed }); - let _ = std::fs::write(&path, serde_json::to_string_pretty(&data).unwrap_or_default()); + let _ = std::fs::write( + &path, + serde_json::to_string_pretty(&data).unwrap_or_default(), + ); } pub fn load(&self, bucket: &str) -> Vec { @@ -148,7 +152,10 @@ impl ReplicationFailureStore { pub fn add(&self, bucket: &str, failure: ReplicationFailure) { let mut failures = self.load(bucket); - if let Some(existing) = failures.iter_mut().find(|f| f.object_key == failure.object_key) { + if let Some(existing) = failures + .iter_mut() + .find(|f| f.object_key == failure.object_key) + { existing.failure_count += 1; existing.timestamp = failure.timestamp; existing.error_message = failure.error_message.clone(); @@ -318,7 +325,101 @@ impl ReplicationManager { let manager = self.clone(); tokio::spawn(async move { let _permit = permit; - manager.replicate_task(&bucket, &key, &rule, &connection, &action).await; + manager + .replicate_task(&bucket, &key, &rule, &connection, &action) + .await; + }); + } + + pub async fn replicate_existing_objects(self: Arc, bucket: String) -> usize { + let rule = match self.get_rule(&bucket) { + Some(r) if r.enabled => r, + _ => return 0, + }; + let connection = match self.connections.get(&rule.target_connection_id) { + Some(c) => c, + None => { + tracing::warn!( + "Cannot replicate existing objects for {}: connection {} not found", + bucket, + rule.target_connection_id + ); + return 0; + } + }; + if !self.check_endpoint(&connection).await { + tracing::warn!( + "Cannot replicate existing objects for {}: endpoint {} is unreachable", + bucket, + connection.endpoint_url + ); + return 0; + } + + let mut continuation_token: Option = None; + let mut submitted = 0usize; + + loop { + let page = match self + .storage + .list_objects( + &bucket, + &ListParams { + max_keys: 1000, + continuation_token: continuation_token.clone(), + prefix: rule.filter_prefix.clone(), + start_after: None, + }, + ) + .await + { + Ok(page) => page, + Err(err) => { + tracing::error!( + "Failed to list existing objects for replication in {}: {}", + bucket, + err + ); + break; + } + }; + + let next_token = page.next_continuation_token.clone(); + let is_truncated = page.is_truncated; + + for object in page.objects { + submitted += 1; + self.clone() + .trigger(bucket.clone(), object.key, "write".to_string()) + .await; + } + + if !is_truncated { + break; + } + + continuation_token = next_token; + if continuation_token.is_none() { + break; + } + } + + submitted + } + + pub fn schedule_existing_objects_sync(self: Arc, bucket: String) { + tokio::spawn(async move { + let submitted = self + .clone() + .replicate_existing_objects(bucket.clone()) + .await; + if submitted > 0 { + tracing::info!( + "Scheduled {} existing object(s) for replication in {}", + submitted, + bucket + ); + } }); } @@ -330,7 +431,8 @@ impl ReplicationManager { conn: &RemoteConnection, action: &str, ) { - if object_key.contains("..") || object_key.starts_with('/') || object_key.starts_with('\\') { + if object_key.contains("..") || object_key.starts_with('/') || object_key.starts_with('\\') + { tracing::error!("Invalid object key (path traversal): {}", object_key); return; } @@ -358,7 +460,12 @@ impl ReplicationManager { } Err(err) => { let msg = format!("{:?}", err); - tracing::error!("Replication DELETE failed {}/{}: {}", bucket, object_key, msg); + tracing::error!( + "Replication DELETE failed {}/{}: {}", + bucket, + object_key, + msg + ); self.failures.add( bucket, ReplicationFailure { @@ -414,16 +521,18 @@ impl ReplicationManager { .send() .await { - Ok(_) | Err(_) => upload_object( - &client, - &rule.target_bucket, - object_key, - &src_path, - file_size, - self.streaming_threshold_bytes, - content_type.as_deref(), - ) - .await, + Ok(_) | Err(_) => { + upload_object( + &client, + &rule.target_bucket, + object_key, + &src_path, + file_size, + self.streaming_threshold_bytes, + content_type.as_deref(), + ) + .await + } } } other => other, @@ -577,9 +686,9 @@ async fn upload_object( ))) })? } else { - let bytes = tokio::fs::read(path).await.map_err(|e| { - aws_sdk_s3::error::SdkError::construction_failure(Box::new(e)) - })?; + let bytes = tokio::fs::read(path) + .await + .map_err(|e| aws_sdk_s3::error::SdkError::construction_failure(Box::new(e)))?; ByteStream::from(bytes) }; diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/s3_client.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/s3_client.rs index 431e149..7f085eb 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/s3_client.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/s3_client.rs @@ -37,8 +37,8 @@ pub fn build_client(connection: &RemoteConnection, options: &ClientOptions) -> C .read_timeout(options.read_timeout) .build(); - let retry_config = aws_smithy_types::retry::RetryConfig::standard() - .with_max_attempts(options.max_attempts); + let retry_config = + aws_smithy_types::retry::RetryConfig::standard().with_max_attempts(options.max_attempts); let config = aws_sdk_s3::config::Builder::new() .behavior_version(BehaviorVersion::latest()) diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/site_registry.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/site_registry.rs index f7a2b48..00ab5c5 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/site_registry.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/site_registry.rs @@ -102,7 +102,12 @@ impl SiteRegistry { } pub fn get_peer(&self, site_id: &str) -> Option { - self.data.read().peers.iter().find(|p| p.site_id == site_id).cloned() + self.data + .read() + .peers + .iter() + .find(|p| p.site_id == site_id) + .cloned() } pub fn add_peer(&self, peer: PeerSite) { diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/site_sync.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/site_sync.rs index a067b92..a1e6a67 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/site_sync.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/site_sync.rs @@ -102,7 +102,10 @@ impl SiteSyncWorker { } pub async fn run(self: Arc) { - tracing::info!("Site sync worker started (interval={}s)", self.interval.as_secs()); + tracing::info!( + "Site sync worker started (interval={}s)", + self.interval.as_secs() + ); loop { tokio::select! { _ = tokio::time::sleep(self.interval) => {} @@ -309,11 +312,10 @@ impl SiteSyncWorker { let resp = match req.send().await { Ok(r) => r, Err(err) => { - let msg = format!("{:?}", err); - if msg.contains("NoSuchBucket") { + if is_not_found_error(&err) { return Ok(result); } - return Err(msg); + return Err(format!("{:?}", err)); } }; for obj in resp.contents() { @@ -409,11 +411,9 @@ impl SiteSyncWorker { } }; - let metadata: Option> = head.metadata().map(|m| { - m.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - }); + let metadata: Option> = head + .metadata() + .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); let stream = resp.body.into_async_read(); let boxed: Pin> = Box::pin(stream); @@ -428,7 +428,12 @@ impl SiteSyncWorker { true } Err(err) => { - tracing::error!("Store pulled object failed {}/{}: {}", local_bucket, key, err); + tracing::error!( + "Store pulled object failed {}/{}: {}", + local_bucket, + key, + err + ); false } } @@ -483,3 +488,11 @@ fn now_secs() -> f64 { .map(|d| d.as_secs_f64()) .unwrap_or(0.0) } + +fn is_not_found_error(err: &aws_sdk_s3::error::SdkError) -> bool { + let msg = format!("{:?}", err); + msg.contains("NoSuchBucket") + || msg.contains("code: Some(\"NotFound\")") + || msg.contains("code: Some(\"NoSuchBucket\")") + || msg.contains("status: 404") +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/system_metrics.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/system_metrics.rs new file mode 100644 index 0000000..6c0849d --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/system_metrics.rs @@ -0,0 +1,203 @@ +use chrono::{DateTime, Utc}; +use myfsio_storage::fs_backend::FsStorageBackend; +use myfsio_storage::traits::StorageEngine; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use sysinfo::{Disks, System}; +use tokio::sync::RwLock; + +#[derive(Debug, Clone)] +pub struct SystemMetricsConfig { + pub interval_minutes: u64, + pub retention_hours: u64, +} + +impl Default for SystemMetricsConfig { + fn default() -> Self { + Self { + interval_minutes: 5, + retention_hours: 24, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemMetricsSnapshot { + pub timestamp: DateTime, + pub cpu_percent: f64, + pub memory_percent: f64, + pub disk_percent: f64, + pub storage_bytes: u64, +} + +pub struct SystemMetricsService { + storage_root: PathBuf, + storage: Arc, + config: SystemMetricsConfig, + history: Arc>>, + history_path: PathBuf, +} + +impl SystemMetricsService { + pub fn new( + storage_root: &Path, + storage: Arc, + config: SystemMetricsConfig, + ) -> Self { + let history_path = storage_root + .join(".myfsio.sys") + .join("config") + .join("metrics_history.json"); + + let mut history = if history_path.exists() { + std::fs::read_to_string(&history_path) + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .and_then(|v| { + v.get("history").and_then(|h| { + serde_json::from_value::>(h.clone()).ok() + }) + }) + .unwrap_or_default() + } else { + Vec::new() + }; + prune_history(&mut history, config.retention_hours); + + Self { + storage_root: storage_root.to_path_buf(), + storage, + config, + history: Arc::new(RwLock::new(history)), + history_path, + } + } + + pub async fn get_history(&self, hours: Option) -> Vec { + let mut history = self.history.read().await.clone(); + prune_history(&mut history, hours.unwrap_or(self.config.retention_hours)); + history + } + + async fn take_snapshot(&self) { + let snapshot = collect_snapshot(&self.storage_root, &self.storage).await; + let mut history = self.history.write().await; + history.push(snapshot); + prune_history(&mut history, self.config.retention_hours); + drop(history); + self.save_history().await; + } + + async fn save_history(&self) { + let history = self.history.read().await; + let data = json!({ "history": *history }); + if let Some(parent) = self.history_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::fs::write( + &self.history_path, + serde_json::to_string_pretty(&data).unwrap_or_default(), + ); + } + + pub fn start_background(self: Arc) -> tokio::task::JoinHandle<()> { + let interval = + std::time::Duration::from_secs(self.config.interval_minutes.saturating_mul(60)); + tokio::spawn(async move { + self.take_snapshot().await; + let mut timer = tokio::time::interval(interval); + loop { + timer.tick().await; + self.take_snapshot().await; + } + }) + } +} + +fn prune_history(history: &mut Vec, retention_hours: u64) { + let cutoff = Utc::now() - chrono::Duration::hours(retention_hours as i64); + history.retain(|item| item.timestamp > cutoff); +} + +fn sample_system_now() -> (f64, f64) { + let mut system = System::new(); + system.refresh_cpu_usage(); + std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); + system.refresh_cpu_usage(); + system.refresh_memory(); + + let cpu_percent = system.global_cpu_usage() as f64; + let memory_percent = if system.total_memory() > 0 { + (system.used_memory() as f64 / system.total_memory() as f64) * 100.0 + } else { + 0.0 + }; + (cpu_percent, memory_percent) +} + +fn normalize_path_for_mount(path: &Path) -> String { + let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + let raw = canonical.to_string_lossy().to_string(); + let stripped = raw.strip_prefix(r"\\?\").unwrap_or(&raw); + stripped.to_lowercase() +} + +fn sample_disk(path: &Path) -> (u64, u64) { + let disks = Disks::new_with_refreshed_list(); + let path_str = normalize_path_for_mount(path); + let mut best: Option<(usize, u64, u64)> = None; + + for disk in disks.list() { + let mount_raw = disk.mount_point().to_string_lossy().to_string(); + let mount = mount_raw + .strip_prefix(r"\\?\") + .unwrap_or(&mount_raw) + .to_lowercase(); + let total = disk.total_space(); + let free = disk.available_space(); + if path_str.starts_with(&mount) { + let len = mount.len(); + match best { + Some((best_len, _, _)) if len <= best_len => {} + _ => best = Some((len, total, free)), + } + } + } + + best.map(|(_, total, free)| (total, free)).unwrap_or((0, 0)) +} + +async fn collect_snapshot( + storage_root: &Path, + storage: &Arc, +) -> SystemMetricsSnapshot { + let (cpu_percent, memory_percent) = sample_system_now(); + let (disk_total, disk_free) = sample_disk(storage_root); + let disk_percent = if disk_total > 0 { + ((disk_total - disk_free) as f64 / disk_total as f64) * 100.0 + } else { + 0.0 + }; + + let mut storage_bytes = 0u64; + let buckets = storage.list_buckets().await.unwrap_or_default(); + for bucket in buckets { + if let Ok(stats) = storage.bucket_stats(&bucket.name).await { + storage_bytes += stats.total_bytes(); + } + } + + SystemMetricsSnapshot { + timestamp: Utc::now(), + cpu_percent: round2(cpu_percent), + memory_percent: round2(memory_percent), + disk_percent: round2(disk_percent), + storage_bytes, + } +} + +fn round2(value: f64) -> f64 { + (value * 100.0).round() / 100.0 +} diff --git a/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs b/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs index 7ce27cf..5060d52 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/services/website_domains.rs @@ -64,7 +64,10 @@ impl WebsiteDomainStore { } pub fn set_mapping(&self, domain: &str, bucket: &str) { - self.data.write().mappings.insert(domain.to_string(), bucket.to_string()); + self.data + .write() + .mappings + .insert(domain.to_string(), bucket.to_string()); self.save(); } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/session.rs b/rust/myfsio-engine/crates/myfsio-server/src/session.rs index d35aedd..dbed866 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/session.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/session.rs @@ -27,21 +27,18 @@ pub struct SessionData { pub csrf_token: String, pub flash: Vec, pub extra: HashMap, - created_at: Instant, last_accessed: Instant, } impl SessionData { pub fn new() -> Self { - let now = Instant::now(); Self { user_id: None, display_name: None, csrf_token: generate_token(CSRF_TOKEN_BYTES), flash: Vec::new(), extra: HashMap::new(), - created_at: now, - last_accessed: now, + last_accessed: Instant::now(), } } diff --git a/rust/myfsio-engine/crates/myfsio-server/src/state.rs b/rust/myfsio-engine/crates/myfsio-server/src/state.rs index 3aeeaa1..34c25ab 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/state.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/state.rs @@ -2,16 +2,18 @@ use std::sync::Arc; use std::time::Duration; use crate::config::ServerConfig; -use crate::session::SessionStore; -use crate::templates::TemplateEngine; +use crate::services::access_logging::AccessLoggingService; use crate::services::gc::GcService; use crate::services::integrity::IntegrityService; use crate::services::metrics::MetricsService; use crate::services::replication::ReplicationManager; use crate::services::site_registry::SiteRegistry; use crate::services::site_sync::SiteSyncWorker; +use crate::services::system_metrics::SystemMetricsService; use crate::services::website_domains::WebsiteDomainStore; +use crate::session::SessionStore; use crate::stores::connections::ConnectionStore; +use crate::templates::TemplateEngine; use myfsio_auth::iam::IamService; use myfsio_crypto::encryption::EncryptionService; use myfsio_crypto::kms::KmsService; @@ -27,6 +29,7 @@ pub struct AppState { pub gc: Option>, pub integrity: Option>, pub metrics: Option>, + pub system_metrics: Option>, pub site_registry: Option>, pub website_domains: Option>, pub connections: Arc, @@ -34,6 +37,7 @@ pub struct AppState { pub site_sync: Option>, pub templates: Option>, pub sessions: Arc, + pub access_logging: Arc, } impl AppState { @@ -66,7 +70,23 @@ impl AppState { let metrics = if config.metrics_enabled { Some(Arc::new(MetricsService::new( &config.storage_root, - crate::services::metrics::MetricsConfig::default(), + crate::services::metrics::MetricsConfig { + interval_minutes: config.metrics_interval_minutes, + retention_hours: config.metrics_retention_hours, + }, + ))) + } else { + None + }; + + let system_metrics = if config.metrics_history_enabled { + Some(Arc::new(SystemMetricsService::new( + &config.storage_root, + storage.clone(), + crate::services::system_metrics::SystemMetricsConfig { + interval_minutes: config.metrics_history_interval_minutes, + retention_hours: config.metrics_history_retention_hours, + }, ))) } else { None @@ -111,6 +131,7 @@ impl AppState { }; let templates = init_templates(&config.templates_dir); + let access_logging = Arc::new(AccessLoggingService::new(&config.storage_root)); Self { config, storage, @@ -120,6 +141,7 @@ impl AppState { gc, integrity, metrics, + system_metrics, site_registry, website_domains, connections, @@ -127,6 +149,7 @@ impl AppState { site_sync, templates, sessions: Arc::new(SessionStore::new(Duration::from_secs(60 * 60 * 12))), + access_logging, } } @@ -149,9 +172,7 @@ impl AppState { let encryption = if config.encryption_enabled { match myfsio_crypto::kms::load_or_create_master_key(&keys_dir).await { - Ok(master_key) => { - Some(Arc::new(EncryptionService::new(master_key, kms.clone()))) - } + Ok(master_key) => Some(Arc::new(EncryptionService::new(master_key, kms.clone()))), Err(e) => { tracing::error!("Failed to initialize encryption: {}", e); None diff --git a/rust/myfsio-engine/crates/myfsio-server/src/templates.rs b/rust/myfsio-engine/crates/myfsio-server/src/templates.rs index 46491f7..153fce3 100644 --- a/rust/myfsio-engine/crates/myfsio-server/src/templates.rs +++ b/rust/myfsio-engine/crates/myfsio-server/src/templates.rs @@ -6,7 +6,8 @@ use parking_lot::RwLock; use serde_json::Value; use tera::{Context, Error as TeraError, Tera}; -pub type EndpointResolver = Arc) -> Option + Send + Sync>; +pub type EndpointResolver = + Arc) -> Option + Send + Sync>; #[derive(Clone)] pub struct TemplateEngine { @@ -17,10 +18,10 @@ pub struct TemplateEngine { impl TemplateEngine { pub fn new(template_glob: &str) -> Result { let mut tera = Tera::new(template_glob)?; + tera.set_escape_fn(html_escape); register_filters(&mut tera); - let endpoints: Arc>> = - Arc::new(RwLock::new(HashMap::new())); + let endpoints: Arc>> = Arc::new(RwLock::new(HashMap::new())); register_functions(&mut tera, endpoints.clone()); @@ -52,9 +53,25 @@ impl TemplateEngine { } } +fn html_escape(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + for c in input.chars() { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + _ => out.push(c), + } + } + out +} + fn register_filters(tera: &mut Tera) { tera.register_filter("format_datetime", format_datetime_filter); tera.register_filter("filesizeformat", filesizeformat_filter); + tera.register_filter("slice", slice_filter); } fn register_functions(tera: &mut Tera, endpoints: Arc>>) { @@ -67,10 +84,7 @@ fn register_functions(tera: &mut Tera, endpoints: Arc) -> tera: Value::String(s) => DateTime::parse_from_rfc3339(s) .ok() .map(|d| d.with_timezone(&Utc)) - .or_else(|| DateTime::parse_from_rfc2822(s).ok().map(|d| d.with_timezone(&Utc))), + .or_else(|| { + DateTime::parse_from_rfc2822(s) + .ok() + .map(|d| d.with_timezone(&Utc)) + }), Value::Number(n) => n.as_f64().and_then(|f| { let secs = f as i64; let nanos = ((f - secs as f64) * 1_000_000_000.0) as u32; @@ -170,6 +188,51 @@ fn format_datetime_filter(value: &Value, args: &HashMap) -> tera: } } +fn slice_filter(value: &Value, args: &HashMap) -> tera::Result { + let start = args.get("start").and_then(|v| v.as_i64()).unwrap_or(0); + let end = args.get("end").and_then(|v| v.as_i64()); + + match value { + Value::String(s) => { + let chars: Vec = s.chars().collect(); + let len = chars.len() as i64; + let norm = |i: i64| -> usize { + if i < 0 { + (len + i).max(0) as usize + } else { + i.min(len) as usize + } + }; + let s_idx = norm(start); + let e_idx = match end { + Some(e) => norm(e), + None => len as usize, + }; + let e_idx = e_idx.max(s_idx); + Ok(Value::String(chars[s_idx..e_idx].iter().collect())) + } + Value::Array(arr) => { + let len = arr.len() as i64; + let norm = |i: i64| -> usize { + if i < 0 { + (len + i).max(0) as usize + } else { + i.min(len) as usize + } + }; + let s_idx = norm(start); + let e_idx = match end { + Some(e) => norm(e), + None => len as usize, + }; + let e_idx = e_idx.max(s_idx); + Ok(Value::Array(arr[s_idx..e_idx].to_vec())) + } + Value::Null => Ok(Value::String(String::new())), + _ => Err(tera::Error::msg("slice: unsupported value type")), + } +} + fn filesizeformat_filter(value: &Value, _args: &HashMap) -> tera::Result { let bytes = match value { Value::Number(n) => n.as_f64().unwrap_or(0.0), @@ -205,7 +268,10 @@ mod tests { engine.register_endpoints(&[ ("ui.buckets_overview", "/ui/buckets"), ("ui.bucket_detail", "/ui/buckets/{bucket_name}"), - ("ui.abort_multipart_upload", "/ui/buckets/{bucket_name}/multipart/{upload_id}/abort"), + ( + "ui.abort_multipart_upload", + "/ui/buckets/{bucket_name}/multipart/{upload_id}/abort", + ), ]); engine } @@ -220,7 +286,10 @@ mod tests { #[test] fn static_url() { let e = test_engine(); - let out = render_inline(&e, "{{ url_for(endpoint='static', filename='css/main.css') }}"); + let out = render_inline( + &e, + "{{ url_for(endpoint='static', filename='css/main.css') }}", + ); assert_eq!(out, "/static/css/main.css"); } @@ -267,7 +336,11 @@ mod tests { .get_template_names() .map(|s| s.to_string()) .collect(); - assert!(names.len() >= 10, "expected 10+ templates, got {}", names.len()); + assert!( + names.len() >= 10, + "expected 10+ templates, got {}", + names.len() + ); } #[test] diff --git a/rust/myfsio-engine/crates/myfsio-server/static/css/main.css b/rust/myfsio-engine/crates/myfsio-server/static/css/main.css new file mode 100644 index 0000000..e48174d --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/css/main.css @@ -0,0 +1,3157 @@ +:root { + --myfsio-body-bg: #f5f6fa; + --myfsio-text: #0f172a; + --myfsio-card-bg: #ffffff; + --myfsio-card-border: #e2e8f0; + --myfsio-muted: #475569; + --myfsio-input-bg: #ffffff; + --myfsio-input-border: #cbd5f5; + --myfsio-nav-gradient: linear-gradient(90deg, #0f172a, #1d4ed8); + --myfsio-nav-link: rgba(255, 255, 255, 0.85); + --myfsio-nav-link-hover: #ffffff; + --myfsio-preview-bg: #f8f9fb; + --myfsio-policy-bg: #0f172a; + --myfsio-policy-fg: #e2e8f0; + --myfsio-hover-bg: rgba(59, 130, 246, 0.12); + --myfsio-accent: #3b82f6; + --myfsio-accent-hover: #2563eb; + --myfsio-tag-key-bg: #e0e7ff; + --myfsio-tag-key-text: #3730a3; + --myfsio-tag-value-bg: #f0f1fa; + --myfsio-tag-value-text: #4338ca; + --myfsio-tag-border: #c7d2fe; + --myfsio-tag-delete-hover: #ef4444; +} + +[data-theme='dark'] { + --myfsio-body-bg: #0b1120; + --myfsio-text: #e2e8f0; + --myfsio-card-bg: #1a1f2e; + --myfsio-card-border: #2d3548; + --myfsio-muted: #94a3b8; + --myfsio-input-bg: #111827; + --myfsio-input-border: #374151; + --myfsio-nav-gradient: linear-gradient(90deg, #020617, #1e3a8a); + --myfsio-nav-link: rgba(248, 250, 252, 0.85); + --myfsio-nav-link-hover: #ffffff; + --myfsio-preview-bg: #1f2937; + --myfsio-policy-bg: #0f1419; + --myfsio-policy-fg: #f8fafc; + --myfsio-hover-bg: rgba(59, 130, 246, 0.2); + --myfsio-accent: #60a5fa; + --myfsio-accent-hover: #3b82f6; + --myfsio-tag-key-bg: #312e81; + --myfsio-tag-key-text: #c7d2fe; + --myfsio-tag-value-bg: #1e1b4b; + --myfsio-tag-value-text: #a5b4fc; + --myfsio-tag-border: #4338ca; + --myfsio-tag-delete-hover: #f87171; +} + +[data-theme='dark'] body, +[data-theme='dark'] html { + color-scheme: dark; +} + +body { + background-color: var(--myfsio-body-bg); + color: var(--myfsio-text); + transition: background-color 0.3s ease, color 0.3s ease; +} + +html, body { + min-height: 100%; +} + +main { + color: var(--myfsio-text); + background-color: var(--myfsio-body-bg); +} + +html { + background-color: var(--myfsio-body-bg); + scroll-behavior: smooth; +} + +.text-muted, +.form-text { + color: var(--myfsio-muted) !important; +} + +.table-responsive { + border-radius: 0.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +.message-stack { position: sticky; top: 1rem; z-index: 100; } + +.table-responsive table { + min-width: 600px; +} + +.table-responsive table th, +.table-responsive table td { + white-space: nowrap; +} + +.table-responsive table td.text-wrap { + white-space: normal; + min-width: 200px; +} +code { font-size: 0.85rem; } + +code { + background-color: rgba(15, 23, 42, 0.08); + color: var(--myfsio-text); + padding: 0.15rem 0.4rem; + border-radius: 0.25rem; +} + +[data-theme='dark'] code { + background-color: rgba(148, 163, 184, 0.15); + color: #93c5fd; +} + +.card, +.card-header, +.modal-content, +.dropdown-menu, +.list-group-item { + background-color: var(--myfsio-card-bg); + color: var(--myfsio-text); + border-color: var(--myfsio-card-border); +} + +.bg-panel { + background-color: var(--myfsio-preview-bg); + color: var(--myfsio-text); + border-color: var(--myfsio-card-border) !important; +} + +.border-dashed { + border-style: dashed !important; +} + +.card { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +[data-theme='dark'] .card { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); +} + +.card-header { + font-weight: 500; +} + +.drop-zone { + position: relative; + transition: all 0.2s ease; +} + +.drop-zone.drag-over { + background-color: var(--myfsio-hover-bg); + border: 2px dashed var(--myfsio-input-border); +} + +.drop-zone.drag-over::after { + content: 'Drop files here to upload'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 1.5rem; + font-weight: 600; + color: var(--myfsio-muted); + pointer-events: none; + z-index: 10; +} + +.drop-zone.drag-over table { + opacity: 0.3; +} + +.modal-header, +.modal-footer { + border-color: var(--myfsio-card-border); +} + +:root { + --sidebar-width: 260px; + --sidebar-collapsed-width: 72px; + --mobile-header-height: 56px; + --sidebar-bg: linear-gradient(180deg, #0f172a 0%, #1e293b 100%); + --sidebar-border: rgba(255, 255, 255, 0.08); + --sidebar-link-color: rgba(255, 255, 255, 0.7); + --sidebar-link-hover: rgba(255, 255, 255, 0.95); + --sidebar-link-active-bg: rgba(59, 130, 246, 0.2); + --sidebar-link-active-border: #3b82f6; + --sidebar-section-color: rgba(255, 255, 255, 0.4); +} + +[data-theme='dark'] { + --sidebar-bg: linear-gradient(180deg, #020617 0%, #0f172a 100%); + --sidebar-border: rgba(255, 255, 255, 0.06); + --sidebar-link-active-bg: rgba(59, 130, 246, 0.25); +} + +body { + display: flex; + min-height: 100vh; +} + +.main-wrapper { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@media (min-width: 992px) { + .main-wrapper { + margin-left: var(--sidebar-width); + } + + body.sidebar-is-collapsed .main-wrapper { + margin-left: var(--sidebar-collapsed-width); + } +} + +.main-content { + flex: 1; + padding: 1.5rem; + max-width: 100%; + overflow-x: hidden; +} + +@media (min-width: 992px) { + .main-content { + padding: 2rem 2.5rem; + } +} + +.mobile-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--mobile-header-height); + background: var(--sidebar-bg); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + z-index: 1030; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.sidebar-toggle-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 10px; + color: white; + cursor: pointer; + transition: all 0.2s ease; +} + +.sidebar-toggle-btn:hover { + background: rgba(255, 255, 255, 0.15); + transform: scale(1.05); +} + +.mobile-brand { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: white; + font-weight: 600; + font-size: 1.1rem; +} + +.mobile-brand img { + border-radius: 8px; +} + +.theme-toggle-mobile { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 10px; + color: white; + cursor: pointer; + transition: all 0.2s ease; +} + +.theme-toggle-mobile:hover { + background: rgba(255, 255, 255, 0.15); +} + +@media (max-width: 991.98px) { + .main-wrapper { + padding-top: var(--mobile-header-height); + } +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + width: var(--sidebar-width); + height: 100vh; + background: var(--sidebar-bg); + display: flex; + flex-direction: column; + z-index: 1040; + border-right: 1px solid var(--sidebar-border); + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: visible; +} + +.sidebar-collapsed { + width: var(--sidebar-collapsed-width); +} + +html.sidebar-will-collapse .sidebar { + width: var(--sidebar-collapsed-width); +} + +html.sidebar-will-collapse .main-wrapper { + margin-left: var(--sidebar-collapsed-width); +} + +html.sidebar-will-collapse .sidebar, +html.sidebar-will-collapse .sidebar *, +html.sidebar-will-collapse .main-wrapper { + transition: none !important; +} + +html.sidebar-will-collapse .sidebar-title, +html.sidebar-will-collapse .sidebar-link-text, +html.sidebar-will-collapse .nav-section-title, +html.sidebar-will-collapse .theme-toggle-text, +html.sidebar-will-collapse .sidebar-user .user-info, +html.sidebar-will-collapse .sidebar-logout-btn .logout-text, +html.sidebar-will-collapse .sidebar-collapse-btn { + display: none !important; + opacity: 0 !important; + width: 0 !important; +} + +html.sidebar-will-collapse .sidebar-header { + justify-content: center; + padding: 1rem 0; +} + +html.sidebar-will-collapse .sidebar-brand { + justify-content: center; +} + +html.sidebar-will-collapse .nav-section { + padding: 0; + width: 100%; +} + +html.sidebar-will-collapse .sidebar-nav { + align-items: center; +} + +html.sidebar-will-collapse .sidebar-link { + justify-content: center; + align-items: center; + width: 48px; + height: 48px; + padding: 0; + margin: 0.25rem auto; + gap: 0; +} + +html.sidebar-will-collapse .sidebar-footer { + padding: 0.75rem 0.5rem; + align-items: center; +} + +html.sidebar-will-collapse .theme-toggle-sidebar, +html.sidebar-will-collapse .sidebar-user, +html.sidebar-will-collapse .sidebar-logout-btn { + justify-content: center; + width: 48px; + height: 48px; + padding: 0; + margin: 0 auto; +} + +html.sidebar-will-collapse .sidebar-user { + background: transparent; +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1rem; + border-bottom: 1px solid var(--sidebar-border); + min-height: 70px; + gap: 0.5rem; + overflow: visible; +} + +.sidebar-collapsed .sidebar-header { + justify-content: center; + padding: 1rem 0; + min-height: 70px; +} + +.sidebar-collapsed .sidebar-logo { + width: 36px; + height: 36px; +} + +.sidebar-collapsed .sidebar-brand { + justify-content: center; + cursor: pointer; + position: relative; + gap: 0; +} + +.sidebar-collapsed .sidebar-brand::after { + content: 'Click to expand'; + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: #1e293b; + color: white; + padding: 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 1050; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.sidebar-collapsed .sidebar-brand:hover::after { + opacity: 1; + visibility: visible; +} + +.sidebar-collapsed .sidebar-collapse-btn { + display: none; +} + +.sidebar-brand { + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + color: white; + overflow: visible; +} + +.sidebar-logo { + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.sidebar-brand:hover .sidebar-logo { + transform: scale(1.05); +} + +.sidebar-title { + font-weight: 700; + font-size: 1.25rem; + letter-spacing: -0.02em; + white-space: nowrap; + opacity: 1; + transition: opacity 0.2s ease; +} + +.sidebar-collapsed .sidebar-title { + opacity: 0; + width: 0; +} + +.sidebar-collapse-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 8px; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.sidebar-collapse-btn:hover { + background: rgba(255, 255, 255, 0.15); + color: white; +} + +.sidebar-collapse-btn svg { + transition: transform 0.3s ease; +} + +.sidebar-collapsed .sidebar-collapse-btn svg { + transform: rotate(180deg); +} + +.sidebar-body { + flex: 1; + padding: 1rem 0; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 0.5rem; + overflow: visible; +} + +.nav-section { + padding: 0 0.75rem; + margin-bottom: 0.5rem; + overflow: visible; +} + +.nav-section-title { + display: block; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--sidebar-section-color); + padding: 0.5rem 0.75rem; + white-space: nowrap; + overflow: hidden; + transition: opacity 0.2s ease; +} + +.sidebar-collapsed .nav-section-title { + opacity: 0; + height: 0; + padding: 0; + margin: 0; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.75rem 1rem; + color: var(--sidebar-link-color); + text-decoration: none; + border-radius: 10px; + transition: all 0.2s ease; + position: relative; + overflow: visible; + margin: 0.5rem; +} + +.sidebar-link::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 0; + background: var(--sidebar-link-active-border); + border-radius: 0 2px 2px 0; + transition: height 0.2s ease; +} + +.sidebar-link:hover { + color: var(--sidebar-link-hover); + background: rgba(255, 255, 255, 0.08); +} + +.sidebar-link.active { + color: white; + background: var(--sidebar-link-active-bg); +} + +.sidebar-link.active::before { + height: 60%; +} + +.sidebar-link svg { + flex-shrink: 0; + opacity: 0.9; + transition: transform 0.2s ease; +} + +.sidebar-link:hover svg { + transform: scale(1.1); +} + +.sidebar-link-text { + white-space: nowrap; + overflow: hidden; + transition: opacity 0.2s ease, width 0.2s ease; +} + +.sidebar-collapsed .sidebar-link-text { + opacity: 0; + width: 0; +} + +.sidebar-collapsed .sidebar-link { + justify-content: center; + align-items: center; + padding: 0; + margin: 0.25rem auto; + width: 48px; + height: 48px; + gap: 0; +} + +.sidebar-collapsed .sidebar-link svg { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.sidebar-collapsed .nav-section { + padding: 0; + width: 100%; +} + +.sidebar-collapsed .sidebar-nav { + align-items: center; +} + +.sidebar-collapsed .sidebar-link[data-tooltip]::after { + content: attr(data-tooltip); + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: #1e293b; + color: white; + padding: 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.85rem; + font-weight: 500; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 1050; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.sidebar-collapsed .sidebar-link[data-tooltip]:hover::after { + opacity: 1; + visibility: visible; +} + +.sidebar-footer { + padding: 1rem; + border-top: 1px solid var(--sidebar-border); + display: flex; + flex-direction: column; + gap: 0.75rem; + overflow: visible; +} + +.sidebar-collapsed .sidebar-footer { + padding: 0.75rem 0.5rem; + align-items: center; +} + +.theme-toggle-sidebar { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.75rem 1rem; + background: rgba(255, 255, 255, 0.08); + border: none; + border-radius: 10px; + color: rgba(255, 255, 255, 0.8); + cursor: pointer; + transition: all 0.2s ease; +} + +.theme-toggle-sidebar:hover { + background: rgba(255, 255, 255, 0.12); + color: white; +} + +.theme-toggle-text { + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + transition: opacity 0.2s ease; +} + +.sidebar-collapsed .theme-toggle-text { + opacity: 0; + width: 0; + display: none; +} + +.sidebar-collapsed .theme-toggle-sidebar { + justify-content: center; + padding: 0; + width: 48px; + height: 48px; + margin: 0 auto; + position: relative; +} + +.sidebar-collapsed .theme-toggle-sidebar svg { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.sidebar-collapsed .theme-toggle-sidebar::after { + content: 'Toggle theme'; + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: #1e293b; + color: white; + padding: 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 1050; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.sidebar-collapsed .theme-toggle-sidebar:hover::after { + opacity: 1; + visibility: visible; +} + +.sidebar-user { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + overflow: hidden; +} + +.sidebar-user .user-avatar { + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + color: white; + flex-shrink: 0; +} + +.sidebar-user .user-info { + overflow: hidden; + transition: opacity 0.2s ease, width 0.2s ease; +} + +.sidebar-collapsed .sidebar-user .user-info { + opacity: 0; + width: 0; + display: none; +} + +.sidebar-collapsed .sidebar-user { + justify-content: center; + padding: 0; + width: 48px; + height: 48px; + margin: 0 auto; + background: transparent; + position: relative; +} + +.sidebar-collapsed .sidebar-user .user-avatar { + width: 36px; + height: 36px; +} + +.sidebar-collapsed .sidebar-user::after { + content: attr(data-username); + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: #1e293b; + color: white; + padding: 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 1050; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.sidebar-collapsed .sidebar-user:hover::after { + opacity: 1; + visibility: visible; +} + +.sidebar-user .user-name { + font-size: 0.875rem; + font-weight: 600; + color: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-user .user-key { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-logout-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + width: 100%; + padding: 0.75rem 1rem; + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 10px; + color: #fca5a5; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.sidebar-logout-btn:hover { + background: rgba(239, 68, 68, 0.25); + border-color: rgba(239, 68, 68, 0.5); + color: #fecaca; +} + +.sidebar-collapsed .sidebar-logout-btn .logout-text { + display: none; +} + +.sidebar-collapsed .sidebar-logout-btn { + justify-content: center; + padding: 0; + width: 48px; + height: 48px; + margin: 0 auto; + position: relative; +} + +.sidebar-collapsed .sidebar-logout-btn svg { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.sidebar-collapsed .sidebar-logout-btn::after { + content: 'Sign out'; + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: #1e293b; + color: white; + padding: 0.5rem 0.875rem; + border-radius: 8px; + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 1050; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.sidebar-collapsed .sidebar-logout-btn:hover::after { + opacity: 1; + visibility: visible; +} + +.sidebar-offcanvas { + width: 280px !important; + background: var(--sidebar-bg) !important; + border-right: none !important; +} + +.sidebar-offcanvas .sidebar-header { + background: transparent; +} + +.sidebar-offcanvas .sidebar-body { + display: flex; + flex-direction: column; + padding: 0; +} + +.sidebar-offcanvas .sidebar-nav { + flex: 1; + padding: 1rem 0; +} + +.sidebar-offcanvas .sidebar-footer { + margin-top: auto; +} + +.myfsio-nav { + background: var(--myfsio-nav-gradient); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.myfsio-nav .navbar-brand { + color: #fff; + font-weight: 600; + letter-spacing: -0.02em; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.myfsio-logo { + border-radius: 0.35rem; + box-shadow: 0 0 6px rgba(15, 23, 42, 0.35); + background-color: rgba(255, 255, 255, 0.1); +} + +.myfsio-title { + display: inline-block; +} + +.myfsio-nav .nav-link { + color: var(--myfsio-nav-link); + transition: color 0.2s ease; +} + +.myfsio-nav .nav-link:hover { + color: var(--myfsio-nav-link-hover); +} + +.myfsio-nav .nav-link.nav-link-muted { opacity: 0.75; } + +.myfsio-nav .nav-link.nav-link-muted .badge { + color: #0f172a; + background-color: #fef08a; +} + +[data-theme='dark'] .myfsio-nav .nav-link.nav-link-muted .badge { + color: #0f172a; + background-color: #fde047; +} + +.myfsio-nav .navbar-toggler { + border-color: rgba(255, 255, 255, 0.6); +} + +.myfsio-nav .navbar-toggler-icon { + filter: invert(1); +} + +.docs-hero { + background: var(--myfsio-nav-gradient); + color: #fff !important; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 15px 35px rgba(15, 23, 42, 0.3); +} + +.docs-hero * { + color: inherit; +} + +.docs-callout { + background-color: rgba(15, 23, 42, 0.35); + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 0.75rem; + padding: 1rem 1.25rem; +} + +.docs-callout code { + color: #fff; + background-color: rgba(0, 0, 0, 0.2); +} + +[data-theme='dark'] .docs-callout { + background-color: rgba(2, 6, 23, 0.55); + border-color: rgba(255, 255, 255, 0.25); +} + +.docs-feature-card + .docs-feature-card { + margin-top: 1.25rem; +} + +.docs-checklist { + padding-left: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.docs-checklist li { + margin: 0; +} + +.docs-section { + border: 1px solid var(--myfsio-card-border); + border-radius: 1rem; +} + +.docs-section-kicker { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 999px; + background: rgba(59, 130, 246, 0.12); + color: #1d4ed8; + font-weight: 600; +} + +[data-theme='dark'] .docs-section-kicker { + background: rgba(59, 130, 246, 0.25); + color: #93c5fd; +} + +.docs-steps { + counter-reset: docs-step; + margin: 1rem 0 1.25rem; + padding-left: 1.25rem; +} + +.docs-steps li { + margin-bottom: 0.4rem; +} + +.docs-highlight { + background: rgba(59, 130, 246, 0.08); + border-radius: 0.75rem; + padding: 1rem 1.25rem; + border: 1px solid rgba(59, 130, 246, 0.2); +} + +[data-theme='dark'] .docs-highlight { + background: rgba(59, 130, 246, 0.18); + border-color: rgba(59, 130, 246, 0.35); +} + +.docs-pill-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; +} + +.docs-pill-list ul { + padding-left: 1.1rem; + margin-bottom: 0; +} + +.docs-table thead { + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.08em; +} + +[data-theme='dark'] .docs-table .table-secondary, +[data-theme='dark'] .docs-section .table-secondary { + --bs-table-bg: rgba(148, 163, 184, 0.14); + --bs-table-striped-bg: rgba(148, 163, 184, 0.16); + --bs-table-hover-bg: rgba(148, 163, 184, 0.2); + --bs-table-color: var(--myfsio-text); + color: var(--myfsio-text); +} + +[data-theme='dark'] .docs-table .table-secondary th, +[data-theme='dark'] .docs-table .table-secondary td, +[data-theme='dark'] .docs-table .table-secondary strong, +[data-theme='dark'] .docs-table .table-secondary code, +[data-theme='dark'] .docs-section .table-secondary th, +[data-theme='dark'] .docs-section .table-secondary td, +[data-theme='dark'] .docs-section .table-secondary strong, +[data-theme='dark'] .docs-section .table-secondary code { + color: var(--myfsio-text); +} + +.main-content:has(.docs-sidebar) { + overflow-x: visible; +} + +.docs-sidebar { + position: sticky; + top: 1.5rem; + border-radius: 1rem; + border: 1px solid var(--myfsio-card-border); + max-height: calc(100vh - 3rem); + overflow-y: auto; +} + +.docs-sidebar-callouts { + display: flex; + flex-direction: column; + gap: 0.85rem; + padding: 1rem; + border-radius: 0.75rem; + background-color: rgba(15, 23, 42, 0.04); +} + +[data-theme='dark'] .docs-sidebar-callouts { + background-color: rgba(248, 250, 252, 0.05); +} + +.docs-sidebar-callouts code { + font-size: 0.85rem; +} + +.docs-toc a { + color: var(--myfsio-text); + text-decoration: none; + display: inline-flex; + gap: 0.35rem; + align-items: center; + padding: 0.2rem 0; +} + +.docs-toc a:hover { + color: #2563eb; +} + +.docs-sidebar-mobile { + border-radius: 0.75rem; + border: 1px solid var(--myfsio-card-border); +} + +.docs-sidebar-mobile .docs-toc { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1rem; + padding-top: 0.5rem; +} + +.docs-sidebar-mobile .docs-toc li { + flex: 1 0 45%; +} + +.min-width-0 { + min-width: 0; +} + +.alert pre { + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.iam-user-card { + position: relative; + border: 1px solid var(--myfsio-card-border) !important; + border-radius: 1rem !important; + overflow: visible; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.iam-user-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.12), 0 4px 8px -4px rgba(0, 0, 0, 0.08); + border-color: var(--myfsio-accent) !important; +} + +[data-theme='dark'] .iam-user-card:hover { + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4), 0 4px 8px -4px rgba(0, 0, 0, 0.3); +} + + +.iam-role-badge { + display: inline-flex; + align-items: center; + padding: 0.25em 0.65em; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.iam-role-admin { + background: rgba(245, 158, 11, 0.15); + color: #d97706; +} + +[data-theme='dark'] .iam-role-admin { + background: rgba(245, 158, 11, 0.25); + color: #fbbf24; +} + +.iam-role-user { + background: rgba(59, 130, 246, 0.12); + color: #2563eb; +} + +[data-theme='dark'] .iam-role-user { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; +} + +.iam-perm-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.3em 0.6em; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 500; + background: rgba(59, 130, 246, 0.08); + color: var(--myfsio-text); + border: 1px solid rgba(59, 130, 246, 0.15); +} + +[data-theme='dark'] .iam-perm-badge { + background: rgba(59, 130, 246, 0.15); + border-color: rgba(59, 130, 246, 0.25); +} + +.iam-copy-key { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: none; + background: transparent; + color: var(--myfsio-muted); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.iam-copy-key:hover { + background: var(--myfsio-hover-bg); + color: var(--myfsio-text); +} + +.iam-no-results { + text-align: center; + padding: 2rem 1rem; + color: var(--myfsio-muted); +} + +@media (max-width: 768px) { + .iam-user-card:hover { + transform: none; + } +} + +.user-avatar-lg { + width: 48px; + height: 48px; + border-radius: 12px; +} + +.btn-icon { + padding: 0.25rem; + line-height: 1; + border: none; + background: transparent; + color: var(--myfsio-muted); + border-radius: 0.375rem; +} + +.btn-icon:hover { + background: var(--myfsio-hover-bg); + color: var(--myfsio-text); +} + +.badge { + font-weight: 500; + padding: 0.35em 0.65em; + font-size: 0.8125rem; +} + +.theme-toggle { + min-width: auto; + width: 38px; + height: 32px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + transition: all 0.2s ease; +} + +.theme-toggle:hover { + transform: translateY(-1px); +} + +.theme-toggle .theme-icon { + transition: opacity 0.2s ease, transform 0.2s ease; +} + +[data-bs-theme="light"] #themeToggleSun, +[data-bs-theme="light"] #themeToggleSunMobile { + display: none !important; +} +[data-bs-theme="light"] #themeToggleMoon, +[data-bs-theme="light"] #themeToggleMoonMobile { + display: inline-block !important; +} +[data-bs-theme="dark"] #themeToggleSun, +[data-bs-theme="dark"] #themeToggleSunMobile { + display: inline-block !important; +} +[data-bs-theme="dark"] #themeToggleMoon, +[data-bs-theme="dark"] #themeToggleMoonMobile { + display: none !important; +} + +.config-copy { + position: absolute; + top: 0.5rem; + right: 0.5rem; + opacity: 0.8; + transition: opacity 0.2s; + background-color: rgba(0, 0, 0, 0.5); + border: none; + color: white; +} + +.config-copy:hover { + opacity: 1; + background-color: rgba(0, 0, 0, 0.7); + color: white; +} + +.bucket-table td:last-child, +.bucket-table th:last-child { white-space: nowrap; } + +.object-key { + max-width: 0; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + +.object-key .fw-medium { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.object-key .text-muted { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.preview-card { top: 1rem; } + +.preview-stage { + background-color: var(--myfsio-preview-bg); + overflow: hidden; + border-color: var(--myfsio-card-border) !important; +} + +.preview-stage:has(#preview-placeholder:not(.d-none)) { + min-height: 0; +} + +.preview-stage:has(#preview-image:not(.d-none)), +.preview-stage:has(#preview-video:not(.d-none)), +.preview-stage:has(#preview-iframe:not(.d-none)) { + min-height: 200px; +} + +#preview-placeholder { + padding: 2rem 1rem; +} + +#preview-text { + padding: 1rem 1.125rem; + max-height: 360px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: 'SFMono-Regular', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + font-size: .8rem; + line-height: 1.6; + tab-size: 4; + color: var(--myfsio-text); + background: transparent; +} + +.upload-progress-stack { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 300px; + overflow-y: auto; +} + +.upload-progress-item { + border: 1px solid var(--myfsio-card-border); + border-radius: 0.75rem; + background-color: var(--myfsio-card-bg); + padding: 0.875rem 1rem; + transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; +} + +.upload-progress-item[data-state='uploading'] { + border-color: rgba(59, 130, 246, 0.4); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08); +} + +.upload-progress-item[data-state='success'] { + border-color: rgba(34, 197, 94, 0.6); + background-color: rgba(34, 197, 94, 0.04); +} + +.upload-progress-item[data-state='error'] { + border-color: rgba(239, 68, 68, 0.7); + background-color: rgba(239, 68, 68, 0.04); +} + +.upload-progress-item .file-name { + font-weight: 500; + word-break: break-all; + margin-bottom: 0.25rem; +} + +.upload-progress-item .file-size { + font-size: 0.75rem; + color: var(--myfsio-muted); +} + +.upload-progress-item .upload-status { + font-size: 0.8rem; + color: var(--myfsio-muted); +} + +.upload-progress-item .upload-status.success { + color: #16a34a; +} + +.upload-progress-item .upload-status.error { + color: #dc2626; +} + +.upload-progress-item .progress-container { + margin-top: 0.5rem; +} + +.upload-progress-item .progress { + height: 6px; + border-radius: 999px; + overflow: hidden; +} + +.upload-progress-item .progress-bar { + transition: width 0.2s ease; +} + +.upload-progress-item .progress-text { + font-size: 0.7rem; + color: var(--myfsio-muted); + margin-top: 0.25rem; + display: flex; + justify-content: space-between; +} + +.progress-thin { + height: 0.35rem; + background-color: rgba(15, 23, 42, 0.1); +} + +[data-theme='dark'] .progress-thin { + background-color: rgba(248, 250, 252, 0.15); +} + +[data-theme='dark'] .upload-progress-item .upload-status.success { + color: #4ade80; +} + +[data-theme='dark'] .upload-progress-item .upload-status.error { + color: #f87171; +} + +#deleteObjectKey { + word-break: break-all; + max-width: 100%; +} + +.preview-stage img, +.preview-stage video, +.preview-stage iframe { + border: 0; + max-height: 360px; +} + +.upload-dropzone { + border: 2px dashed var(--myfsio-card-border); + border-radius: 0.75rem; + padding: 1.5rem; + cursor: pointer; + transition: border-color 0.2s ease, background-color 0.2s ease; + position: relative; + overflow: hidden; +} + +.upload-dropzone::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.03) 0%, rgba(139, 92, 246, 0.03) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.upload-dropzone:hover::before, +.upload-dropzone.is-dragover::before { + opacity: 1; +} + +.upload-dropzone.is-dragover { + background-color: rgba(59, 130, 246, 0.08); + border-color: #3b82f6; +} + +.upload-dropzone.upload-locked { + background-color: rgba(59, 130, 246, 0.05); + border-color: #3b82f6; + border-style: dashed; +} + +.upload-dropzone.upload-locked::after { + content: 'Drop more files to add to queue'; + display: block; + margin-top: 0.5rem; + font-size: 0.8rem; + color: #3b82f6; + font-weight: 500; +} + +.metadata-stack .metadata-entry + .metadata-entry { + margin-top: 0.75rem; +} + +.metadata-stack .metadata-key { + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--myfsio-muted); +} + +.metadata-stack .metadata-value { + font-weight: 600; +} + +.policy-preview { + background-color: var(--myfsio-policy-bg); + color: var(--myfsio-policy-fg); + border-radius: 0.5rem; + padding: 1rem; + font-size: 0.85rem; + max-height: 320px; + overflow: auto; + border: 1px solid var(--myfsio-card-border); +} + +.policy-editor-disabled { + opacity: 0.72; + cursor: not-allowed; +} + +.objects-table-container { + max-height: 600px; + overflow-y: auto; +} + +.objects-table-container thead { + position: sticky; + top: 0; + z-index: 10; +} + +.objects-table-container thead th { + background-color: var(--myfsio-preview-bg); + border-bottom: 2px solid var(--myfsio-card-border); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); +} + +[data-theme='dark'] .objects-table-container thead th { + background-color: #1a2234; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.btn-group form { display: inline; } + +.font-monospace { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; } + +.table { + color: var(--myfsio-text); + background-color: var(--myfsio-card-bg); + border-collapse: separate; + border-spacing: 0; + font-size: 0.9rem; +} + +.table th, +.table td { + border-color: var(--myfsio-card-border); + padding: 0.875rem 1rem; + vertical-align: middle; +} + +.table th { + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--myfsio-muted); + border-bottom: 2px solid var(--myfsio-card-border); +} + +.table td { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +[data-theme='dark'] .table td { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.table tbody tr:last-child td { + border-bottom: none; +} + +.table-light th { + background-color: var(--myfsio-preview-bg); +} + +[data-theme='dark'] .table-light th { + background-color: rgba(248, 250, 252, 0.03); + color: var(--myfsio-muted); +} + +.table-hover tbody tr { + transition: all 0.15s ease; +} + +.table-hover tbody tr:hover { + background-color: var(--myfsio-hover-bg); + cursor: pointer; +} + +.table-hover tbody tr:hover td { + border-bottom-color: transparent; +} + +.table thead { + background-color: var(--myfsio-preview-bg); + color: var(--myfsio-muted); +} + +[data-theme='dark'] .table thead { + background-color: rgba(248, 250, 252, 0.03); + color: var(--myfsio-muted); +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.015); +} + +[data-theme='dark'] .table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.02); +} + +.form-control, +.form-select { + background-color: var(--myfsio-input-bg); + color: var(--myfsio-text); + border-color: var(--myfsio-input-border); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.form-control::placeholder { + color: var(--myfsio-muted); + opacity: 0.6; +} + +[data-theme='dark'] .form-control::placeholder { + opacity: 0.5; +} + +.form-control:focus, +.form-select:focus { + background-color: var(--myfsio-input-bg); + color: var(--myfsio-text); + border-color: #3b82f6; + box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25); +} + +.alert { + color: var(--myfsio-text); + border-color: var(--myfsio-card-border); + border-width: 1px; + border-left-width: 4px; +} + +.alert-success { + background-color: rgba(34, 197, 94, 0.1); + border-left-color: #22c55e; +} + +[data-theme='dark'] .alert-success { + background-color: rgba(34, 197, 94, 0.15); + color: #86efac; +} + +.alert-danger { + background-color: rgba(239, 68, 68, 0.1); + border-left-color: #ef4444; +} + +[data-theme='dark'] .alert-danger { + background-color: rgba(239, 68, 68, 0.15); + color: #fca5a5; +} + +.alert-warning { + background-color: rgba(251, 191, 36, 0.1); + border-left-color: #fbbf24; +} + +[data-theme='dark'] .alert-warning { + background-color: rgba(251, 191, 36, 0.15); + color: #fde047; +} + +.alert-info { + background-color: rgba(59, 130, 246, 0.1); + border-left-color: #3b82f6; +} + +[data-theme='dark'] .alert-info { + background-color: rgba(59, 130, 246, 0.15); + color: #93c5fd; +} + +.btn { + color: inherit; + transition: all 0.2s ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +.btn-icon { + width: 36px; + height: 36px; + padding: 0.4rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.btn-group-sm .btn-icon { + width: 34px; + height: 34px; +} + +[data-theme='dark'] .btn-outline-secondary { + color: #e2e8f0; + border-color: #475569; +} + +[data-theme='dark'] .btn-outline-secondary:hover { + background-color: rgba(148, 163, 184, 0.2); + border-color: #64748b; + color: #f8fafc; +} + +[data-theme='dark'] .btn-outline-danger { + color: #fca5a5; + border-color: #f87171; +} + +[data-theme='dark'] .btn-outline-danger:hover { + background-color: rgba(248, 113, 113, 0.2); + border-color: #ef4444; + color: #fecaca; +} + +[data-theme='dark'] .btn-outline-primary { + color: #93c5fd; + border-color: #60a5fa; +} + +[data-theme='dark'] .btn-outline-primary:hover { + background-color: rgba(59, 130, 246, 0.2); + border-color: #3b82f6; + color: #bfdbfe; +} + +[data-theme='dark'] .btn-primary { + background-color: #2563eb; + border-color: #1d4ed8; + color: #ffffff; +} + +[data-theme='dark'] .btn-primary:hover { + background-color: #1d4ed8; + border-color: #1e40af; +} + +.btn-primary { + color: #ffffff; + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + border: none; + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.btn-primary:active { + box-shadow: 0 1px 2px rgba(59, 130, 246, 0.3); +} + +[data-theme='dark'] .btn-danger { + background-color: #dc2626; + border-color: #b91c1c; +} + +[data-theme='dark'] .btn-danger:hover { + background-color: #b91c1c; + border-color: #991b1b; +} + +.badge.text-bg-info { + background-color: #bae6fd; + color: #0f172a; +} + +[data-theme='dark'] .badge.text-bg-info { + background-color: #0ea5e9; + color: #e2e8f0; +} + +[data-theme='dark'] .badge.text-bg-warning { + background-color: #fde047; + color: #0f172a; +} + +[data-theme='dark'] .badge.text-bg-secondary { + background-color: #475569; + color: #e2e8f0; +} + +[data-theme='dark'] .badge.text-bg-success { + background-color: #22c55e; + color: #ffffff; +} + +[data-theme='dark'] .badge.text-bg-primary { + background-color: #3b82f6; + color: #ffffff; +} + +.dropdown-menu { + border-color: var(--myfsio-card-border); +} + +[data-theme='dark'] .form-label, +[data-theme='dark'] label, +[data-theme='dark'] .modal-title, +[data-theme='dark'] .fw-semibold { + color: var(--myfsio-text); +} + +.modal-backdrop.show { + opacity: 0.6; +} + +[data-theme='dark'] .btn-close { + filter: invert(1) grayscale(100%) brightness(200%); +} + +[data-theme='dark'] .border { + border-color: var(--myfsio-card-border) !important; +} + +.btn-link { + color: #3b82f6; + text-decoration: none; +} + +.btn-link:hover { + color: #2563eb; + text-decoration: underline; + transform: none; +} + +[data-theme='dark'] .btn-link { + color: #60a5fa; +} + +[data-theme='dark'] .btn-link:hover { + color: #93c5fd; +} + +[data-theme='dark'] .input-group-text { + background-color: var(--myfsio-input-bg); + color: var(--myfsio-text); + border-color: var(--myfsio-input-border); +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-weight: 600; + letter-spacing: -0.02em; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +[data-theme='dark'] .text-primary { + color: #60a5fa !important; +} + +[data-theme='dark'] .text-success { + color: #86efac !important; +} + +[data-theme='dark'] .text-danger { + color: #fca5a5 !important; +} + +[data-theme='dark'] .text-warning { + color: #fde047 !important; +} + +[data-theme='dark'] .lead { + color: var(--myfsio-muted); +} + +.btn-sm { + transition: all 0.15s ease; +} + +[data-theme='dark'] .btn-outline-light { + color: #f8fafc; + border-color: rgba(248, 250, 252, 0.3); +} + +[data-theme='dark'] .btn-outline-light:hover { + background-color: rgba(248, 250, 252, 0.1); + border-color: rgba(248, 250, 252, 0.5); +} + +pre { + background-color: rgba(15, 23, 42, 0.05); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.6; +} + +[data-theme='dark'] pre { + background-color: #111827; + border-color: rgba(148, 163, 184, 0.24); + color: #e5eefb; +} + +pre code { + background: none; + padding: 0; + color: inherit; +} + +[data-theme='dark'] .docs-section .bg-light { + background-color: #182235 !important; + border: 1px solid rgba(148, 163, 184, 0.18); + color: #e5eefb; +} + +[data-theme='dark'] .docs-section .bg-light .text-muted { + color: #a9b6c8 !important; +} + +.docs-section + .docs-section { + margin-top: 1.25rem; +} + +.breadcrumb { + background-color: transparent; + padding: 0.5rem 0; + font-size: 0.9rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + content: "›"; + color: var(--myfsio-muted); +} + +.breadcrumb-item a { + color: var(--myfsio-text); + text-decoration: none; + transition: color 0.2s ease; +} + +.breadcrumb-item a:hover { + color: #3b82f6; + text-decoration: underline; +} + +[data-theme='dark'] .breadcrumb-item a:hover { + color: #60a5fa; +} + +.breadcrumb-item.active { + color: var(--myfsio-muted); +} + +.bi { + vertical-align: -0.125em; +} + +.sticky-top { + top: 1.5rem; +} + +.card-body dl:last-child { + margin-bottom: 0; +} + +.text-center svg { + display: inline-block; +} + +[data-theme='dark'] .input-group .btn-outline-primary { + background-color: transparent; +} + +.text-nowrap { + white-space: nowrap; +} + +.alert svg { + flex-shrink: 0; +} + +[data-object-row]:hover { + background-color: var(--myfsio-hover-bg) !important; +} + +.folder-row { + background-color: var(--myfsio-section-bg); + transition: background-color 0.15s ease; +} + +.folder-row:hover { + background-color: var(--myfsio-hover-bg) !important; +} + +.folder-row td:first-child { + padding-left: 0.5rem; +} + +.btn-group-sm .btn { + padding: 0.25rem 0.6rem; + font-size: 0.875rem; +} + +.modal-content { + border: none; + border-radius: 1rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + overflow: hidden; +} + +.modal-header { + background-color: var(--myfsio-card-bg); + border-bottom: 1px solid var(--myfsio-card-border); + padding: 1.25rem 1.5rem; +} + +.modal-header.border-0 { + border-bottom: none; +} + +.modal-title { + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.modal-body { + padding: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.modal-footer { + background-color: var(--myfsio-preview-bg); + border-top: 1px solid var(--myfsio-card-border); + padding: 1rem 1.5rem; + gap: 0.75rem; +} + +.modal-footer.border-0 { + border-top: none; + background-color: var(--myfsio-card-bg); +} + +[data-theme='dark'] .modal-footer { + background-color: rgba(0, 0, 0, 0.2); +} + +[data-theme='dark'] .modal-footer.border-0 { + background-color: var(--myfsio-card-bg); +} + +.modal-icon { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; +} + +.modal-icon-danger { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.modal-icon-warning { + background: rgba(251, 191, 36, 0.1); + color: #f59e0b; +} + +.modal-icon-success { + background: rgba(34, 197, 94, 0.1); + color: #22c55e; +} + +.modal-icon-info { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; +} + +[data-theme='dark'] .modal-icon-danger { + background: rgba(239, 68, 68, 0.2); + color: #f87171; +} + +[data-theme='dark'] .modal-icon-warning { + background: rgba(251, 191, 36, 0.2); + color: #fbbf24; +} + +[data-theme='dark'] .modal-icon-success { + background: rgba(34, 197, 94, 0.2); + color: #4ade80; +} + +[data-theme='dark'] .modal-icon-info { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; +} + +.modal .alert { + border-radius: 0.75rem; +} + +.modal .form-control, +.modal .form-select { + border-radius: 0.5rem; +} + +.modal .btn { + border-radius: 0.5rem; + padding: 0.5rem 1rem; + font-weight: 500; +} + +.modal .btn-sm { + padding: 0.375rem 0.75rem; +} + +.modal .list-group { + border-radius: 0.75rem; + overflow: hidden; +} + +.modal .list-group-item { + border-color: var(--myfsio-card-border); +} + +.modal-backdrop { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + color: white; + flex-shrink: 0; +} + +.connection-icon { + width: 32px; + height: 32px; + border-radius: 0.5rem; + background: var(--myfsio-preview-bg); + display: flex; + align-items: center; + justify-content: center; + color: var(--myfsio-muted); + flex-shrink: 0; +} + +[data-theme='dark'] .connection-icon { + background: rgba(255, 255, 255, 0.1); +} + +.bucket-card { + position: relative; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--myfsio-card-border) !important; + overflow: hidden; + border-radius: 1rem !important; +} + +.bucket-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #3b82f6, #8b5cf6); + opacity: 0; + transition: opacity 0.2s ease; +} + +.bucket-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.12), 0 4px 8px -4px rgba(0, 0, 0, 0.08); + border-color: var(--myfsio-accent) !important; +} + +.bucket-card:hover::before { + opacity: 1; +} + +[data-theme='dark'] .bucket-card:hover { + box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4), 0 4px 8px -4px rgba(0, 0, 0, 0.3); +} + +.bucket-card .card-body { + padding: 1.25rem 1.5rem; +} + +.bucket-card .card-footer { + padding: 0.75rem 1.5rem; +} + +.bucket-card .bucket-icon { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%); + color: var(--myfsio-accent); + transition: transform 0.2s ease; +} + +.bucket-card:hover .bucket-icon { + transform: scale(1.05); +} + +[data-theme='dark'] .bucket-card .bucket-icon { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(139, 92, 246, 0.25) 100%); +} + +.bucket-card .bucket-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + padding: 1rem; + margin-top: 1rem; + background: var(--myfsio-preview-bg); + border-radius: 0.75rem; +} + +.bucket-card .bucket-stat { + text-align: center; +} + +.bucket-card .bucket-stat-value { + font-size: 1.25rem; + font-weight: 700; + color: var(--myfsio-text); + line-height: 1.2; +} + +.bucket-card .bucket-stat-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--myfsio-muted); + margin-top: 0.25rem; +} + +.bucket-card .bucket-name { + font-size: 1.1rem; + font-weight: 600; + color: var(--myfsio-text); + margin: 0; + line-height: 1.3; +} + +.bucket-card .bucket-access-badge { + font-size: 0.7rem; + padding: 0.35em 0.75em; + border-radius: 999px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.form-control:focus, +.form-select:focus, +.btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3), 0 0 0 1px rgba(59, 130, 246, 0.5); +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton { + background: linear-gradient(90deg, var(--myfsio-card-bg) 25%, var(--myfsio-hover-bg) 50%, var(--myfsio-card-bg) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 4px; +} + +.toast-container { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 1100; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.toast-item { + background: var(--myfsio-card-bg); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.75rem; + padding: 1rem 1.25rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 0.75rem; + animation: slideInRight 0.3s ease-out; + max-width: 360px; +} + +@keyframes slideInRight { + from { opacity: 0; transform: translateX(100%); } + to { opacity: 1; transform: translateX(0); } +} + +.empty-state { + padding: 3rem 2rem; + text-align: center; +} + +.empty-state-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); + border-radius: 50%; + color: #3b82f6; +} + +[data-theme='dark'] .empty-state-icon { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%); + color: #60a5fa; +} + +.metric-card { + position: relative; + overflow: hidden; +} + +.metric-card::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 120px; + height: 120px; + background: linear-gradient(135deg, transparent 40%, rgba(59, 130, 246, 0.05) 100%); + border-radius: 50%; + transform: translate(30%, -30%); +} + +[data-theme='dark'] .metric-card::after { + background: linear-gradient(135deg, transparent 40%, rgba(59, 130, 246, 0.1) 100%); +} + +.progress { + overflow: visible; + border-radius: 999px; +} + +.progress-bar { + border-radius: 999px; + position: relative; + transition: width 0.6s ease; +} + +.icon-box { + transition: all 0.2s ease; +} + +.card:hover .icon-box { + transform: scale(1.1) rotate(5deg); +} + +.search-input-wrapper { + position: relative; +} + +.search-input-wrapper .form-control { + padding-left: 2.75rem; + border-radius: 999px; + transition: all 0.2s ease; +} + +.search-input-wrapper .form-control:focus { + padding-left: 2.75rem; +} + +.search-input-wrapper .search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--myfsio-muted); + pointer-events: none; + transition: color 0.2s ease; +} + +.search-input-wrapper:focus-within .search-icon { + color: #3b82f6; +} + +.nav-tabs { + border-bottom: 2px solid var(--myfsio-card-border); +} + +.nav-tabs .nav-link { + border: none; + color: var(--myfsio-muted); + padding: 0.75rem 1.25rem; + font-weight: 500; + position: relative; + transition: all 0.2s ease; +} + +.nav-tabs .nav-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #3b82f6, #8b5cf6); + transform: scaleX(0); + transition: transform 0.2s ease; +} + +.nav-tabs .nav-link:hover { + color: var(--myfsio-text); + border: none; +} + +.nav-tabs .nav-link.active { + background: transparent; + color: #3b82f6; + border: none; +} + +.nav-tabs .nav-link.active::after { + transform: scaleX(1); +} + +[data-theme='dark'] .nav-tabs .nav-link.active { + color: #60a5fa; +} + +.file-type-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.file-type-image { background: rgba(139, 92, 246, 0.15); color: #7c3aed; } +.file-type-video { background: rgba(236, 72, 153, 0.15); color: #db2777; } +.file-type-audio { background: rgba(245, 158, 11, 0.15); color: #d97706; } +.file-type-document { background: rgba(59, 130, 246, 0.15); color: #2563eb; } +.file-type-archive { background: rgba(34, 197, 94, 0.15); color: #16a34a; } +.file-type-code { background: rgba(99, 102, 241, 0.15); color: #4f46e5; } + +[data-theme='dark'] .file-type-image { background: rgba(139, 92, 246, 0.25); color: #a78bfa; } +[data-theme='dark'] .file-type-video { background: rgba(236, 72, 153, 0.25); color: #f472b6; } +[data-theme='dark'] .file-type-audio { background: rgba(245, 158, 11, 0.25); color: #fbbf24; } +[data-theme='dark'] .file-type-document { background: rgba(59, 130, 246, 0.25); color: #60a5fa; } +[data-theme='dark'] .file-type-archive { background: rgba(34, 197, 94, 0.25); color: #4ade80; } +[data-theme='dark'] .file-type-code { background: rgba(99, 102, 241, 0.25); color: #818cf8; } + +.table-hover [data-object-row] .btn-group { + opacity: 0.5; + transition: opacity 0.2s ease; +} + +.table-hover [data-object-row]:hover .btn-group { + opacity: 1; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--myfsio-body-bg); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--myfsio-muted); + border-radius: 4px; + opacity: 0.5; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--myfsio-text); +} + +.modal.fade .modal-dialog { + transform: scale(0.95) translateY(-20px); + transition: transform 0.2s ease-out; +} + +.modal.show .modal-dialog { + transform: scale(1) translateY(0); +} + +.tooltip-inner { + background: var(--myfsio-policy-bg); + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.8125rem; +} + +@keyframes countUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.stat-value { + animation: countUp 0.5s ease-out; +} + +.action-btn-group { + display: flex; + gap: 0.5rem; +} + +.action-btn-group .btn { + border-radius: 8px; +} + +@keyframes livePulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.live-indicator { + width: 8px; + height: 8px; + background: #22c55e; + border-radius: 50%; + animation: livePulse 2s infinite; +} + +.login-card { + border: none; + border-radius: 1rem; + overflow: hidden; +} + +.login-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899); +} + +@media (max-width: 768px) { + .bucket-card:hover { + transform: none; + } + + .objects-table-container { + max-height: 60vh; + } + + .preview-card { + position: relative !important; + top: 0 !important; + } + + .card-body .table-responsive { + margin: -1rem; + padding: 0; + width: calc(100% + 2rem); + } + + .card-body .table-responsive table { + margin-bottom: 0; + } + + .table th, + .table td { + padding: 0.5rem 0.75rem; + } + + .table-responsive::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 20px; + background: linear-gradient(to left, var(--myfsio-card-bg), transparent); + pointer-events: none; + opacity: 0; + transition: opacity 0.3s; + } + + .table-responsive:not(:hover)::after { + opacity: 0.8; + } +} + +*:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +* { + transition-property: background-color, border-color, color, fill, stroke; + transition-duration: 0s; + transition-timing-function: ease; +} + +body.theme-transitioning, +body.theme-transitioning * { + transition-duration: 0.3s !important; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; +} + +.status-badge-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.status-badge-success .status-badge-dot { background: #22c55e; } +.status-badge-warning .status-badge-dot { background: #f59e0b; } +.status-badge-danger .status-badge-dot { background: #ef4444; } +.status-badge-info .status-badge-dot { background: #3b82f6; } + +.bucket-list-item { + display: flex; + align-items: center; + padding: 1rem 1.25rem; + background: var(--myfsio-card-bg); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.75rem; + transition: all 0.2s ease; + gap: 1rem; +} + +.bucket-list-item:hover { + border-color: rgba(59, 130, 246, 0.3); + background: var(--myfsio-hover-bg); +} + +.text-gradient { + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.shadow-glow { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); +} + +.border-gradient { + border: 2px solid transparent; + background: linear-gradient(var(--myfsio-card-bg), var(--myfsio-card-bg)) padding-box, linear-gradient(135deg, #3b82f6, #8b5cf6) border-box; +} + +#objects-table .dropdown-menu { + position: fixed !important; + z-index: 1050; +} + +.btn-icon.dropdown-toggle::after { + display: none; +} + +.floating-upload-progress { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 1055; + min-width: 320px; + max-width: 400px; + animation: slideInUp 0.3s ease-out; +} + +@keyframes slideInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.floating-upload-content { + background: var(--myfsio-card-bg); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.75rem; + padding: 1rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1); +} + +[data-theme='dark'] .floating-upload-content { + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3); +} + +.objects-header-responsive { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.objects-header-responsive > .header-title { + flex: 0 0 auto; +} + +.objects-header-responsive > .header-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + flex: 1; + justify-content: flex-end; +} + +@media (max-width: 640px) { + .objects-header-responsive { + flex-direction: column; + align-items: stretch; + } + + .objects-header-responsive > .header-title { + margin-bottom: 0.5rem; + } + + .objects-header-responsive > .header-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + } + + .objects-header-responsive > .header-actions .btn { + justify-content: center; + } + + .objects-header-responsive > .header-actions .search-wrapper { + grid-column: span 2; + } + + .objects-header-responsive > .header-actions .search-wrapper input { + max-width: 100% !important; + width: 100%; + } + + .objects-header-responsive > .header-actions .bulk-actions { + grid-column: span 2; + display: flex; + gap: 0.5rem; + } + + .objects-header-responsive > .header-actions .bulk-actions .btn { + flex: 1; + } +} + +.modal { + z-index: 1055; +} + +.modal-backdrop { + z-index: 1050; +} + +@media (min-width: 992px) { + .toast-container { + right: 1.5rem; + } +} + +@media (min-width: 992px) { + .floating-upload-progress { + right: 1.5rem; + } +} + +.sidebar-body::-webkit-scrollbar { + width: 4px; +} + +.sidebar-body::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; +} + +.sidebar-body::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +@keyframes sidebarLinkPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } + 50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0); } +} + +.sidebar-link.active { + animation: sidebarLinkPulse 2s ease-in-out infinite; +} + +.offcanvas-backdrop.show { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +body:has(.login-card) .sidebar, +body:has(.login-card) .mobile-header { + display: none !important; +} + +body:has(.login-card) .main-wrapper { + margin-left: 0 !important; + padding-top: 0 !important; +} + +.context-menu { + position: fixed; + z-index: 1060; + min-width: 180px; + background: var(--myfsio-card-bg); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.5rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + padding: 0.25rem 0; + font-size: 0.875rem; +} + +[data-theme='dark'] .context-menu { + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3); +} + +.context-menu-item { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.5rem 0.875rem; + color: var(--myfsio-text); + cursor: pointer; + transition: background-color 0.1s ease; + border: none; + background: none; + width: 100%; + text-align: left; + font-size: inherit; +} + +.context-menu-item:hover { + background-color: var(--myfsio-hover-bg); +} + +.context-menu-item.text-danger:hover { + background-color: rgba(239, 68, 68, 0.1); +} + +.context-menu-divider { + height: 1px; + background: var(--myfsio-card-border); + margin: 0.25rem 0; +} + +.context-menu-shortcut { + margin-left: auto; + font-size: 0.75rem; + color: var(--myfsio-muted); +} + +.kbd-shortcuts-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.kbd-shortcuts-list .shortcut-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0; +} + +.kbd-shortcuts-list kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.75rem; + padding: 0.2rem 0.5rem; + font-family: inherit; + font-size: 0.75rem; + font-weight: 600; + background: var(--myfsio-preview-bg); + border: 1px solid var(--myfsio-card-border); + border-radius: 0.25rem; + box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.05); + color: var(--myfsio-text); +} + +[data-theme='dark'] .kbd-shortcuts-list kbd { + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.2); +} + +.sort-dropdown .dropdown-item.active, +.sort-dropdown .dropdown-item:active { + background-color: var(--myfsio-hover-bg); + color: var(--myfsio-text); +} + +.sort-dropdown .dropdown-item { + font-size: 0.875rem; + padding: 0.375rem 1rem; +} + +.tag-pill { + display: inline-flex; + border-radius: 9999px; + border: 1px solid var(--myfsio-tag-border); + overflow: hidden; + font-size: 0.75rem; + line-height: 1; +} + +.tag-pill-key { + padding: 0.3rem 0.5rem; + background: var(--myfsio-tag-key-bg); + color: var(--myfsio-tag-key-text); + font-weight: 600; +} + +.tag-pill-value { + padding: 0.3rem 0.5rem; + background: var(--myfsio-tag-value-bg); + color: var(--myfsio-tag-value-text); + font-weight: 400; +} + +.tag-editor-card { + background: var(--myfsio-preview-bg); + border-radius: 0.5rem; + padding: 0.75rem; +} + +.tag-editor-header, +.tag-editor-row { + display: grid; + grid-template-columns: 1fr 1fr 28px; + gap: 0.5rem; + align-items: center; +} + +.tag-editor-header { + padding-bottom: 0.375rem; + border-bottom: 1px solid var(--myfsio-card-border); + margin-bottom: 0.5rem; +} + +.tag-editor-header span { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + color: var(--myfsio-muted); + letter-spacing: 0.05em; +} + +.tag-editor-row { + margin-bottom: 0.375rem; +} + +.tag-editor-delete { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + color: var(--myfsio-muted); + border-radius: 0.375rem; + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.tag-editor-delete:hover { + color: var(--myfsio-tag-delete-hover); + background: rgba(239, 68, 68, 0.1); +} + +.tag-editor-actions { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.75rem; + padding-top: 0.5rem; + border-top: 1px solid var(--myfsio-card-border); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +@media print { + .sidebar, + .mobile-header { + display: none !important; + } + + .main-wrapper { + margin-left: 0 !important; + padding-top: 0 !important; + } +} diff --git a/rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.ico b/rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.ico new file mode 100644 index 0000000000000000000000000000000000000000..58ea56f1053f0b1e6fd93cad11911931e7e25ebe GIT binary patch literal 205086 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 literal 0 HcmV?d00001 diff --git a/rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.png b/rust/myfsio-engine/crates/myfsio-server/static/images/MyFSIO.png new file mode 100644 index 0000000000000000000000000000000000000000..e425febdc6f82560d71ceca7fd7cebdfea266402 GIT binary patch literal 893198 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 z

+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

bVfzDG#|v#VfkF& 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 sM0x3V_q{%JE_Rd2>guFtQNlm|3vr2 { + if (!Number.isFinite(bytes)) return `${bytes} bytes`; + const units = ['bytes', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; + }, + escapeHtml: (value) => { + if (value === null || value === undefined) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }, + fallbackCopy: () => false, + setupJsonAutoIndent: () => { } + }; + + setupJsonAutoIndent(document.getElementById('policyDocument')); + + const getFileTypeIcon = (key) => { + const ext = (key.split('.').pop() || '').toLowerCase(); + const iconMap = { + image: ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'ico', 'bmp', 'tiff', 'tif'], + document: ['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'pages'], + spreadsheet: ['xls', 'xlsx', 'csv', 'ods', 'numbers'], + archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz'], + code: ['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'h', 'hpp', 'cs', 'go', 'rs', 'rb', 'php', 'html', 'htm', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'md', 'sh', 'bat', 'ps1', 'sql'], + audio: ['mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a', 'wma', 'aiff'], + video: ['mp4', 'avi', 'mov', 'mkv', 'webm', 'wmv', 'flv', 'm4v', 'mpeg', 'mpg'], + }; + const icons = { + image: ` + + + `, + document: ` + + + `, + spreadsheet: ` + + `, + archive: ` + + + `, + code: ` + + + `, + audio: ` + + + + `, + video: ` + + `, + default: ` + + `, + }; + for (const [type, extensions] of Object.entries(iconMap)) { + if (extensions.includes(ext)) { + return icons[type]; + } + } + return icons.default; + }; + + const selectAllCheckbox = document.querySelector('[data-select-all]'); + const bulkDeleteButton = document.querySelector('[data-bulk-delete-trigger]'); + const bulkDeleteLabel = bulkDeleteButton?.querySelector('[data-bulk-delete-label]'); + const bulkDeleteModalEl = document.getElementById('bulkDeleteModal'); + const bulkDeleteModal = bulkDeleteModalEl ? new bootstrap.Modal(bulkDeleteModalEl) : null; + const bulkDeleteList = document.getElementById('bulkDeleteList'); + const bulkDeleteCount = document.getElementById('bulkDeleteCount'); + const bulkDeleteStatus = document.getElementById('bulkDeleteStatus'); + const bulkDeleteConfirm = document.getElementById('bulkDeleteConfirm'); + const bulkDeletePurge = document.getElementById('bulkDeletePurge'); + const previewPanel = document.getElementById('preview-panel'); + const previewEmpty = document.getElementById('preview-empty'); + const previewKey = document.getElementById('preview-key'); + const previewSize = document.getElementById('preview-size'); + const previewModified = document.getElementById('preview-modified'); + const previewEtag = document.getElementById('preview-etag'); + const previewMetadata = document.getElementById('preview-metadata'); + const previewMetadataList = document.getElementById('preview-metadata-list'); + const previewPlaceholder = document.getElementById('preview-placeholder'); + const previewPlaceholderDefault = previewPlaceholder ? previewPlaceholder.innerHTML : ''; + const previewErrorAlert = document.getElementById('preview-error-alert'); + const previewDetailsMeta = document.getElementById('preview-details-meta'); + const previewImage = document.getElementById('preview-image'); + const previewVideo = document.getElementById('preview-video'); + const previewAudio = document.getElementById('preview-audio'); + const previewText = document.getElementById('preview-text'); + const previewIframe = document.getElementById('preview-iframe'); + const downloadButton = document.getElementById('downloadButton'); + const presignButton = document.getElementById('presignButton'); + const presignModalEl = document.getElementById('presignModal'); + const presignModal = presignModalEl ? new bootstrap.Modal(presignModalEl) : null; + const presignMethod = document.getElementById('presignMethod'); + const presignTtl = document.getElementById('presignTtl'); + const presignLink = document.getElementById('presignLink'); + const copyPresignLink = document.getElementById('copyPresignLink'); + const copyPresignDefaultLabel = copyPresignLink?.textContent?.trim() || 'Copy'; + const generatePresignButton = document.getElementById('generatePresignButton'); + const policyForm = document.getElementById('bucketPolicyForm'); + const policyTextarea = document.getElementById('policyDocument'); + const policyPreset = document.getElementById('policyPreset'); + const policyMode = document.getElementById('policyMode'); + const uploadForm = document.querySelector('[data-upload-form]'); + const uploadModalEl = document.getElementById('uploadModal'); + const uploadModal = uploadModalEl ? bootstrap.Modal.getOrCreateInstance(uploadModalEl) : null; + const uploadFileInput = uploadForm?.querySelector('input[name="object"]'); + const uploadDropZone = uploadForm?.querySelector('[data-dropzone]'); + const uploadDropZoneLabel = uploadDropZone?.querySelector('[data-dropzone-label]'); + const messageModalEl = document.getElementById('messageModal'); + const messageModal = messageModalEl ? new bootstrap.Modal(messageModalEl) : null; + const messageModalTitle = document.getElementById('messageModalTitle'); + const messageModalBody = document.getElementById('messageModalBody'); + const messageModalAction = document.getElementById('messageModalAction'); + let messageModalActionHandler = null; + let isGeneratingPresign = false; + const objectsContainer = document.querySelector('.objects-table-container[data-bucket]'); + const bulkDeleteEndpoint = objectsContainer?.dataset.bulkDeleteEndpoint || ''; + const objectsApiUrl = objectsContainer?.dataset.objectsApi || ''; + const objectsStreamUrl = objectsContainer?.dataset.objectsStream || ''; + const versionPanel = document.getElementById('version-panel'); + const versionList = document.getElementById('version-list'); + const refreshVersionsButton = document.getElementById('refreshVersionsButton'); + let archivedCard = document.getElementById('archived-objects-card'); + let archivedBody = archivedCard?.querySelector('[data-archived-body]'); + let archivedCountBadge = archivedCard?.querySelector('[data-archived-count]'); + let archivedRefreshButton = archivedCard?.querySelector('[data-archived-refresh]'); + let archivedEndpoint = archivedCard?.dataset.archivedEndpoint; + let versioningEnabled = objectsContainer?.dataset.versioning === 'true'; + const versionsCache = new Map(); + let activeRow = null; + const selectedRows = new Map(); + let bulkDeleting = false; + if (presignButton) presignButton.disabled = true; + if (generatePresignButton) generatePresignButton.disabled = true; + if (downloadButton) downloadButton.classList.add('disabled'); + + const objectCountBadge = document.getElementById('object-count-badge'); + const loadMoreContainer = document.getElementById('load-more-container'); + const loadMoreSpinner = document.getElementById('load-more-spinner'); + const loadMoreStatus = document.getElementById('load-more-status'); + const objectsLoadingRow = document.getElementById('objects-loading-row'); + let nextContinuationToken = null; + let totalObjectCount = 0; + let loadedObjectCount = 0; + let isLoadingObjects = false; + let hasMoreObjects = false; + let currentFilterTerm = ''; + let currentSortField = 'name'; + let currentSortDir = 'asc'; + let pageSize = 5000; + let currentPrefix = ''; + let allObjects = []; + let streamFolders = []; + let useDelimiterMode = true; + let urlTemplates = null; + let streamAbortController = null; + let useStreaming = !!objectsStreamUrl; + let streamingComplete = false; + const STREAM_RENDER_BATCH = 500; + let pendingStreamObjects = []; + let streamRenderScheduled = false; + + const buildUrlFromTemplate = (template, key) => { + if (!template) return ''; + return template.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/')); + }; + + const ROW_HEIGHT = 53; + const BUFFER_ROWS = 10; + let visibleItems = []; + let renderedRange = { start: 0, end: 0 }; + + let memoizedVisibleItems = null; + let memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + + const createObjectRow = (obj, displayKey = null) => { + const tr = document.createElement('tr'); + tr.dataset.objectRow = ''; + tr.dataset.key = obj.key; + tr.dataset.size = obj.size; + tr.dataset.lastModified = obj.lastModified ?? obj.last_modified ?? ''; + tr.dataset.lastModifiedDisplay = obj.lastModifiedDisplay ?? obj.last_modified_display ?? new Date(obj.lastModified || obj.last_modified).toLocaleString(); + tr.dataset.lastModifiedIso = obj.lastModifiedIso ?? obj.last_modified_iso ?? obj.lastModified ?? obj.last_modified ?? ''; + tr.dataset.etag = obj.etag ?? ''; + tr.dataset.previewUrl = obj.previewUrl ?? obj.preview_url ?? ''; + tr.dataset.downloadUrl = obj.downloadUrl ?? obj.download_url ?? ''; + tr.dataset.presignEndpoint = obj.presignEndpoint ?? obj.presign_endpoint ?? ''; + tr.dataset.deleteEndpoint = obj.deleteEndpoint ?? obj.delete_endpoint ?? ''; + tr.dataset.metadataUrl = obj.metadataUrl ?? obj.metadata_url ?? ''; + tr.dataset.versionsEndpoint = obj.versionsEndpoint ?? obj.versions_endpoint ?? ''; + tr.dataset.restoreTemplate = obj.restoreTemplate ?? obj.restore_template ?? ''; + tr.dataset.tagsUrl = obj.tagsUrl ?? obj.tags_url ?? ''; + tr.dataset.copyUrl = obj.copyUrl ?? obj.copy_url ?? ''; + tr.dataset.moveUrl = obj.moveUrl ?? obj.move_url ?? ''; + + const keyToShow = displayKey || obj.key; + const lastModDisplay = obj.lastModifiedDisplay || obj.last_modified_display || new Date(obj.lastModified || obj.last_modified).toLocaleDateString(); + + tr.innerHTML = ` + + + + +
+ ${getFileTypeIcon(obj.key)} + ${escapeHtml(keyToShow)} +
+
Modified ${escapeHtml(lastModDisplay)}
+ + + ${formatBytes(obj.size)} + + +
+ + + + +
+ + `; + + return tr; + }; + + const showEmptyState = () => { + if (!objectsTableBody) return; + objectsTableBody.innerHTML = ` + + +
+
+ + + + +
+
No objects yet
+

Drag and drop files here or click Upload to get started.

+ +
+ + + `; + }; + + const showLoadError = (message) => { + if (!objectsTableBody) return; + objectsTableBody.innerHTML = ` + + +
+ + + +

Failed to load objects

+

${escapeHtml(message)}

+ +
+ + + `; + }; + + let bucketTotalObjects = objectsContainer ? parseInt(objectsContainer.dataset.bucketTotalObjects || '0', 10) : 0; + + const updateObjectCountBadge = () => { + if (!objectCountBadge) return; + if (useDelimiterMode) { + const total = bucketTotalObjects || totalObjectCount; + objectCountBadge.textContent = `${total.toLocaleString()} object${total !== 1 ? 's' : ''}`; + } else { + objectCountBadge.textContent = `${totalObjectCount.toLocaleString()} object${totalObjectCount !== 1 ? 's' : ''}`; + } + }; + + let topSpacer = null; + let bottomSpacer = null; + + const initVirtualScrollElements = () => { + if (!objectsTableBody) return; + + if (!topSpacer) { + topSpacer = document.createElement('tr'); + topSpacer.id = 'virtual-top-spacer'; + topSpacer.innerHTML = ''; + } + if (!bottomSpacer) { + bottomSpacer = document.createElement('tr'); + bottomSpacer.id = 'virtual-bottom-spacer'; + bottomSpacer.innerHTML = ''; + } + }; + + const computeVisibleItems = (forceRecompute = false) => { + const currentInputs = { + objectCount: allObjects.length, + folderCount: streamFolders.length, + prefix: currentPrefix, + filterTerm: currentFilterTerm, + sortField: currentSortField, + sortDir: currentSortDir + }; + + if (!forceRecompute && + memoizedVisibleItems !== null && + memoizedInputs.objectCount === currentInputs.objectCount && + memoizedInputs.folderCount === currentInputs.folderCount && + memoizedInputs.prefix === currentInputs.prefix && + memoizedInputs.filterTerm === currentInputs.filterTerm && + memoizedInputs.sortField === currentInputs.sortField && + memoizedInputs.sortDir === currentInputs.sortDir) { + return memoizedVisibleItems; + } + + const items = []; + + if (searchResults !== null) { + searchResults.forEach(obj => { + items.push({ type: 'file', data: obj, displayKey: obj.key }); + }); + } else if (useDelimiterMode && streamFolders.length > 0) { + streamFolders.forEach(folderPath => { + const folderName = folderPath.slice(currentPrefix.length).replace(/\/$/, ''); + items.push({ type: 'folder', path: folderPath, displayKey: folderName }); + }); + allObjects.forEach(obj => { + const remainder = obj.key.slice(currentPrefix.length); + if (!remainder) return; + items.push({ type: 'file', data: obj, displayKey: remainder }); + }); + } else { + const folders = new Set(); + + allObjects.forEach(obj => { + if (!obj.key.startsWith(currentPrefix)) return; + + const remainder = obj.key.slice(currentPrefix.length); + + if (!remainder) return; + + const isFolderMarker = obj.key.endsWith('/') && obj.size === 0; + const slashIndex = remainder.indexOf('/'); + + if (slashIndex === -1 && !isFolderMarker) { + items.push({ type: 'file', data: obj, displayKey: remainder }); + } else { + const effectiveSlashIndex = isFolderMarker && slashIndex === remainder.length - 1 + ? slashIndex + : (slashIndex === -1 ? remainder.length - 1 : slashIndex); + const folderName = remainder.slice(0, effectiveSlashIndex); + const folderPath = currentPrefix + folderName + '/'; + if (!folders.has(folderPath)) { + folders.add(folderPath); + items.push({ type: 'folder', path: folderPath, displayKey: folderName }); + } + } + }); + } + + items.sort((a, b) => { + if (a.type === 'folder' && b.type === 'file') return -1; + if (a.type === 'file' && b.type === 'folder') return 1; + if (a.type === 'folder' && b.type === 'folder') { + return a.path.localeCompare(b.path); + } + const dir = currentSortDir === 'asc' ? 1 : -1; + if (currentSortField === 'size') { + return (a.data.size - b.data.size) * dir; + } + if (currentSortField === 'date') { + const aTime = new Date(a.data.lastModified || a.data.last_modified || 0).getTime(); + const bTime = new Date(b.data.lastModified || b.data.last_modified || 0).getTime(); + return (aTime - bTime) * dir; + } + return a.data.key.localeCompare(b.data.key) * dir; + }); + + memoizedVisibleItems = items; + memoizedInputs = currentInputs; + return items; + }; + + const renderVirtualRows = () => { + if (!objectsTableBody || !scrollContainer) return; + + const containerHeight = scrollContainer.clientHeight; + const scrollTop = scrollContainer.scrollTop; + + const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER_ROWS); + const endIndex = Math.min(visibleItems.length, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + BUFFER_ROWS); + + if (startIndex === renderedRange.start && endIndex === renderedRange.end) return; + + renderedRange = { start: startIndex, end: endIndex }; + + objectsTableBody.innerHTML = ''; + + initVirtualScrollElements(); + topSpacer.querySelector('td').style.height = `${startIndex * ROW_HEIGHT}px`; + objectsTableBody.appendChild(topSpacer); + + for (let i = startIndex; i < endIndex; i++) { + const item = visibleItems[i]; + if (!item) continue; + + let row; + if (item.type === 'folder') { + row = createFolderRow(item.path, item.displayKey); + } else { + row = createObjectRow(item.data, item.displayKey); + } + row.dataset.virtualIndex = i; + objectsTableBody.appendChild(row); + } + + const remainingRows = visibleItems.length - endIndex; + bottomSpacer.querySelector('td').style.height = `${remainingRows * ROW_HEIGHT}px`; + objectsTableBody.appendChild(bottomSpacer); + + attachRowHandlers(); + }; + + let scrollTimeout = null; + const handleVirtualScroll = () => { + if (scrollTimeout) cancelAnimationFrame(scrollTimeout); + scrollTimeout = requestAnimationFrame(renderVirtualRows); + }; + + const refreshVirtualList = () => { + visibleItems = computeVisibleItems(); + renderedRange = { start: -1, end: -1 }; + + if (visibleItems.length === 0) { + if (allObjects.length === 0 && streamFolders.length === 0 && !hasMoreObjects) { + showEmptyState(); + } else { + objectsTableBody.innerHTML = ` + + +
+
+ + + +
+
Empty folder
+

This folder contains no objects${hasMoreObjects ? ' yet. Loading more...' : '.'}

+
+ + + `; + } + } else { + renderVirtualRows(); + } + + updateFolderViewStatus(); + }; + + const updateFolderViewStatus = () => { + const folderViewStatusEl = document.getElementById('folder-view-status'); + if (!folderViewStatusEl) return; + folderViewStatusEl.classList.add('d-none'); + }; + + const processStreamObject = (obj) => { + const key = obj.key; + return { + key: key, + size: obj.size, + lastModified: obj.last_modified, + lastModifiedDisplay: obj.last_modified_display, + lastModifiedIso: obj.last_modified_iso, + etag: obj.etag, + previewUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.preview, key) : '', + downloadUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.download, key) : '', + presignEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.presign, key) : '', + deleteEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.delete, key) : '', + metadataUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.metadata, key) : '', + versionsEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.versions, key) : '', + restoreTemplate: urlTemplates ? urlTemplates.restore.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/')) : '', + tagsUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.tags, key) : '', + copyUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.copy, key) : '', + moveUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.move, key) : '' + }; + }; + + let lastStreamRenderTime = 0; + const STREAM_RENDER_THROTTLE_MS = 500; + + const buildBottomStatusText = (complete) => { + if (!complete) { + const countText = totalObjectCount > 0 ? ` of ${totalObjectCount.toLocaleString()}` : ''; + return `${loadedObjectCount.toLocaleString()}${countText} loading...`; + } + const parts = []; + if (useDelimiterMode && streamFolders.length > 0) { + parts.push(`${streamFolders.length.toLocaleString()} folder${streamFolders.length !== 1 ? 's' : ''}`); + } + parts.push(`${loadedObjectCount.toLocaleString()} object${loadedObjectCount !== 1 ? 's' : ''}`); + return parts.join(', '); + }; + + const flushPendingStreamObjects = () => { + if (pendingStreamObjects.length > 0) { + const batch = pendingStreamObjects.splice(0, pendingStreamObjects.length); + batch.forEach(obj => { + loadedObjectCount++; + allObjects.push(obj); + }); + } + updateObjectCountBadge(); + if (loadMoreStatus) { + loadMoreStatus.textContent = buildBottomStatusText(streamingComplete); + } + if (objectsLoadingRow && objectsLoadingRow.parentNode) { + const loadingText = objectsLoadingRow.querySelector('p'); + if (loadingText) { + const countText = totalObjectCount > 0 ? ` of ${totalObjectCount.toLocaleString()}` : ''; + loadingText.textContent = `Loading ${loadedObjectCount.toLocaleString()}${countText} objects...`; + } + } + const now = performance.now(); + if (!streamingComplete && now - lastStreamRenderTime < STREAM_RENDER_THROTTLE_MS) { + streamRenderScheduled = false; + return; + } + lastStreamRenderTime = now; + refreshVirtualList(); + streamRenderScheduled = false; + }; + + const scheduleStreamRender = () => { + if (streamRenderScheduled) return; + streamRenderScheduled = true; + requestAnimationFrame(flushPendingStreamObjects); + }; + + const loadObjectsStreaming = async () => { + if (isLoadingObjects) return; + isLoadingObjects = true; + streamingComplete = false; + + if (objectsLoadingRow) objectsLoadingRow.style.display = ''; + nextContinuationToken = null; + loadedObjectCount = 0; + totalObjectCount = 0; + allObjects = []; + streamFolders = []; + memoizedVisibleItems = null; + memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + pendingStreamObjects = []; + lastStreamRenderTime = 0; + + streamAbortController = new AbortController(); + + try { + const params = new URLSearchParams(); + if (currentPrefix) params.set('prefix', currentPrefix); + if (useDelimiterMode) params.set('delimiter', '/'); + + const response = await fetch(`${objectsStreamUrl}?${params}`, { + signal: streamAbortController.signal + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + if (objectsLoadingRow) { + const loadingText = objectsLoadingRow.querySelector('p'); + if (loadingText) loadingText.textContent = 'Receiving objects...'; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + switch (msg.type) { + case 'meta': + urlTemplates = msg.url_templates; + versioningEnabled = msg.versioning_enabled; + if (objectsContainer) { + objectsContainer.dataset.versioning = versioningEnabled ? 'true' : 'false'; + } + break; + case 'count': + totalObjectCount = msg.total_count || 0; + if (objectsLoadingRow) { + const loadingText = objectsLoadingRow.querySelector('p'); + if (loadingText) loadingText.textContent = `Loading 0 of ${totalObjectCount.toLocaleString()} objects...`; + } + break; + case 'folder': + streamFolders.push(msg.prefix); + scheduleStreamRender(); + break; + case 'object': + pendingStreamObjects.push(processStreamObject(msg)); + if (pendingStreamObjects.length >= STREAM_RENDER_BATCH) { + scheduleStreamRender(); + } + break; + case 'error': + throw new Error(msg.error); + case 'done': + streamingComplete = true; + break; + } + } catch (parseErr) { + console.warn('Failed to parse stream line:', line, parseErr); + } + } + if (pendingStreamObjects.length > 0) { + scheduleStreamRender(); + } + } + + if (buffer.trim()) { + try { + const msg = JSON.parse(buffer); + if (msg.type === 'object') { + pendingStreamObjects.push(processStreamObject(msg)); + } else if (msg.type === 'done') { + streamingComplete = true; + } + } catch (e) { } + } + + streamingComplete = true; + flushPendingStreamObjects(); + hasMoreObjects = false; + totalObjectCount = loadedObjectCount; + if (!currentPrefix && !useDelimiterMode) bucketTotalObjects = totalObjectCount; + updateObjectCountBadge(); + + if (objectsLoadingRow && objectsLoadingRow.parentNode) { + objectsLoadingRow.remove(); + } + + if (loadMoreStatus) { + loadMoreStatus.textContent = buildBottomStatusText(true); + } + refreshVirtualList(); + renderBreadcrumb(currentPrefix); + + } catch (error) { + if (error.name === 'AbortError') return; + console.error('Streaming failed, falling back to paginated:', error); + useStreaming = false; + isLoadingObjects = false; + await loadObjectsPaginated(false); + return; + } finally { + isLoadingObjects = false; + streamAbortController = null; + } + }; + + const loadObjectsPaginated = async (append = false) => { + if (isLoadingObjects) return; + isLoadingObjects = true; + + if (!append) { + if (objectsLoadingRow) objectsLoadingRow.style.display = ''; + nextContinuationToken = null; + loadedObjectCount = 0; + totalObjectCount = 0; + allObjects = []; + streamFolders = []; + memoizedVisibleItems = null; + memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + } + + if (append && loadMoreSpinner) { + loadMoreSpinner.classList.remove('d-none'); + } + + try { + const params = new URLSearchParams({ max_keys: String(pageSize) }); + if (nextContinuationToken) { + params.set('continuation_token', nextContinuationToken); + } + + const response = await fetch(`${objectsApiUrl}?${params}`); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || `HTTP ${response.status}`); + } + + const data = await response.json(); + + versioningEnabled = data.versioning_enabled; + if (objectsContainer) { + objectsContainer.dataset.versioning = versioningEnabled ? 'true' : 'false'; + } + + totalObjectCount = data.total_count || 0; + if (!append && !currentPrefix && !useDelimiterMode) bucketTotalObjects = totalObjectCount; + nextContinuationToken = data.next_continuation_token; + + if (!append && objectsLoadingRow) { + objectsLoadingRow.remove(); + } + + if (data.url_templates && !urlTemplates) { + urlTemplates = data.url_templates; + } + + data.objects.forEach(obj => { + loadedObjectCount++; + allObjects.push(processStreamObject(obj)); + }); + + updateObjectCountBadge(); + hasMoreObjects = data.is_truncated; + + if (loadMoreStatus) { + if (data.is_truncated) { + loadMoreStatus.textContent = `${loadedObjectCount.toLocaleString()} of ${totalObjectCount.toLocaleString()} loaded`; + } else { + loadMoreStatus.textContent = `${loadedObjectCount.toLocaleString()} objects`; + } + } + + refreshVirtualList(); + renderBreadcrumb(currentPrefix); + + } catch (error) { + console.error('Failed to load objects:', error); + if (!append) { + showLoadError(error.message); + } else { + showMessage({ title: 'Load Failed', body: error.message, variant: 'danger' }); + } + } finally { + isLoadingObjects = false; + if (loadMoreSpinner) { + loadMoreSpinner.classList.add('d-none'); + } + } + }; + + const loadObjects = async (append = false) => { + if (useStreaming && !append) { + return loadObjectsStreaming(); + } + return loadObjectsPaginated(append); + }; + + const attachRowHandlers = () => { + const objectRows = document.querySelectorAll('[data-object-row]'); + objectRows.forEach(row => { + if (row.dataset.handlersAttached) return; + row.dataset.handlersAttached = 'true'; + + const deleteBtn = row.querySelector('[data-delete-object]'); + deleteBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + const deleteModalEl = document.getElementById('deleteObjectModal'); + const deleteModal = deleteModalEl ? bootstrap.Modal.getOrCreateInstance(deleteModalEl) : null; + const deleteObjectForm = document.getElementById('deleteObjectForm'); + const deleteObjectKey = document.getElementById('deleteObjectKey'); + if (deleteModal && deleteObjectForm) { + deleteObjectForm.setAttribute('action', row.dataset.deleteEndpoint); + if (deleteObjectKey) deleteObjectKey.textContent = row.dataset.key; + deleteModal.show(); + } + }); + + const selectCheckbox = row.querySelector('[data-object-select]'); + selectCheckbox?.addEventListener('click', (event) => event.stopPropagation()); + selectCheckbox?.addEventListener('change', () => { + toggleRowSelection(row, selectCheckbox.checked); + }); + + if (selectedRows.has(row.dataset.key)) { + selectCheckbox.checked = true; + row.classList.add('table-active'); + } + + if (activeRow && activeRow.dataset.key === row.dataset.key) { + row.classList.add('table-active'); + activeRow = row; + } + }); + + const folderRows = document.querySelectorAll('.folder-row'); + folderRows.forEach(row => { + if (row.dataset.handlersAttached) return; + row.dataset.handlersAttached = 'true'; + + const folderPath = row.dataset.folderPath; + + const checkbox = row.querySelector('[data-folder-select]'); + checkbox?.addEventListener('change', (e) => { + e.stopPropagation(); + if (checkbox.checked) { + selectedRows.set(folderPath, { key: folderPath, isFolder: true }); + } else { + selectedRows.delete(folderPath); + } + const folderObjects = allObjects.filter(obj => obj.key.startsWith(folderPath)); + folderObjects.forEach(obj => { + if (checkbox.checked) { + selectedRows.set(obj.key, obj); + } else { + selectedRows.delete(obj.key); + } + }); + updateBulkDeleteState(); + }); + + const folderBtn = row.querySelector('button'); + folderBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + navigateToFolder(folderPath); + }); + + row.addEventListener('click', (e) => { + if (e.target.closest('[data-folder-select]') || e.target.closest('button')) return; + navigateToFolder(folderPath); + }); + }); + + updateBulkDeleteState(); + }; + + const scrollSentinel = document.getElementById('scroll-sentinel'); + const scrollContainer = document.querySelector('.objects-table-container'); + + if (scrollContainer) { + scrollContainer.addEventListener('scroll', handleVirtualScroll, { passive: true }); + } + + if (scrollSentinel && scrollContainer) { + const containerObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && hasMoreObjects && !isLoadingObjects) { + loadObjects(true); + } + }); + }, { + root: scrollContainer, + rootMargin: '500px', + threshold: 0 + }); + containerObserver.observe(scrollSentinel); + + const viewportObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && hasMoreObjects && !isLoadingObjects) { + loadObjects(true); + } + }); + }, { + root: null, + rootMargin: '500px', + threshold: 0 + }); + viewportObserver.observe(scrollSentinel); + } + + + if (objectsApiUrl) { + loadObjects(); + } + + const folderBreadcrumb = document.getElementById('folder-breadcrumb'); + const objectsTableBody = document.querySelector('#objects-table tbody'); + + if (objectsTableBody) { + objectsTableBody.addEventListener('click', (e) => { + const row = e.target.closest('[data-object-row]'); + if (!row) return; + + if (e.target.closest('[data-delete-object]') || e.target.closest('[data-object-select]') || e.target.closest('a') || e.target.closest('.dropdown')) { + return; + } + + selectRow(row); + }); + } + + const hasFolders = () => streamFolders.length > 0 || allObjects.some(obj => obj.key.includes('/')); + + const getFoldersAtPrefix = (prefix) => { + const folders = new Set(); + const files = []; + + allObjects.forEach(obj => { + const key = obj.key; + if (!key.startsWith(prefix)) return; + + const remainder = key.slice(prefix.length); + const slashIndex = remainder.indexOf('/'); + + if (slashIndex === -1) { + + files.push(obj); + } else { + + const folderName = remainder.slice(0, slashIndex + 1); + folders.add(prefix + folderName); + } + }); + + return { folders: Array.from(folders).sort(), files }; + }; + + const countObjectsInFolder = (folderPrefix) => { + if (useDelimiterMode) { + return { count: 0, mayHaveMore: true }; + } + const count = allObjects.filter(obj => obj.key.startsWith(folderPrefix)).length; + return { count, mayHaveMore: hasMoreObjects }; + }; + + const renderBreadcrumb = (prefix) => { + if (!folderBreadcrumb) return; + + if (!prefix && !hasFolders()) { + folderBreadcrumb.classList.add('d-none'); + return; + } + + folderBreadcrumb.classList.remove('d-none'); + const ol = folderBreadcrumb.querySelector('ol'); + ol.innerHTML = ''; + + const rootLi = document.createElement('li'); + rootLi.className = 'breadcrumb-item'; + if (!prefix) { + rootLi.classList.add('active'); + rootLi.setAttribute('aria-current', 'page'); + rootLi.innerHTML = ` + + + + Root + `; + } else { + rootLi.innerHTML = ` + + + + + Root + + `; + } + ol.appendChild(rootLi); + + if (prefix) { + const parts = prefix.split('/').filter(Boolean); + let accumulated = ''; + parts.forEach((part, index) => { + accumulated += part + '/'; + const li = document.createElement('li'); + li.className = 'breadcrumb-item'; + + if (index === parts.length - 1) { + li.classList.add('active'); + li.setAttribute('aria-current', 'page'); + li.textContent = part; + } else { + const a = document.createElement('a'); + a.href = '#'; + a.className = 'text-decoration-none'; + a.dataset.folderNav = accumulated; + a.textContent = part; + li.appendChild(a); + } + ol.appendChild(li); + }); + } + + ol.querySelectorAll('[data-folder-nav]').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + navigateToFolder(link.dataset.folderNav); + }); + }); + }; + + const getObjectsInFolder = (folderPrefix) => { + return allObjects.filter(obj => obj.key.startsWith(folderPrefix)); + }; + + const createFolderRow = (folderPath, displayName = null) => { + const folderName = displayName || folderPath.slice(currentPrefix.length).replace(/\/$/, ''); + const { count: objectCount, mayHaveMore } = countObjectsInFolder(folderPath); + let countLine = ''; + if (useDelimiterMode) { + countLine = ''; + } else { + const countDisplay = mayHaveMore ? `${objectCount}+` : objectCount; + countLine = `
${countDisplay} object${objectCount !== 1 ? 's' : ''}
`; + } + + const tr = document.createElement('tr'); + tr.className = 'folder-row'; + tr.dataset.folderPath = folderPath; + tr.style.cursor = 'pointer'; + + tr.innerHTML = ` + + + + +
+ + + + ${escapeHtml(folderName)}/ +
+ ${countLine} + + + + + + + + `; + + return tr; + }; + + const navigateToFolder = (prefix) => { + if (streamAbortController) { + streamAbortController.abort(); + streamAbortController = null; + } + + currentPrefix = prefix; + + if (scrollContainer) scrollContainer.scrollTop = 0; + + selectedRows.clear(); + + if (typeof updateBulkDeleteState === 'function') { + updateBulkDeleteState(); + } + + if (previewPanel) previewPanel.classList.add('d-none'); + if (previewEmpty) previewEmpty.classList.remove('d-none'); + activeRow = null; + + isLoadingObjects = false; + loadObjects(false); + }; + + const renderObjectsView = () => { + if (!objectsTableBody) return; + + const { folders, files } = getFoldersAtPrefix(currentPrefix); + + objectsTableBody.innerHTML = ''; + + folders.forEach(folderPath => { + objectsTableBody.appendChild(createFolderRow(folderPath)); + }); + + files.forEach(obj => { + objectsTableBody.appendChild(obj.element); + obj.element.style.display = ''; + + const keyCell = obj.element.querySelector('.object-key .fw-medium'); + if (keyCell && currentPrefix) { + const displayName = obj.key.slice(currentPrefix.length); + keyCell.textContent = displayName; + keyCell.closest('.object-key').title = obj.key; + } else if (keyCell) { + keyCell.textContent = obj.key; + } + }); + + allObjects.forEach(obj => { + if (!files.includes(obj)) { + obj.element.style.display = 'none'; + } + }); + + if (folders.length === 0 && files.length === 0) { + const emptyRow = document.createElement('tr'); + emptyRow.innerHTML = ` + +
+
+ + + +
+
Empty folder
+

This folder contains no objects.

+
+ + `; + objectsTableBody.appendChild(emptyRow); + } + + if (typeof updateBulkDeleteState === 'function') { + updateBulkDeleteState(); + } + }; + + const showMessage = ({ title = 'Notice', body = '', bodyHtml = null, variant = 'info', actionText = null, onAction = null }) => { + if (!actionText && !onAction && window.showToast) { + window.showToast(body || title, title, variant); + return; + } + if (!messageModal) { + window.alert(body || title); + return; + } + document.querySelectorAll('.modal.show').forEach(modal => { + const instance = bootstrap.Modal.getInstance(modal); + if (instance && modal.id !== 'messageModal') { + instance.hide(); + } + }); + const iconEl = document.getElementById('messageModalIcon'); + if (iconEl) { + const iconPaths = { + success: '', + danger: '', + warning: '', + info: '' + }; + const iconColors = { success: 'text-success', danger: 'text-danger', warning: 'text-warning', info: 'text-primary' }; + iconEl.innerHTML = iconPaths[variant] || iconPaths.info; + iconEl.classList.remove('text-success', 'text-danger', 'text-warning', 'text-primary'); + iconEl.classList.add(iconColors[variant] || 'text-primary'); + } + messageModalTitle.textContent = title; + if (bodyHtml) { + messageModalBody.innerHTML = bodyHtml; + } else { + messageModalBody.textContent = body; + } + messageModalActionHandler = null; + const variantClass = { + success: 'btn-success', + danger: 'btn-danger', + warning: 'btn-warning', + info: 'btn-primary', + }; + Object.values(variantClass).forEach((cls) => messageModalAction.classList.remove(cls)); + if (actionText && typeof onAction === 'function') { + messageModalAction.textContent = actionText; + messageModalAction.classList.remove('d-none'); + messageModalAction.classList.add(variantClass[variant] || 'btn-primary'); + messageModalActionHandler = onAction; + } else { + messageModalAction.classList.add('d-none'); + } + setTimeout(() => messageModal.show(), 150); + }; + + messageModalAction?.addEventListener('click', () => { + if (typeof messageModalActionHandler === 'function') { + messageModalActionHandler(); + } + messageModal?.hide(); + }); + + messageModalEl?.addEventListener('hidden.bs.modal', () => { + messageModalActionHandler = null; + messageModalAction.classList.add('d-none'); + }); + + const normalizePolicyTemplate = (rawTemplate) => { + if (!rawTemplate) { + return ''; + } + try { + let parsed = JSON.parse(rawTemplate); + if (typeof parsed === 'string') { + parsed = JSON.parse(parsed); + } + return JSON.stringify(parsed, null, 2); + } catch { + return rawTemplate; + } + }; + + let publicPolicyTemplate = normalizePolicyTemplate(policyTextarea?.dataset.publicTemplate || ''); + let customPolicyDraft = policyTextarea?.value || ''; + const policyReadonlyHint = document.getElementById('policyReadonlyHint'); + const presetButtons = Array.from(document.querySelectorAll('.preset-btn[data-preset]')); + + const setActivePolicyPreset = (preset) => { + if (policyPreset) { + policyPreset.value = preset; + } + presetButtons.forEach(button => { + button.classList.toggle('active', button.dataset.preset === preset); + }); + }; + + const setPolicyTextareaState = (readonly) => { + if (!policyTextarea) return; + if (readonly) { + policyTextarea.setAttribute('readonly', 'readonly'); + policyTextarea.classList.add('bg-body-secondary'); + policyTextarea.classList.add('policy-editor-disabled'); + policyTextarea.setAttribute('aria-disabled', 'true'); + } else { + policyTextarea.removeAttribute('readonly'); + policyTextarea.classList.remove('bg-body-secondary'); + policyTextarea.classList.remove('policy-editor-disabled'); + policyTextarea.removeAttribute('aria-disabled'); + } + }; + + const applyPolicyPreset = (preset) => { + if (!policyTextarea || !policyMode) return; + const isPresetMode = preset === 'private' || preset === 'public'; + if (policyReadonlyHint) { + policyReadonlyHint.classList.toggle('d-none', !isPresetMode); + } + switch (preset) { + case 'private': + setPolicyTextareaState(true); + policyTextarea.value = ''; + policyMode.value = 'delete'; + break; + case 'public': + setPolicyTextareaState(true); + policyTextarea.value = publicPolicyTemplate || ''; + policyMode.value = 'upsert'; + break; + default: + setPolicyTextareaState(false); + policyTextarea.value = customPolicyDraft; + policyMode.value = 'upsert'; + break; + } + }; + + policyTextarea?.addEventListener('input', () => { + if (policyPreset?.value === 'custom') { + customPolicyDraft = policyTextarea.value; + } + }); + + presetButtons.forEach(btn => { + btn.addEventListener('click', () => { + setActivePolicyPreset(btn.dataset.preset); + applyPolicyPreset(btn.dataset.preset); + }); + }); + + if (policyPreset) { + setActivePolicyPreset(policyPreset.value || policyPreset.dataset.default || 'custom'); + applyPolicyPreset(policyPreset.value || policyPreset.dataset.default || 'custom'); + } + + policyForm?.addEventListener('submit', () => { + if (!policyMode || !policyPreset || !policyTextarea) { + return; + } + if (policyPreset.value === 'private') { + policyMode.value = 'delete'; + policyTextarea.value = ''; + } else if (policyPreset.value === 'public') { + policyMode.value = 'upsert'; + policyTextarea.value = publicPolicyTemplate || policyTextarea.value; + } else { + policyMode.value = 'upsert'; + } + }); + + const bulkActionsWrapper = document.getElementById('bulk-actions-wrapper'); + const updateBulkDeleteState = () => { + const selectedCount = selectedRows.size; + if (bulkDeleteButton) { + const shouldShow = Boolean(bulkDeleteEndpoint) && (selectedCount > 0 || bulkDeleting); + bulkDeleteButton.disabled = !bulkDeleteEndpoint || selectedCount === 0 || bulkDeleting; + if (bulkDeleteLabel) { + bulkDeleteLabel.textContent = selectedCount ? `Delete (${selectedCount})` : 'Delete'; + } + if (bulkActionsWrapper) { + bulkActionsWrapper.classList.toggle('d-none', !shouldShow); + } + } + if (bulkDeleteConfirm) { + bulkDeleteConfirm.disabled = selectedCount === 0 || bulkDeleting; + } + if (selectAllCheckbox) { + const filesInView = visibleItems.filter(item => item.type === 'file'); + const foldersInView = visibleItems.filter(item => item.type === 'folder'); + const total = filesInView.length + foldersInView.length; + const fileSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length; + const folderSelectedCount = foldersInView.filter(item => selectedRows.has(item.path)).length; + const visibleSelectedCount = fileSelectedCount + folderSelectedCount; + selectAllCheckbox.disabled = total === 0; + selectAllCheckbox.checked = visibleSelectedCount > 0 && visibleSelectedCount === total && total > 0; + selectAllCheckbox.indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < total; + } + }; + + function toggleRowSelection(row, shouldSelect) { + if (!row || !row.dataset.key) return; + if (shouldSelect) { + selectedRows.set(row.dataset.key, row); + } else { + selectedRows.delete(row.dataset.key); + } + updateBulkDeleteState(); + } + + const renderBulkDeletePreview = () => { + if (!bulkDeleteList) return; + const keys = Array.from(selectedRows.keys()); + bulkDeleteList.innerHTML = ''; + if (bulkDeleteCount) { + const folderCount = keys.filter(k => k.endsWith('/')).length; + const objectCount = keys.length - folderCount; + const parts = []; + if (folderCount) parts.push(`${folderCount} folder${folderCount !== 1 ? 's' : ''}`); + if (objectCount) parts.push(`${objectCount} object${objectCount !== 1 ? 's' : ''}`); + bulkDeleteCount.textContent = `${parts.join(' and ')} selected`; + } + if (!keys.length) { + const empty = document.createElement('li'); + empty.className = 'list-group-item py-2 small text-muted'; + empty.textContent = 'No objects selected.'; + bulkDeleteList.appendChild(empty); + if (bulkDeleteStatus) { + bulkDeleteStatus.textContent = ''; + } + return; + } + const preview = keys.slice(0, 6); + preview.forEach((key) => { + const item = document.createElement('li'); + item.className = 'list-group-item py-1 small text-break'; + item.textContent = key; + bulkDeleteList.appendChild(item); + }); + if (bulkDeleteStatus) { + bulkDeleteStatus.textContent = keys.length > preview.length ? `+${keys.length - preview.length} more not shown` : ''; + } + }; + + const openBulkDeleteModal = () => { + if (!bulkDeleteModal) { + return; + } + if (selectedRows.size === 0) { + showMessage({ title: 'Select objects', body: 'Choose at least one object to delete.', variant: 'warning' }); + return; + } + renderBulkDeletePreview(); + if (bulkDeletePurge) { + bulkDeletePurge.checked = false; + } + if (bulkDeleteConfirm) { + bulkDeleteConfirm.disabled = bulkDeleting; + bulkDeleteConfirm.textContent = bulkDeleting ? 'Deleting…' : 'Delete objects'; + } + bulkDeleteModal.show(); + }; + + const performBulkDelete = async () => { + if (!bulkDeleteEndpoint || selectedRows.size === 0 || !bulkDeleteConfirm) { + return; + } + bulkDeleting = true; + bulkDeleteConfirm.disabled = true; + bulkDeleteConfirm.textContent = 'Deleting…'; + updateBulkDeleteState(); + const payload = { + keys: Array.from(selectedRows.keys()), + }; + if (versioningEnabled && bulkDeletePurge?.checked) { + payload.purge_versions = true; + } + try { + const response = await fetch(bulkDeleteEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify(payload), + }); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok || data.error) { + throw new Error(data.error || data.message || 'Unable to delete selected objects'); + } + bulkDeleteModal?.hide(); + const deletedCount = Array.isArray(data.deleted) ? data.deleted.length : selectedRows.size; + const errorCount = Array.isArray(data.errors) ? data.errors.length : 0; + const messageParts = []; + if (deletedCount) { + messageParts.push(`${deletedCount} deleted`); + } + if (errorCount) { + messageParts.push(`${errorCount} failed`); + } + const summary = messageParts.length ? messageParts.join(', ') : 'Bulk delete finished'; + showMessage({ title: 'Bulk delete complete', body: data.message || summary, variant: errorCount ? 'warning' : 'success' }); + selectedRows.clear(); + previewEmpty.classList.remove('d-none'); + previewPanel.classList.add('d-none'); + activeRow = null; + loadObjects(false); + } catch (error) { + bulkDeleteModal?.hide(); + showMessage({ title: 'Delete failed', body: (error && error.message) || 'Unable to delete selected objects', variant: 'danger' }); + } finally { + bulkDeleting = false; + if (bulkDeleteConfirm) { + bulkDeleteConfirm.disabled = false; + bulkDeleteConfirm.textContent = 'Delete objects'; + } + updateBulkDeleteState(); + } + }; + + const updateGeneratePresignState = () => { + if (!generatePresignButton) return; + if (isGeneratingPresign) { + generatePresignButton.disabled = true; + generatePresignButton.textContent = 'Generating…'; + return; + } + generatePresignButton.textContent = 'Generate link'; + generatePresignButton.disabled = !activeRow; + }; + + const requestPresignedUrl = async () => { + if (!activeRow) { + showMessage({ title: 'Select an object', body: 'Choose an object before generating a presigned URL.', variant: 'warning' }); + return; + } + const endpoint = activeRow.dataset.presignEndpoint; + if (!endpoint) { + showMessage({ title: 'Unavailable', body: 'Presign endpoint unavailable for this object.', variant: 'danger' }); + return; + } + if (isGeneratingPresign) { + return; + } + isGeneratingPresign = true; + updateGeneratePresignState(); + presignLink.value = ''; + try { + const payload = { + method: presignMethod?.value || 'GET', + expires_in: Number(presignTtl?.value) || 900, + }; + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify(payload), + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Unable to generate presigned URL'); + } + presignLink.value = data.url; + } catch (error) { + presignModal?.hide(); + showMessage({ title: 'Presign failed', body: (error && error.message) || 'Unable to generate presigned URL', variant: 'danger' }); + } finally { + isGeneratingPresign = false; + updateGeneratePresignState(); + } + }; + + const INTERNAL_METADATA_KEYS = new Set([ + '__etag__', + '__size__', + '__content_type__', + '__last_modified__', + '__storage_class__', + ]); + + const isInternalKey = (key) => INTERNAL_METADATA_KEYS.has(key.toLowerCase()); + + const renderMetadata = (metadata) => { + if (!previewMetadata || !previewMetadataList) return; + previewMetadataList.innerHTML = ''; + if (!metadata) { + previewMetadata.classList.add('d-none'); + return; + } + const userMetadata = Object.entries(metadata).filter(([key]) => !isInternalKey(key)); + if (userMetadata.length === 0) { + previewMetadata.classList.add('d-none'); + return; + } + previewMetadata.classList.remove('d-none'); + userMetadata.forEach(([key, value]) => { + const wrapper = document.createElement('div'); + wrapper.className = 'metadata-entry'; + const label = document.createElement('div'); + label.className = 'metadata-key small'; + label.textContent = key; + const val = document.createElement('div'); + val.className = 'metadata-value text-break'; + val.textContent = value; + wrapper.appendChild(label); + wrapper.appendChild(val); + previewMetadataList.appendChild(wrapper); + }); + }; + + const describeVersionReason = (reason) => { + switch (reason) { + case 'delete': + return 'delete marker'; + case 'restore-overwrite': + return 'restore overwrite'; + default: + return reason || 'update'; + } + }; + + const confirmVersionRestore = (row, version, label = null, onConfirm) => { + if (!version) return; + const timestamp = (version.archived_at || version.last_modified) ? new Date(version.archived_at || version.last_modified).toLocaleString() : version.version_id; + const sizeLabel = formatBytes(Number(version.size) || 0); + const reasonLabel = describeVersionReason(version.reason); + const targetLabel = label || row?.dataset.key || 'this object'; + const metadata = version.metadata && typeof version.metadata === 'object' ? Object.entries(version.metadata) : []; + const metadataHtml = metadata.length + ? `
Metadata

` + : ''; + const summaryHtml = ` +
+
Target: ${escapeHtml(targetLabel)}
+
Version ID: ${escapeHtml(version.version_id)}
+
Timestamp: ${escapeHtml(timestamp)}
+
Size: ${escapeHtml(sizeLabel)}
+
Reason: ${escapeHtml(reasonLabel)}
+
+ ${metadataHtml} + `; + const fallbackText = `Restore ${targetLabel} from ${timestamp}? Size ${sizeLabel}. Reason: ${reasonLabel}.`; + showMessage({ + title: 'Restore archived version?', + body: fallbackText, + bodyHtml: summaryHtml, + variant: 'warning', + actionText: 'Restore version', + onAction: () => { + if (typeof onConfirm === 'function') { + onConfirm(); + } else { + restoreVersion(row, version); + } + }, + }); + }; + + const updateArchivedCount = (count) => { + if (!archivedCountBadge) return; + const label = count === 1 ? 'item' : 'items'; + archivedCountBadge.textContent = `${count} ${label}`; + }; + + function renderArchivedRows(items) { + if (!archivedBody) return; + archivedBody.innerHTML = ''; + if (!items || items.length === 0) { + archivedBody.innerHTML = 'No archived-only objects.'; + updateArchivedCount(0); + return; + } + updateArchivedCount(items.length); + items.forEach((item) => { + const row = document.createElement('tr'); + + const keyCell = document.createElement('td'); + const keyLabel = document.createElement('div'); + keyLabel.className = 'fw-semibold text-break'; + keyLabel.textContent = item.key; + const badgeWrap = document.createElement('div'); + badgeWrap.className = 'mt-1'; + const badge = document.createElement('span'); + badge.className = 'badge text-bg-warning'; + badge.textContent = 'Archived'; + badgeWrap.appendChild(badge); + keyCell.appendChild(keyLabel); + keyCell.appendChild(badgeWrap); + + const latestCell = document.createElement('td'); + if (item.latest) { + const ts = (item.latest.archived_at || item.latest.last_modified) ? new Date(item.latest.archived_at || item.latest.last_modified).toLocaleString() : item.latest.version_id; + const sizeLabel = formatBytes(Number(item.latest.size) || 0); + latestCell.innerHTML = `
${ts}
${sizeLabel} · ${describeVersionReason(item.latest.reason)}
`; + } else { + latestCell.innerHTML = 'Unknown'; + } + + const countCell = document.createElement('td'); + countCell.className = 'text-end text-muted'; + countCell.textContent = item.versions; + + const actionsCell = document.createElement('td'); + actionsCell.className = 'text-end'; + const btnGroup = document.createElement('div'); + btnGroup.className = 'btn-group btn-group-sm'; + + const restoreButton = document.createElement('button'); + restoreButton.type = 'button'; + restoreButton.className = 'btn btn-outline-primary'; + restoreButton.textContent = 'Restore'; + restoreButton.disabled = !item.latest || !item.restore_url; + restoreButton.addEventListener('click', () => confirmVersionRestore(null, item.latest, item.key, () => restoreArchivedObject(item))); + + const purgeButton = document.createElement('button'); + purgeButton.type = 'button'; + purgeButton.className = 'btn btn-outline-danger'; + purgeButton.textContent = 'Delete versions'; + purgeButton.addEventListener('click', () => confirmArchivedPurge(item)); + + btnGroup.appendChild(restoreButton); + btnGroup.appendChild(purgeButton); + actionsCell.appendChild(btnGroup); + + row.appendChild(keyCell); + row.appendChild(latestCell); + row.appendChild(countCell); + row.appendChild(actionsCell); + archivedBody.appendChild(row); + }); + } + + async function restoreArchivedObject(item) { + if (!item?.restore_url) return; + try { + const response = await fetch(item.restore_url, { method: 'POST' }); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok) { + throw new Error(data.error || 'Unable to restore archived object'); + } + showMessage({ title: 'Restore scheduled', body: data.message || 'Object restored from archive.', variant: 'success' }); + await loadArchivedObjects(); + loadObjects(false); + } catch (error) { + showMessage({ title: 'Restore failed', body: (error && error.message) || 'Unable to restore archived object', variant: 'danger' }); + } + } + + async function purgeArchivedObject(item) { + if (!item?.purge_url) return; + try { + const response = await fetch(item.purge_url, { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok) { + throw new Error(data.error || 'Unable to delete archived versions'); + } + showMessage({ title: 'Archived versions removed', body: data.message || 'All archived data for this key has been deleted.', variant: 'success' }); + await loadArchivedObjects(); + } catch (error) { + showMessage({ title: 'Delete failed', body: (error && error.message) || 'Unable to delete archived versions', variant: 'danger' }); + } + } + + function confirmArchivedPurge(item) { + const label = item?.key || 'this object'; + const count = item?.versions || 0; + const countLabel = count === 1 ? 'version' : 'versions'; + showMessage({ + title: 'Delete archived versions?', + body: `Permanently remove ${count} archived ${countLabel} for ${label}? This cannot be undone.`, + variant: 'danger', + actionText: 'Delete versions', + onAction: () => purgeArchivedObject(item), + }); + } + + async function loadArchivedObjects() { + if (!archivedEndpoint || !archivedBody) return; + archivedBody.innerHTML = 'Loading…'; + try { + const response = await fetch(archivedEndpoint); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok) { + throw new Error(data.error || 'Unable to load archived objects'); + } + const items = Array.isArray(data.objects) ? data.objects : []; + renderArchivedRows(items); + } catch (error) { + archivedBody.innerHTML = `${(error && error.message) || 'Unable to load archived objects'}`; + updateArchivedCount(0); + } + } + + if (archivedRefreshButton) { + archivedRefreshButton.addEventListener('click', () => loadArchivedObjects()); + } + if (archivedCard && archivedEndpoint) { + loadArchivedObjects(); + } + + const propertiesTab = document.getElementById('properties-tab'); + if (propertiesTab) { + propertiesTab.addEventListener('shown.bs.tab', () => { + if (archivedCard && archivedEndpoint) { + loadArchivedObjects(); + } + }); + } + + async function restoreVersion(row, version) { + if (!row || !version?.version_id) return; + const template = row.dataset.restoreTemplate; + if (!template) return; + const url = template.replace('VERSION_ID_PLACEHOLDER', version.version_id); + try { + const response = await fetch(url, { method: 'POST' }); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok) { + throw new Error(data.error || 'Unable to restore version'); + } + const endpoint = row.dataset.versionsEndpoint; + if (endpoint) { + versionsCache.delete(endpoint); + } + await loadObjectVersions(row, { force: true }); + showMessage({ title: 'Version restored', body: data.message || 'The selected version has been restored.', variant: 'success' }); + loadObjects(false); + } catch (error) { + showMessage({ title: 'Restore failed', body: (error && error.message) || 'Unable to restore version', variant: 'danger' }); + } + } + + function renderVersionEntries(entries, row) { + if (!versionList) return; + if (!entries || entries.length === 0) { + versionList.innerHTML = '

No previous versions yet.

'; + return; + } + versionList.innerHTML = ''; + entries.forEach((entry, index) => { + const versionNumber = index + 1; + const item = document.createElement('div'); + item.className = 'd-flex align-items-center justify-content-between py-2 border-bottom'; + const textStack = document.createElement('div'); + textStack.className = 'me-3'; + const heading = document.createElement('div'); + heading.className = 'd-flex align-items-center'; + const badge = document.createElement('span'); + badge.className = 'badge text-bg-secondary me-2'; + badge.textContent = `#${versionNumber}`; + const title = document.createElement('div'); + title.className = 'fw-semibold small'; + const timestamp = (entry.archived_at || entry.last_modified) ? new Date(entry.archived_at || entry.last_modified).toLocaleString() : entry.version_id; + title.textContent = timestamp; + heading.appendChild(badge); + heading.appendChild(title); + const meta = document.createElement('div'); + meta.className = 'text-muted small'; + const reason = describeVersionReason(entry.reason); + const sizeLabel = formatBytes(Number(entry.size) || 0); + meta.textContent = `${sizeLabel} · ${reason}`; + textStack.appendChild(heading); + textStack.appendChild(meta); + const restoreButton = document.createElement('button'); + restoreButton.type = 'button'; + restoreButton.className = 'btn btn-outline-primary btn-sm'; + restoreButton.textContent = 'Restore'; + restoreButton.addEventListener('click', () => confirmVersionRestore(row, entry)); + item.appendChild(textStack); + item.appendChild(restoreButton); + versionList.appendChild(item); + }); + } + + async function loadObjectVersions(row, { force = false } = {}) { + if (!versionPanel || !versionList || !versioningEnabled) { + versionPanel?.classList.add('d-none'); + return; + } + if (!row) { + versionPanel.classList.add('d-none'); + return; + } + const endpoint = row.dataset.versionsEndpoint; + if (!endpoint) { + versionPanel.classList.add('d-none'); + return; + } + versionPanel.classList.remove('d-none'); + if (!force && versionsCache.has(endpoint)) { + renderVersionEntries(versionsCache.get(endpoint), row); + return; + } + versionList.innerHTML = '
Loading versions…
'; + try { + const response = await fetch(endpoint); + let data = {}; + try { + data = await response.json(); + } catch { + data = {}; + } + if (!response.ok) { + throw new Error(data.error || 'Unable to load versions'); + } + const entries = Array.isArray(data.versions) ? data.versions : []; + versionsCache.set(endpoint, entries); + renderVersionEntries(entries, row); + } catch (error) { + versionList.innerHTML = `

${(error && error.message) || 'Unable to load versions'}

`; + } + } + + renderMetadata(null); + const deleteModalEl = document.getElementById('deleteObjectModal'); + const deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null; + const deleteObjectForm = document.getElementById('deleteObjectForm'); + const deleteObjectKey = document.getElementById('deleteObjectKey'); + + if (deleteObjectForm) { + deleteObjectForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = deleteObjectForm.querySelector('[type="submit"]'); + const originalHtml = submitBtn ? submitBtn.innerHTML : ''; + try { + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = 'Deleting...'; + } + const formData = new FormData(deleteObjectForm); + const csrfToken = formData.get('csrf_token') || (window.getCsrfToken ? window.getCsrfToken() : ''); + const formAction = deleteObjectForm.getAttribute('action'); + const response = await fetch(formAction, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken, + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: formData + }); + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + throw new Error('Server returned an unexpected response. Please try again.'); + } + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Unable to delete object'); + } + if (deleteModal) deleteModal.hide(); + showMessage({ title: 'Object deleted', body: data.message || 'The object has been deleted.', variant: 'success' }); + previewEmpty.classList.remove('d-none'); + previewPanel.classList.add('d-none'); + activeRow = null; + loadObjects(false); + } catch (err) { + if (deleteModal) deleteModal.hide(); + showMessage({ title: 'Delete failed', body: err.message || 'Unable to delete object', variant: 'danger' }); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalHtml; + } + } + }); + } + + const resetPreviewMedia = () => { + [previewImage, previewVideo, previewAudio, previewIframe].forEach((el) => { + if (!el) return; + el.classList.add('d-none'); + if (el.tagName === 'IMG') { + el.removeAttribute('src'); + el.onload = null; + } + if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO') { + el.pause(); + el.removeAttribute('src'); + } + if (el.tagName === 'IFRAME') { + el.setAttribute('src', 'about:blank'); + } + }); + if (previewText) { + previewText.classList.add('d-none'); + previewText.textContent = ''; + } + previewPlaceholder.innerHTML = previewPlaceholderDefault; + previewPlaceholder.classList.remove('d-none'); + }; + + let previewFailed = false; + + const handlePreviewError = () => { + previewFailed = true; + if (downloadButton) { + downloadButton.classList.add('disabled'); + downloadButton.removeAttribute('href'); + } + if (presignButton) presignButton.disabled = true; + if (generatePresignButton) generatePresignButton.disabled = true; + if (previewDetailsMeta) previewDetailsMeta.classList.add('d-none'); + if (previewMetadata) previewMetadata.classList.add('d-none'); + const tagsPanel = document.getElementById('preview-tags'); + if (tagsPanel) tagsPanel.classList.add('d-none'); + const versionPanel = document.getElementById('version-panel'); + if (versionPanel) versionPanel.classList.add('d-none'); + if (previewErrorAlert) { + previewErrorAlert.textContent = 'Unable to load object \u2014 it may have been deleted, or the server returned an error.'; + previewErrorAlert.classList.remove('d-none'); + } + }; + + const clearPreviewError = () => { + previewFailed = false; + if (previewErrorAlert) previewErrorAlert.classList.add('d-none'); + if (previewDetailsMeta) previewDetailsMeta.classList.remove('d-none'); + }; + + async function fetchMetadata(metadataUrl) { + if (!metadataUrl) return null; + try { + const resp = await fetch(metadataUrl); + if (resp.ok) { + const data = await resp.json(); + return data.metadata || {}; + } + } catch (e) { + console.warn('Failed to load metadata', e); + } + return null; + } + + async function selectRow(row) { + document.querySelectorAll('[data-object-row]').forEach((r) => r.classList.remove('table-active')); + row.classList.add('table-active'); + previewEmpty.classList.add('d-none'); + previewPanel.classList.remove('d-none'); + activeRow = row; + renderMetadata(null); + clearPreviewError(); + + previewKey.textContent = row.dataset.key; + previewSize.textContent = formatBytes(Number(row.dataset.size)); + previewModified.textContent = row.dataset.lastModifiedIso || row.dataset.lastModified; + previewEtag.textContent = row.dataset.etag; + downloadButton.href = row.dataset.downloadUrl; + downloadButton.classList.remove('disabled'); + if (presignButton) { + presignButton.dataset.endpoint = row.dataset.presignEndpoint; + presignButton.disabled = false; + } + if (generatePresignButton) { + generatePresignButton.disabled = false; + } + updateGeneratePresignState(); + if (versioningEnabled) { + loadObjectVersions(row); + } + + resetPreviewMedia(); + const previewUrl = row.dataset.previewUrl; + const lower = row.dataset.key.toLowerCase(); + if (previewUrl && lower.match(/\.(png|jpg|jpeg|gif|webp|svg|ico|bmp)$/)) { + previewPlaceholder.innerHTML = '
Loading preview\u2026
'; + const currentRow = row; + fetch(previewUrl) + .then((r) => { + if (activeRow !== currentRow) return; + if (!r.ok) { + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + return; + } + return r.blob(); + }) + .then((blob) => { + if (!blob || activeRow !== currentRow) return; + const url = URL.createObjectURL(blob); + previewImage.onload = () => { + if (activeRow !== currentRow) { URL.revokeObjectURL(url); return; } + previewImage.classList.remove('d-none'); + previewPlaceholder.classList.add('d-none'); + }; + previewImage.onerror = () => { + if (activeRow !== currentRow) { URL.revokeObjectURL(url); return; } + URL.revokeObjectURL(url); + previewPlaceholder.innerHTML = '
Failed to load preview
'; + }; + previewImage.src = url; + }) + .catch(() => { + if (activeRow !== currentRow) return; + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + }); + } else if (previewUrl && lower.match(/\.(mp4|webm|ogv|mov|avi|mkv)$/)) { + const currentRow = row; + previewVideo.onerror = () => { + if (activeRow !== currentRow) return; + previewVideo.classList.add('d-none'); + previewPlaceholder.classList.remove('d-none'); + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + }; + previewVideo.src = previewUrl; + previewVideo.classList.remove('d-none'); + previewPlaceholder.classList.add('d-none'); + } else if (previewUrl && lower.match(/\.(mp3|wav|flac|ogg|aac|m4a|wma)$/)) { + const currentRow = row; + previewAudio.onerror = () => { + if (activeRow !== currentRow) return; + previewAudio.classList.add('d-none'); + previewPlaceholder.classList.remove('d-none'); + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + }; + previewAudio.src = previewUrl; + previewAudio.classList.remove('d-none'); + previewPlaceholder.classList.add('d-none'); + } else if (previewUrl && lower.match(/\.(pdf)$/)) { + const currentRow = row; + previewIframe.onerror = () => { + if (activeRow !== currentRow) return; + previewIframe.classList.add('d-none'); + previewPlaceholder.classList.remove('d-none'); + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + }; + previewIframe.src = previewUrl; + previewIframe.style.minHeight = '500px'; + previewIframe.classList.remove('d-none'); + previewPlaceholder.classList.add('d-none'); + } else if (previewUrl && previewText && lower.match(/\.(txt|log|json|md|csv|xml|html|htm|js|ts|py|java|c|cpp|h|css|scss|yaml|yml|toml|ini|cfg|conf|sh|bat|rs|go|rb|php|sql|r|swift|kt|scala|pl|lua|zig|ex|exs|hs|erl|ps1|psm1|psd1|fish|zsh|env|properties|gradle|makefile|dockerfile|vagrantfile|gitignore|gitattributes|editorconfig|eslintrc|prettierrc)$/)) { + previewText.textContent = 'Loading\u2026'; + previewText.classList.remove('d-none'); + previewPlaceholder.classList.add('d-none'); + const currentRow = row; + fetch(previewUrl) + .then((r) => { + if (!r.ok) throw new Error(r.statusText); + const len = parseInt(r.headers.get('Content-Length') || '0', 10); + if (len > 512 * 1024) { + return r.text().then((t) => t.slice(0, 512 * 1024) + '\n\n--- Truncated (file too large for preview) ---'); + } + return r.text(); + }) + .then((text) => { + if (activeRow !== currentRow) return; + previewText.textContent = text; + }) + .catch(() => { + if (activeRow !== currentRow) return; + previewText.classList.add('d-none'); + previewPlaceholder.classList.remove('d-none'); + previewPlaceholder.innerHTML = '
Failed to load preview
'; + handlePreviewError(); + }); + } + + const metadataUrl = row.dataset.metadataUrl; + if (metadataUrl) { + const metadata = await fetchMetadata(metadataUrl); + if (activeRow === row && !previewFailed) { + renderMetadata(metadata); + } + } + } + + updateBulkDeleteState(); + + function initFolderNavigation() { + if (hasFolders()) { + renderBreadcrumb(currentPrefix); + renderObjectsView(); + } + if (typeof updateFolderViewStatus === 'function') { + updateFolderViewStatus(); + } + if (typeof updateFilterWarning === 'function') { + updateFilterWarning(); + } + } + + bulkDeleteButton?.addEventListener('click', () => openBulkDeleteModal()); + bulkDeleteConfirm?.addEventListener('click', () => performBulkDelete()); + + const filterWarning = document.getElementById('filter-warning'); + const filterWarningText = document.getElementById('filter-warning-text'); + const folderViewStatus = document.getElementById('folder-view-status'); + + const updateFilterWarning = () => { + if (!filterWarning) return; + const isFiltering = currentFilterTerm.length > 0; + if (isFiltering && hasMoreObjects) { + filterWarning.classList.remove('d-none'); + } else { + filterWarning.classList.add('d-none'); + } + }; + + let searchDebounceTimer = null; + let searchAbortController = null; + let searchResults = null; + + const performServerSearch = async (term) => { + if (searchAbortController) searchAbortController.abort(); + searchAbortController = new AbortController(); + + try { + const params = new URLSearchParams({ q: term, limit: '500' }); + if (currentPrefix) params.set('prefix', currentPrefix); + const searchUrl = objectsStreamUrl.replace('/stream', '/search'); + const response = await fetch(`${searchUrl}?${params}`, { + signal: searchAbortController.signal + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + searchResults = (data.results || []).map(obj => processStreamObject(obj)); + memoizedVisibleItems = null; + memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + refreshVirtualList(); + if (loadMoreStatus) { + const countText = searchResults.length.toLocaleString(); + const truncated = data.truncated ? '+' : ''; + loadMoreStatus.textContent = `${countText}${truncated} result${searchResults.length !== 1 ? 's' : ''}`; + } + } catch (e) { + if (e.name === 'AbortError') return; + if (loadMoreStatus) { + loadMoreStatus.textContent = 'Search failed'; + } + } + }; + + document.getElementById('object-search')?.addEventListener('input', (event) => { + const newTerm = event.target.value.toLowerCase(); + const wasFiltering = currentFilterTerm.length > 0; + const isFiltering = newTerm.length > 0; + currentFilterTerm = newTerm; + + clearTimeout(searchDebounceTimer); + + if (isFiltering) { + searchDebounceTimer = setTimeout(() => performServerSearch(newTerm), 300); + return; + } + + if (!isFiltering && wasFiltering) { + if (searchAbortController) searchAbortController.abort(); + searchResults = null; + memoizedVisibleItems = null; + memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + if (loadMoreStatus) { + loadMoreStatus.textContent = buildBottomStatusText(streamingComplete); + } + } + + updateFilterWarning(); + refreshVirtualList(); + }); + + document.querySelectorAll('[data-sort-field]').forEach(el => { + el.addEventListener('click', (e) => { + e.preventDefault(); + const field = el.dataset.sortField; + const dir = el.dataset.sortDir || 'asc'; + currentSortField = field; + currentSortDir = dir; + document.querySelectorAll('[data-sort-field]').forEach(s => s.classList.remove('active')); + el.classList.add('active'); + var label = document.getElementById('sort-dropdown-label'); + if (label) label.textContent = el.textContent.trim(); + refreshVirtualList(); + }); + }); + + document.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT' || e.target.isContentEditable) return; + + if (e.key === '/' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + document.getElementById('object-search')?.focus(); + } + + if (e.key === '?' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + var kbModal = document.getElementById('keyboardShortcutsModal'); + if (kbModal) { + var instance = bootstrap.Modal.getOrCreateInstance(kbModal); + instance.toggle(); + } + } + + if (e.key === 'Escape') { + var searchInput = document.getElementById('object-search'); + if (searchInput && document.activeElement === searchInput) { + searchInput.value = ''; + const wasFiltering = currentFilterTerm.length > 0; + currentFilterTerm = ''; + if (wasFiltering) { + clearTimeout(searchDebounceTimer); + if (searchAbortController) searchAbortController.abort(); + searchResults = null; + memoizedVisibleItems = null; + memoizedInputs = { objectCount: -1, folderCount: -1, prefix: null, filterTerm: null }; + if (loadMoreStatus) { + loadMoreStatus.textContent = buildBottomStatusText(streamingComplete); + } + } + refreshVirtualList(); + searchInput.blur(); + } + } + + if (e.key === 'Delete' && !e.ctrlKey && !e.metaKey) { + if (selectedRows.size > 0 && bulkDeleteButton && !bulkDeleteButton.disabled) { + bulkDeleteButton.click(); + } + } + + if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { + if (visibleItems.length > 0 && selectAllCheckbox) { + e.preventDefault(); + selectAllCheckbox.checked = true; + selectAllCheckbox.dispatchEvent(new Event('change')); + } + } + }); + + const ctxMenu = document.getElementById('objectContextMenu'); + let ctxTargetRow = null; + + const hideContextMenu = () => { + if (ctxMenu) ctxMenu.classList.add('d-none'); + ctxTargetRow = null; + }; + + if (ctxMenu) { + document.addEventListener('click', hideContextMenu); + document.addEventListener('contextmenu', (e) => { + const row = e.target.closest('[data-object-row]'); + if (!row) { hideContextMenu(); return; } + e.preventDefault(); + ctxTargetRow = row; + + const x = Math.min(e.clientX, window.innerWidth - 200); + const y = Math.min(e.clientY, window.innerHeight - 200); + ctxMenu.style.left = x + 'px'; + ctxMenu.style.top = y + 'px'; + ctxMenu.classList.remove('d-none'); + }); + + ctxMenu.querySelectorAll('[data-ctx-action]').forEach(btn => { + btn.addEventListener('click', () => { + if (!ctxTargetRow) return; + const action = btn.dataset.ctxAction; + const key = ctxTargetRow.dataset.key; + const bucket = objectsContainer?.dataset.bucket || ''; + + if (action === 'download') { + const url = ctxTargetRow.dataset.downloadUrl; + if (url) window.open(url, '_blank'); + } else if (action === 'copy-path') { + const s3Path = 's3://' + bucket + '/' + key; + if (navigator.clipboard) { + navigator.clipboard.writeText(s3Path).then(() => { + if (window.showToast) window.showToast('Copied: ' + s3Path, 'Copied', 'success'); + }); + } + } else if (action === 'presign') { + selectRow(ctxTargetRow); + presignLink.value = ''; + presignModal?.show(); + requestPresignedUrl(); + } else if (action === 'delete') { + const deleteEndpoint = ctxTargetRow.dataset.deleteEndpoint; + if (deleteEndpoint) { + selectRow(ctxTargetRow); + const deleteModalEl = document.getElementById('deleteObjectModal'); + const deleteModal = deleteModalEl ? bootstrap.Modal.getOrCreateInstance(deleteModalEl) : null; + const deleteObjectForm = document.getElementById('deleteObjectForm'); + const deleteObjectKey = document.getElementById('deleteObjectKey'); + if (deleteModal && deleteObjectForm) { + deleteObjectForm.setAttribute('action', deleteEndpoint); + if (deleteObjectKey) deleteObjectKey.textContent = key; + deleteModal.show(); + } + } + } + hideContextMenu(); + }); + }); + } + + refreshVersionsButton?.addEventListener('click', () => { + if (!activeRow) { + versionList.innerHTML = '

Select an object to view versions.

'; + return; + } + const endpoint = activeRow.dataset.versionsEndpoint; + if (endpoint) { + versionsCache.delete(endpoint); + } + loadObjectVersions(activeRow, { force: true }); + }); + + presignButton?.addEventListener('click', () => { + if (!activeRow) { + showMessage({ title: 'Select an object', body: 'Choose an object before generating a presigned URL.', variant: 'warning' }); + return; + } + presignLink.value = ''; + presignModal?.show(); + requestPresignedUrl(); + }); + + generatePresignButton?.addEventListener('click', () => { + requestPresignedUrl(); + }); + + copyPresignLink?.addEventListener('click', async () => { + if (!presignLink?.value) { + return; + } + + const fallbackCopy = (text) => { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + let success = false; + try { + success = document.execCommand('copy'); + } catch (err) { + success = false; + } + textArea.remove(); + return success; + }; + + let copied = false; + + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(presignLink.value); + copied = true; + } catch (error) { + + } + } + + if (!copied) { + copied = fallbackCopy(presignLink.value); + } + + if (copied) { + copyPresignLink.textContent = 'Copied!'; + window.setTimeout(() => { + copyPresignLink.textContent = copyPresignDefaultLabel; + }, 1500); + } else { + showMessage({ title: 'Copy Failed', body: 'Unable to copy link to clipboard. Please select the link and copy manually.', variant: 'warning' }); + } + }); + + if (uploadForm && uploadFileInput) { + const uploadSubmitBtn = document.getElementById('uploadSubmitBtn'); + const uploadCancelBtn = document.getElementById('uploadCancelBtn'); + const uploadBtnText = document.getElementById('uploadBtnText'); + const bulkUploadProgress = document.getElementById('bulkUploadProgress'); + const bulkUploadStatus = document.getElementById('bulkUploadStatus'); + const bulkUploadCounter = document.getElementById('bulkUploadCounter'); + const bulkUploadProgressBar = document.getElementById('bulkUploadProgressBar'); + const bulkUploadCurrentFile = document.getElementById('bulkUploadCurrentFile'); + const bulkUploadResults = document.getElementById('bulkUploadResults'); + const bulkUploadSuccessAlert = document.getElementById('bulkUploadSuccessAlert'); + const bulkUploadErrorAlert = document.getElementById('bulkUploadErrorAlert'); + const bulkUploadSuccessCount = document.getElementById('bulkUploadSuccessCount'); + const bulkUploadErrorCount = document.getElementById('bulkUploadErrorCount'); + const bulkUploadErrorList = document.getElementById('bulkUploadErrorList'); + const uploadKeyPrefix = document.getElementById('uploadKeyPrefix'); + const singleFileOptions = document.getElementById('singleFileOptions'); + const floatingProgress = document.getElementById('floatingUploadProgress'); + const floatingProgressBar = document.getElementById('floatingUploadProgressBar'); + const floatingProgressStatus = document.getElementById('floatingUploadStatus'); + const floatingProgressTitle = document.getElementById('floatingUploadTitle'); + const floatingProgressExpand = document.getElementById('floatingUploadExpand'); + const floatingProgressCancel = document.getElementById('floatingUploadCancel'); + const uploadQueueContainer = document.getElementById('uploadQueueContainer'); + const uploadQueueList = document.getElementById('uploadQueueList'); + const uploadQueueCount = document.getElementById('uploadQueueCount'); + const clearUploadQueueBtn = document.getElementById('clearUploadQueueBtn'); + let isUploading = false; + let uploadQueue = []; + let activeXHRs = []; + let activeMultipartUpload = null; + let uploadCancelled = false; + let uploadStats = { + totalFiles: 0, + completedFiles: 0, + totalBytes: 0, + uploadedBytes: 0, + currentFileBytes: 0, + currentFileLoaded: 0, + currentFileName: '' + }; + + window.addEventListener('beforeunload', (e) => { + if (isUploading) { + e.preventDefault(); + e.returnValue = 'Upload in progress. Are you sure you want to leave?'; + return e.returnValue; + } + }); + + const showFloatingProgress = () => { + if (floatingProgress) { + floatingProgress.classList.remove('d-none'); + } + }; + + const hideFloatingProgress = () => { + if (floatingProgress) { + floatingProgress.classList.add('d-none'); + } + }; + + const updateFloatingProgress = () => { + const { totalFiles, completedFiles, totalBytes, uploadedBytes, currentFileLoaded, currentFileName } = uploadStats; + const effectiveUploaded = uploadedBytes + currentFileLoaded; + + if (floatingProgressBar && totalBytes > 0) { + const percent = Math.round((effectiveUploaded / totalBytes) * 100); + floatingProgressBar.style.width = `${percent}%`; + } + if (floatingProgressStatus) { + const bytesText = `${formatBytes(effectiveUploaded)} / ${formatBytes(totalBytes)}`; + const queuedCount = uploadQueue.length; + let statusText = `${completedFiles}/${totalFiles} files`; + if (queuedCount > 0) { + statusText += ` (+${queuedCount} queued)`; + } + statusText += ` • ${bytesText}`; + floatingProgressStatus.textContent = statusText; + } + if (floatingProgressTitle) { + const remaining = totalFiles - completedFiles; + const queuedCount = uploadQueue.length; + let title = `Uploading ${remaining} file${remaining !== 1 ? 's' : ''}`; + if (queuedCount > 0) { + title += ` (+${queuedCount} queued)`; + } + floatingProgressTitle.textContent = title + '...'; + } + }; + + floatingProgressExpand?.addEventListener('click', () => { + if (uploadModal) { + uploadModal.show(); + } + }); + + const cancelAllUploads = async () => { + uploadCancelled = true; + + activeXHRs.forEach(xhr => { + try { xhr.abort(); } catch { } + }); + activeXHRs = []; + + if (activeMultipartUpload) { + const { abortUrl } = activeMultipartUpload; + const csrfToken = document.querySelector('input[name="csrf_token"]')?.value; + try { + await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } }); + } catch { } + activeMultipartUpload = null; + } + + uploadQueue = []; + isProcessingQueue = false; + isUploading = false; + setUploadLockState(false); + hideFloatingProgress(); + resetUploadUI(); + + showMessage({ title: 'Upload cancelled', body: 'All uploads have been cancelled.', variant: 'info' }); + loadObjects(false); + }; + + floatingProgressCancel?.addEventListener('click', () => { + cancelAllUploads(); + }); + + const refreshUploadDropLabel = () => { + if (!uploadDropZoneLabel) return; + if (isUploading) { + uploadDropZoneLabel.textContent = 'Drop files here to add to queue'; + if (singleFileOptions) singleFileOptions.classList.add('d-none'); + return; + } + const files = uploadFileInput.files; + if (!files || files.length === 0) { + uploadDropZoneLabel.textContent = 'No file selected'; + if (singleFileOptions) singleFileOptions.classList.remove('d-none'); + return; + } + uploadDropZoneLabel.textContent = files.length === 1 ? files[0].name : `${files.length} files selected`; + + if (singleFileOptions) { + singleFileOptions.classList.toggle('d-none', files.length > 1); + } + }; + + const updateUploadBtnText = () => { + if (!uploadBtnText) return; + if (isUploading) { + const files = uploadFileInput.files; + if (files && files.length > 0) { + uploadBtnText.textContent = `Add ${files.length} to queue`; + if (uploadSubmitBtn) uploadSubmitBtn.disabled = false; + } else { + uploadBtnText.textContent = 'Uploading...'; + } + return; + } + const files = uploadFileInput.files; + if (!files || files.length <= 1) { + uploadBtnText.textContent = 'Upload'; + } else { + uploadBtnText.textContent = `Upload ${files.length} files`; + } + }; + + const resetUploadUI = () => { + if (bulkUploadProgress) bulkUploadProgress.classList.add('d-none'); + if (bulkUploadResults) bulkUploadResults.classList.add('d-none'); + if (bulkUploadSuccessAlert) bulkUploadSuccessAlert.classList.remove('d-none'); + if (bulkUploadErrorAlert) bulkUploadErrorAlert.classList.add('d-none'); + if (bulkUploadErrorList) bulkUploadErrorList.innerHTML = ''; + if (uploadSubmitBtn) uploadSubmitBtn.disabled = false; + if (uploadFileInput) uploadFileInput.disabled = false; + const progressStack = document.querySelector('[data-upload-progress]'); + if (progressStack) progressStack.innerHTML = ''; + if (uploadDropZone) { + uploadDropZone.classList.remove('upload-locked'); + uploadDropZone.style.pointerEvents = ''; + } + isUploading = false; + hideFloatingProgress(); + }; + + const MULTIPART_THRESHOLD = 8 * 1024 * 1024; + const CHUNK_SIZE = 8 * 1024 * 1024; + const uploadProgressStack = document.querySelector('[data-upload-progress]'); + const multipartInitUrl = uploadForm.dataset.multipartInitUrl; + const multipartPartTemplate = uploadForm.dataset.multipartPartTemplate; + const multipartCompleteTemplate = uploadForm.dataset.multipartCompleteTemplate; + const multipartAbortTemplate = uploadForm.dataset.multipartAbortTemplate; + + const createProgressItem = (file) => { + const item = document.createElement('div'); + item.className = 'upload-progress-item'; + item.dataset.state = 'uploading'; + item.innerHTML = ` +
+
+
${escapeHtml(file.name)}
+
${formatBytes(file.size)}
+
+
Preparing...
+
+
+
+
+
+
+ 0 B + 0% +
+
+ `; + return item; + }; + + const updateProgressItem = (item, { loaded, total, status, state, error }) => { + if (state) item.dataset.state = state; + const statusEl = item.querySelector('.upload-status'); + const progressBar = item.querySelector('.progress-bar'); + const progressLoaded = item.querySelector('.progress-loaded'); + const progressPercent = item.querySelector('.progress-percent'); + + if (status) { + statusEl.textContent = status; + statusEl.className = 'upload-status text-end ms-2'; + if (state === 'success') statusEl.classList.add('success'); + if (state === 'error') statusEl.classList.add('error'); + } + if (typeof loaded === 'number' && typeof total === 'number' && total > 0) { + const percent = Math.round((loaded / total) * 100); + progressBar.style.width = `${percent}%`; + progressLoaded.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`; + progressPercent.textContent = `${percent}%`; + } + if (error) { + const progressContainer = item.querySelector('.progress-container'); + if (progressContainer) { + progressContainer.innerHTML = `
${escapeHtml(error)}
`; + } + } + }; + + const uploadMultipart = async (file, objectKey, metadata, progressItem) => { + const csrfToken = document.querySelector('input[name="csrf_token"]')?.value; + + if (uploadCancelled) throw new Error('Upload cancelled'); + + updateProgressItem(progressItem, { status: 'Initiating...', loaded: 0, total: file.size }); + const initResp = await fetch(multipartInitUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken || '' }, + body: JSON.stringify({ object_key: objectKey, metadata }) + }); + if (!initResp.ok) { + const err = await initResp.json().catch(() => ({})); + throw new Error(err.error || 'Failed to initiate upload'); + } + const { upload_id } = await initResp.json(); + + const partUrl = multipartPartTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + const completeUrl = multipartCompleteTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + const abortUrl = multipartAbortTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + + activeMultipartUpload = { upload_id, abortUrl }; + + const parts = []; + const totalParts = Math.ceil(file.size / CHUNK_SIZE); + let uploadedBytes = 0; + + try { + for (let partNumber = 1; partNumber <= totalParts; partNumber++) { + if (uploadCancelled) throw new Error('Upload cancelled'); + + const start = (partNumber - 1) * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + updateProgressItem(progressItem, { + status: `Part ${partNumber}/${totalParts}`, + loaded: uploadedBytes, + total: file.size + }); + uploadStats.currentFileLoaded = uploadedBytes; + updateFloatingProgress(); + + const partResp = await fetch(`${partUrl}?partNumber=${partNumber}`, { + method: 'PUT', + headers: { + 'X-CSRFToken': csrfToken || '', + 'Content-Type': 'application/octet-stream' + }, + body: chunk + }); + + if (uploadCancelled) throw new Error('Upload cancelled'); + + if (!partResp.ok) { + const err = await partResp.json().catch(() => ({})); + throw new Error(err.error || `Part ${partNumber} failed`); + } + + const partData = await partResp.json(); + parts.push({ part_number: partNumber, etag: partData.etag }); + uploadedBytes += chunk.size; + + updateProgressItem(progressItem, { + loaded: uploadedBytes, + total: file.size + }); + uploadStats.currentFileLoaded = uploadedBytes; + updateFloatingProgress(); + } + + updateProgressItem(progressItem, { status: 'Completing...', loaded: file.size, total: file.size }); + const completeResp = await fetch(completeUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken || '' }, + body: JSON.stringify({ parts }) + }); + + if (!completeResp.ok) { + const err = await completeResp.json().catch(() => ({})); + throw new Error(err.error || 'Failed to complete upload'); + } + + activeMultipartUpload = null; + return await completeResp.json(); + } catch (err) { + if (!uploadCancelled) { + try { + await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } }); + } catch { } + } + activeMultipartUpload = null; + throw err; + } + }; + + const uploadRegular = async (file, objectKey, metadata, progressItem) => { + return new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append('object', file); + formData.append('object_key', objectKey); + if (metadata) formData.append('metadata', JSON.stringify(metadata)); + const csrfToken = document.querySelector('input[name="csrf_token"]')?.value; + if (csrfToken) formData.append('csrf_token', csrfToken); + + const xhr = new XMLHttpRequest(); + activeXHRs.push(xhr); + xhr.open('POST', uploadForm.action, true); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('X-CSRFToken', csrfToken || ''); + + const removeXHR = () => { + const idx = activeXHRs.indexOf(xhr); + if (idx > -1) activeXHRs.splice(idx, 1); + }; + + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + updateProgressItem(progressItem, { + status: 'Uploading...', + loaded: e.loaded, + total: e.total + }); + uploadStats.currentFileLoaded = e.loaded; + updateFloatingProgress(); + } + }); + + xhr.addEventListener('load', () => { + removeXHR(); + if (xhr.status >= 200 && xhr.status < 300) { + try { + const data = JSON.parse(xhr.responseText); + if (data.status === 'error') { + reject(new Error(data.message || 'Upload failed')); + } else { + resolve(data); + } + } catch { + resolve({}); + } + } else { + try { + const data = JSON.parse(xhr.responseText); + reject(new Error(data.message || `Upload failed (${xhr.status})`)); + } catch { + reject(new Error(`Upload failed (${xhr.status})`)); + } + } + }); + + xhr.addEventListener('error', () => { removeXHR(); reject(new Error('Network error')); }); + xhr.addEventListener('abort', () => { removeXHR(); reject(new Error('Upload cancelled')); }); + + xhr.send(formData); + }); + }; + + const uploadSingleFile = async (file, keyPrefix = '', metadata = null, progressItem = null) => { + const objectKey = keyPrefix ? `${keyPrefix}${file.name}` : file.name; + const shouldUseMultipart = file.size >= MULTIPART_THRESHOLD && multipartInitUrl; + + if (!progressItem && uploadProgressStack) { + progressItem = createProgressItem(file); + uploadProgressStack.appendChild(progressItem); + } + + try { + let result; + if (shouldUseMultipart) { + updateProgressItem(progressItem, { status: 'Multipart upload...', loaded: 0, total: file.size }); + result = await uploadMultipart(file, objectKey, metadata, progressItem); + } else { + updateProgressItem(progressItem, { status: 'Uploading...', loaded: 0, total: file.size }); + result = await uploadRegular(file, objectKey, metadata, progressItem); + } + updateProgressItem(progressItem, { state: 'success', status: 'Complete', loaded: file.size, total: file.size }); + return result; + } catch (err) { + updateProgressItem(progressItem, { state: 'error', status: 'Failed', error: err.message }); + throw err; + } + }; + + const setUploadLockState = (locked) => { + if (uploadDropZone) { + uploadDropZone.classList.toggle('upload-locked', locked); + } + }; + + let uploadSuccessFiles = []; + let uploadErrorFiles = []; + let isProcessingQueue = false; + + const updateQueueListDisplay = () => { + if (!uploadQueueList || !uploadQueueContainer || !uploadQueueCount) return; + if (uploadQueue.length === 0) { + uploadQueueContainer.classList.add('d-none'); + return; + } + uploadQueueContainer.classList.remove('d-none'); + uploadQueueCount.textContent = uploadQueue.length; + uploadQueueList.innerHTML = uploadQueue.map((item, idx) => ` +
  • + + + + + ${escapeHtml(item.file.name)} + + ${formatBytes(item.file.size)} +
  • + `).join(''); + }; + + const addFilesToQueue = (files, keyPrefix, metadata) => { + for (const file of files) { + uploadQueue.push({ file, keyPrefix, metadata }); + uploadStats.totalFiles++; + uploadStats.totalBytes += file.size; + } + updateFloatingProgress(); + updateQueueListDisplay(); + }; + + const clearUploadQueue = () => { + const clearedCount = uploadQueue.length; + if (clearedCount === 0) return; + for (const item of uploadQueue) { + uploadStats.totalFiles--; + uploadStats.totalBytes -= item.file.size; + } + uploadQueue.length = 0; + updateFloatingProgress(); + updateQueueListDisplay(); + }; + + if (clearUploadQueueBtn) { + clearUploadQueueBtn.addEventListener('click', clearUploadQueue); + } + + const processUploadQueue = async () => { + if (isProcessingQueue) return; + isProcessingQueue = true; + + while (uploadQueue.length > 0 && !uploadCancelled) { + const item = uploadQueue.shift(); + const { file, keyPrefix, metadata } = item; + updateQueueListDisplay(); + + uploadStats.currentFileName = file.name; + uploadStats.currentFileBytes = file.size; + uploadStats.currentFileLoaded = 0; + + if (bulkUploadCounter) { + const queuedCount = uploadQueue.length; + let counterText = `${uploadStats.completedFiles + 1}/${uploadStats.totalFiles}`; + if (queuedCount > 0) { + counterText += ` (+${queuedCount} queued)`; + } + bulkUploadCounter.textContent = counterText; + } + if (bulkUploadCurrentFile) { + bulkUploadCurrentFile.textContent = `Uploading: ${file.name}`; + } + if (bulkUploadProgressBar) { + const percent = Math.round(((uploadStats.completedFiles + 1) / uploadStats.totalFiles) * 100); + bulkUploadProgressBar.style.width = `${percent}%`; + } + updateFloatingProgress(); + + try { + await uploadSingleFile(file, keyPrefix, metadata); + uploadSuccessFiles.push(file.name); + } catch (error) { + uploadErrorFiles.push({ name: file.name, error: error.message || 'Unknown error' }); + } + + uploadStats.uploadedBytes += file.size; + uploadStats.completedFiles++; + uploadStats.currentFileLoaded = 0; + updateFloatingProgress(); + } + + isProcessingQueue = false; + + if (uploadQueue.length === 0 && !uploadCancelled) { + finishUploadSession(); + } + }; + + const finishUploadSession = () => { + if (bulkUploadProgress) bulkUploadProgress.classList.add('d-none'); + if (bulkUploadResults) bulkUploadResults.classList.remove('d-none'); + hideFloatingProgress(); + + if (bulkUploadSuccessCount) bulkUploadSuccessCount.textContent = uploadSuccessFiles.length; + if (uploadSuccessFiles.length === 0 && bulkUploadSuccessAlert) { + bulkUploadSuccessAlert.classList.add('d-none'); + } + + if (uploadErrorFiles.length > 0) { + if (bulkUploadErrorCount) bulkUploadErrorCount.textContent = uploadErrorFiles.length; + if (bulkUploadErrorAlert) bulkUploadErrorAlert.classList.remove('d-none'); + if (bulkUploadErrorList) { + bulkUploadErrorList.innerHTML = uploadErrorFiles + .map(f => `
  • ${escapeHtml(f.name)}: ${escapeHtml(f.error)}
  • `) + .join(''); + } + } + + isUploading = false; + setUploadLockState(false); + refreshUploadDropLabel(); + updateUploadBtnText(); + updateQueueListDisplay(); + + if (uploadSubmitBtn) uploadSubmitBtn.disabled = false; + if (uploadFileInput) { + uploadFileInput.disabled = false; + uploadFileInput.value = ''; + } + + const previousKey = activeRow?.dataset.key || null; + loadObjects(false).then(() => { + if (previousKey) { + const newRow = document.querySelector(`[data-object-row][data-key="${CSS.escape(previousKey)}"]`); + if (newRow) { + selectRow(newRow); + if (versioningEnabled) loadObjectVersions(newRow, { force: true }); + } + } + }); + + const successCount = uploadSuccessFiles.length; + const errorCount = uploadErrorFiles.length; + if (successCount > 0 && errorCount > 0) { + showMessage({ title: 'Upload complete', body: `${successCount} uploaded, ${errorCount} failed.`, variant: 'warning' }); + } else if (successCount > 0) { + showMessage({ title: 'Upload complete', body: `${successCount} object(s) uploaded successfully.`, variant: 'success' }); + } else if (errorCount > 0) { + showMessage({ title: 'Upload failed', body: `${errorCount} file(s) failed to upload.`, variant: 'danger' }); + } + }; + + const performBulkUpload = async (files) => { + if (!files || files.length === 0) return; + + const keyPrefix = (uploadKeyPrefix?.value || '').trim(); + const metadataRaw = uploadForm.querySelector('textarea[name="metadata"]')?.value?.trim(); + let metadata = null; + if (metadataRaw) { + try { + metadata = JSON.parse(metadataRaw); + } catch { + showMessage({ title: 'Invalid metadata', body: 'Metadata must be valid JSON.', variant: 'danger' }); + return; + } + } + + if (!isUploading) { + isUploading = true; + uploadCancelled = false; + uploadSuccessFiles = []; + uploadErrorFiles = []; + uploadStats = { + totalFiles: 0, + completedFiles: 0, + totalBytes: 0, + uploadedBytes: 0, + currentFileBytes: 0, + currentFileLoaded: 0, + currentFileName: '' + }; + + if (bulkUploadProgress) bulkUploadProgress.classList.remove('d-none'); + if (bulkUploadResults) bulkUploadResults.classList.add('d-none'); + if (uploadSubmitBtn) uploadSubmitBtn.disabled = true; + refreshUploadDropLabel(); + updateUploadBtnText(); + + if (uploadModal) uploadModal.hide(); + showFloatingProgress(); + } + + const fileCount = files.length; + addFilesToQueue(Array.from(files), keyPrefix, metadata); + + if (uploadFileInput) { + uploadFileInput.value = ''; + } + refreshUploadDropLabel(); + updateUploadBtnText(); + + processUploadQueue(); + }; + + refreshUploadDropLabel(); + uploadFileInput.addEventListener('change', () => { + refreshUploadDropLabel(); + updateUploadBtnText(); + if (!isUploading) { + resetUploadUI(); + } + }); + uploadDropZone?.addEventListener('click', () => { + uploadFileInput?.click(); + }); + + uploadForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const files = uploadFileInput.files; + if (!files || files.length === 0) return; + + const keyPrefix = (uploadKeyPrefix?.value || '').trim(); + + if (uploadSubmitBtn) { + uploadSubmitBtn.disabled = true; + if (uploadBtnText) uploadBtnText.textContent = 'Uploading...'; + } + + await performBulkUpload(Array.from(files)); + }); + + uploadModalEl?.addEventListener('show.bs.modal', () => { + if (hasFolders() && currentPrefix) { + uploadKeyPrefix.value = currentPrefix; + + const advancedToggle = document.querySelector('[data-bs-target="#advancedUploadOptions"]'); + const advancedCollapse = document.getElementById('advancedUploadOptions'); + if (advancedToggle && advancedCollapse && !advancedCollapse.classList.contains('show')) { + new bootstrap.Collapse(advancedCollapse, { show: true }); + } + } else if (uploadKeyPrefix) { + + uploadKeyPrefix.value = ''; + } + }); + + uploadModalEl?.addEventListener('hide.bs.modal', (event) => { + if (isUploading) { + showFloatingProgress(); + } + }); + + uploadModalEl?.addEventListener('hidden.bs.modal', () => { + if (!isUploading) { + resetUploadUI(); + uploadFileInput.value = ''; + refreshUploadDropLabel(); + updateUploadBtnText(); + } + }); + + uploadModalEl?.addEventListener('show.bs.modal', () => { + if (isUploading) { + hideFloatingProgress(); + } + }); + + const preventDefaults = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const wireDropTarget = (target, { highlightClass = '', autoOpenModal = false } = {}) => { + if (!target) return; + ['dragenter', 'dragover'].forEach((eventName) => { + target.addEventListener(eventName, (event) => { + preventDefaults(event); + if (highlightClass) { + target.classList.add(highlightClass); + } + }); + }); + ['dragleave', 'drop'].forEach((eventName) => { + target.addEventListener(eventName, (event) => { + preventDefaults(event); + if (highlightClass) { + target.classList.remove(highlightClass); + } + }); + }); + target.addEventListener('drop', (event) => { + if (!event.dataTransfer?.files?.length) { + return; + } + if (isUploading) { + performBulkUpload(event.dataTransfer.files); + } else { + if (uploadFileInput) { + uploadFileInput.files = event.dataTransfer.files; + uploadFileInput.dispatchEvent(new Event('change', { bubbles: true })); + } + if (autoOpenModal && uploadModal) { + uploadModal.show(); + } + } + }); + }; + + if (uploadDropZone) { + wireDropTarget(uploadDropZone, { highlightClass: 'is-dragover' }); + } + + if (objectsContainer) { + wireDropTarget(objectsContainer, { highlightClass: 'drag-over', autoOpenModal: true }); + } + } + + const bulkDownloadButton = document.querySelector('[data-bulk-download-trigger]'); + const bulkDownloadEndpoint = document.getElementById('objects-drop-zone')?.dataset.bulkDownloadEndpoint; + + const updateBulkDownloadState = () => { + if (!bulkDownloadButton) return; + const selectedCount = document.querySelectorAll('[data-object-select]:checked').length; + bulkDownloadButton.disabled = selectedCount === 0; + }; + + selectAllCheckbox?.addEventListener('change', (event) => { + const shouldSelect = Boolean(event.target?.checked); + + const filesInView = visibleItems.filter(item => item.type === 'file'); + + filesInView.forEach(item => { + if (shouldSelect) { + selectedRows.set(item.data.key, item.data); + } else { + selectedRows.delete(item.data.key); + } + }); + + const foldersInView = visibleItems.filter(item => item.type === 'folder'); + foldersInView.forEach(item => { + if (shouldSelect) { + selectedRows.set(item.path, { key: item.path, isFolder: true }); + } else { + selectedRows.delete(item.path); + } + }); + + document.querySelectorAll('[data-folder-select]').forEach(cb => { + cb.checked = shouldSelect; + }); + + document.querySelectorAll('[data-object-row]').forEach((row) => { + const checkbox = row.querySelector('[data-object-select]'); + if (checkbox) { + checkbox.checked = shouldSelect; + } + }); + + updateBulkDeleteState(); + setTimeout(updateBulkDownloadState, 0); + }); + + bulkDownloadButton?.addEventListener('click', async () => { + if (!bulkDownloadEndpoint) return; + const selected = Array.from(selectedRows.keys()); + if (selected.length === 0) return; + + bulkDownloadButton.disabled = true; + const originalHtml = bulkDownloadButton.innerHTML; + bulkDownloadButton.innerHTML = ' Downloading...'; + + try { + const response = await fetch(bulkDownloadEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '', + }, + body: JSON.stringify({ keys: selected }), + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Download failed'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${document.getElementById('objects-drop-zone').dataset.bucket}-download.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + } catch (error) { + showMessage({ title: 'Download Failed', body: error.message, variant: 'danger' }); + } finally { + bulkDownloadButton.disabled = false; + bulkDownloadButton.innerHTML = originalHtml; + } + }); + + const replicationStatsContainer = document.getElementById('replication-stats-cards'); + if (replicationStatsContainer) { + const statusEndpoint = replicationStatsContainer.dataset.statusEndpoint; + const syncedEl = replicationStatsContainer.querySelector('[data-stat="synced"]'); + const pendingEl = replicationStatsContainer.querySelector('[data-stat="pending"]'); + const orphanedEl = replicationStatsContainer.querySelector('[data-stat="orphaned"]'); + const bytesEl = replicationStatsContainer.querySelector('[data-stat="bytes"]'); + const lastSyncEl = document.getElementById('replication-last-sync'); + const lastSyncTimeEl = document.querySelector('[data-stat="last-sync-time"]'); + const lastSyncKeyEl = document.querySelector('[data-stat="last-sync-key"]'); + const endpointWarning = document.getElementById('replication-endpoint-warning'); + const endpointErrorEl = document.getElementById('replication-endpoint-error'); + const statusAlert = document.getElementById('replication-status-alert'); + const statusBadge = document.getElementById('replication-status-badge'); + const statusText = document.getElementById('replication-status-text'); + const pauseForm = document.getElementById('pause-replication-form'); + + const loadReplicationStats = async () => { + try { + const resp = await fetch(statusEndpoint); + if (!resp.ok) throw new Error('Failed to fetch stats'); + const data = await resp.json(); + + // Handle endpoint health status + if (data.endpoint_healthy === false) { + // Show warning and hide success alert + if (endpointWarning) { + endpointWarning.classList.remove('d-none'); + if (endpointErrorEl && data.endpoint_error) { + endpointErrorEl.textContent = data.endpoint_error + '. Replication is paused until the endpoint is available.'; + } + } + if (statusAlert) statusAlert.classList.add('d-none'); + + // Update status badge to show "Paused" with warning styling + if (statusBadge) { + statusBadge.className = 'badge bg-warning-subtle text-warning px-3 py-2'; + statusBadge.innerHTML = ` + + + + Paused (Endpoint Unavailable)`; + } + + // Hide the pause button since replication is effectively already paused + if (pauseForm) pauseForm.classList.add('d-none'); + } else { + // Hide warning and show success alert + if (endpointWarning) endpointWarning.classList.add('d-none'); + if (statusAlert) statusAlert.classList.remove('d-none'); + + // Restore status badge to show "Enabled" + if (statusBadge) { + statusBadge.className = 'badge bg-success-subtle text-success px-3 py-2'; + statusBadge.innerHTML = ` + + + + Enabled`; + } + + // Show the pause button + if (pauseForm) pauseForm.classList.remove('d-none'); + } + + if (syncedEl) syncedEl.textContent = data.objects_synced; + if (pendingEl) { + pendingEl.textContent = data.objects_pending; + if (data.objects_pending > 0) pendingEl.classList.add('text-warning'); + } + if (orphanedEl) orphanedEl.textContent = data.objects_orphaned; + if (bytesEl) bytesEl.textContent = formatBytes(data.bytes_synced); + + if (data.last_sync_at && lastSyncEl) { + lastSyncEl.style.display = ''; + const date = new Date(data.last_sync_at * 1000); + if (lastSyncTimeEl) lastSyncTimeEl.textContent = date.toLocaleString(); + if (lastSyncKeyEl && data.last_sync_key) { + lastSyncKeyEl.innerHTML = ' — ' + escapeHtml(data.last_sync_key) + ''; + } + } + } catch (err) { + console.error('Failed to load replication stats:', err); + if (syncedEl) syncedEl.textContent = '—'; + if (pendingEl) pendingEl.textContent = '—'; + if (orphanedEl) orphanedEl.textContent = '—'; + if (bytesEl) bytesEl.textContent = '—'; + } + }; + + loadReplicationStats(); + + if (window.pollingManager) { + window.pollingManager.start('replication', loadReplicationStats); + } + + const refreshBtn = document.querySelector('[data-refresh-replication]'); + refreshBtn?.addEventListener('click', () => { + + if (syncedEl) syncedEl.innerHTML = ''; + if (pendingEl) pendingEl.innerHTML = ''; + if (orphanedEl) orphanedEl.innerHTML = ''; + if (bytesEl) bytesEl.innerHTML = ''; + loadReplicationStats(); + loadReplicationFailures(); + }); + + const failuresCard = document.getElementById('replication-failures-card'); + const failuresBody = document.getElementById('replication-failures-body'); + const failureCountBadge = document.getElementById('replication-failure-count'); + const retryAllBtn = document.getElementById('retry-all-failures-btn'); + const clearFailuresBtn = document.getElementById('clear-failures-btn'); + const showMoreFailuresBtn = document.getElementById('show-more-failures'); + const failuresPagination = document.getElementById('replication-failures-pagination'); + const failuresShownCount = document.getElementById('failures-shown-count'); + const clearFailuresModal = document.getElementById('clearFailuresModal'); + const confirmClearFailuresBtn = document.getElementById('confirmClearFailuresBtn'); + const clearFailuresModalInstance = clearFailuresModal ? new bootstrap.Modal(clearFailuresModal) : null; + + let failuresExpanded = false; + let currentFailures = []; + + const loadReplicationFailures = async () => { + if (!failuresCard) return; + + const endpoint = failuresCard.dataset.failuresEndpoint; + const limit = failuresExpanded ? 50 : 5; + + try { + const resp = await fetch(`${endpoint}?limit=${limit}`); + if (!resp.ok) throw new Error('Failed to fetch failures'); + const data = await resp.json(); + + currentFailures = data.failures; + const total = data.total; + + if (total > 0) { + failuresCard.style.display = ''; + failureCountBadge.textContent = total; + renderFailures(currentFailures); + + if (total > 5 && !failuresExpanded) { + failuresPagination.style.display = ''; + failuresShownCount.textContent = `Showing ${Math.min(5, total)} of ${total}`; + } else { + failuresPagination.style.display = 'none'; + } + } else { + failuresCard.style.display = 'none'; + } + } catch (err) { + console.error('Failed to load replication failures:', err); + } + }; + + const renderFailures = (failures) => { + if (!failuresBody) return; + failuresBody.innerHTML = failures.map(f => ` + + + ${escapeHtml(f.object_key)} + + + ${escapeHtml(f.error_message)} + + ${new Date(f.timestamp * 1000).toLocaleString()} + ${f.failure_count} + + + + + + `).join(''); + }; + + window.retryFailure = async (btn, objectKey) => { + const originalHtml = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ''; + const endpoint = failuresCard.dataset.retryEndpoint.replace('__KEY__', encodeURIComponent(objectKey)); + try { + const resp = await fetch(endpoint, { method: 'POST' }); + if (resp.ok) { + loadReplicationFailures(); + } + } catch (err) { + console.error('Failed to retry:', err); + btn.disabled = false; + btn.innerHTML = originalHtml; + } + }; + + window.dismissFailure = async (btn, objectKey) => { + const originalHtml = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ''; + const endpoint = failuresCard.dataset.dismissEndpoint.replace('__KEY__', encodeURIComponent(objectKey)); + try { + const resp = await fetch(endpoint, { method: 'DELETE' }); + if (resp.ok) { + loadReplicationFailures(); + } + } catch (err) { + console.error('Failed to dismiss:', err); + btn.disabled = false; + btn.innerHTML = originalHtml; + } + }; + + retryAllBtn?.addEventListener('click', async () => { + const btn = retryAllBtn; + const originalHtml = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = 'Retrying...'; + const endpoint = failuresCard.dataset.retryAllEndpoint; + try { + const resp = await fetch(endpoint, { method: 'POST' }); + if (resp.ok) { + loadReplicationFailures(); + } + } catch (err) { + console.error('Failed to retry all:', err); + } finally { + btn.disabled = false; + btn.innerHTML = originalHtml; + } + }); + + clearFailuresBtn?.addEventListener('click', () => { + clearFailuresModalInstance?.show(); + }); + + confirmClearFailuresBtn?.addEventListener('click', async () => { + const btn = confirmClearFailuresBtn; + const originalHtml = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = 'Clearing...'; + const endpoint = failuresCard.dataset.clearEndpoint; + try { + const resp = await fetch(endpoint, { method: 'DELETE' }); + if (resp.ok) { + clearFailuresModalInstance?.hide(); + loadReplicationFailures(); + } + } catch (err) { + console.error('Failed to clear failures:', err); + } finally { + btn.disabled = false; + btn.innerHTML = originalHtml; + } + }); + + showMoreFailuresBtn?.addEventListener('click', () => { + failuresExpanded = !failuresExpanded; + showMoreFailuresBtn.textContent = failuresExpanded ? 'Show less' : 'Show more...'; + loadReplicationFailures(); + }); + + loadReplicationFailures(); + } + + const algoAes256Radio = document.getElementById('algo_aes256'); + const algoKmsRadio = document.getElementById('algo_kms'); + const kmsKeySection = document.getElementById('kmsKeySection'); + const encryptionForm = document.getElementById('encryptionForm'); + const encryptionAction = document.getElementById('encryptionAction'); + const disableEncryptionBtn = document.getElementById('disableEncryptionBtn'); + + const updateKmsKeyVisibility = () => { + if (!kmsKeySection) return; + const showKms = algoKmsRadio?.checked; + kmsKeySection.style.display = showKms ? '' : 'none'; + }; + + algoAes256Radio?.addEventListener('change', updateKmsKeyVisibility); + algoKmsRadio?.addEventListener('change', updateKmsKeyVisibility); + + const targetBucketInput = document.getElementById('target_bucket'); + const targetBucketFeedback = document.getElementById('target_bucket_feedback'); + + const validateBucketName = (name) => { + if (!name) return { valid: false, error: 'Bucket name is required' }; + if (name.length < 3) return { valid: false, error: 'Bucket name must be at least 3 characters' }; + if (name.length > 63) return { valid: false, error: 'Bucket name must be 63 characters or less' }; + if (!/^[a-z0-9]/.test(name)) return { valid: false, error: 'Bucket name must start with a lowercase letter or number' }; + if (!/[a-z0-9]$/.test(name)) return { valid: false, error: 'Bucket name must end with a lowercase letter or number' }; + if (/[A-Z]/.test(name)) return { valid: false, error: 'Bucket name must not contain uppercase letters' }; + if (/_/.test(name)) return { valid: false, error: 'Bucket name must not contain underscores' }; + if (/\.\.|--/.test(name)) return { valid: false, error: 'Bucket name must not contain consecutive periods or hyphens' }; + if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return { valid: false, error: 'Bucket name must not be formatted as an IP address' }; + if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(name) && name.length > 2) return { valid: false, error: 'Bucket name contains invalid characters. Use only lowercase letters, numbers, hyphens, and periods.' }; + return { valid: true, error: null }; + }; + + const updateBucketNameValidation = () => { + if (!targetBucketInput || !targetBucketFeedback) return; + const name = targetBucketInput.value.trim(); + if (!name) { + targetBucketInput.classList.remove('is-valid', 'is-invalid'); + targetBucketFeedback.textContent = ''; + return; + } + const result = validateBucketName(name); + targetBucketInput.classList.toggle('is-valid', result.valid); + targetBucketInput.classList.toggle('is-invalid', !result.valid); + targetBucketFeedback.textContent = result.error || ''; + }; + + targetBucketInput?.addEventListener('input', updateBucketNameValidation); + targetBucketInput?.addEventListener('blur', updateBucketNameValidation); + + const replicationForm = targetBucketInput?.closest('form'); + replicationForm?.addEventListener('submit', (e) => { + const name = targetBucketInput.value.trim(); + const result = validateBucketName(name); + if (!result.valid) { + e.preventDefault(); + updateBucketNameValidation(); + targetBucketInput.focus(); + return false; + } + }); + + const formatPolicyBtn = document.getElementById('formatPolicyBtn'); + const policyValidationStatus = document.getElementById('policyValidationStatus'); + const policyValidBadge = document.getElementById('policyValidBadge'); + const policyInvalidBadge = document.getElementById('policyInvalidBadge'); + const policyErrorDetail = document.getElementById('policyErrorDetail'); + + const validatePolicyJson = () => { + if (!policyTextarea || !policyValidationStatus) return; + const value = policyTextarea.value.trim(); + if (!value) { + policyValidationStatus.classList.add('d-none'); + policyErrorDetail?.classList.add('d-none'); + return; + } + policyValidationStatus.classList.remove('d-none'); + try { + JSON.parse(value); + policyValidBadge?.classList.remove('d-none'); + policyInvalidBadge?.classList.add('d-none'); + policyErrorDetail?.classList.add('d-none'); + } catch (err) { + policyValidBadge?.classList.add('d-none'); + policyInvalidBadge?.classList.remove('d-none'); + if (policyErrorDetail) { + policyErrorDetail.textContent = err.message; + policyErrorDetail.classList.remove('d-none'); + } + } + }; + + policyTextarea?.addEventListener('input', validatePolicyJson); + policyTextarea?.addEventListener('blur', validatePolicyJson); + + formatPolicyBtn?.addEventListener('click', () => { + if (!policyTextarea) return; + const value = policyTextarea.value.trim(); + if (!value) return; + try { + const parsed = JSON.parse(value); + policyTextarea.value = JSON.stringify(parsed, null, 2); + validatePolicyJson(); + } catch (err) { + validatePolicyJson(); + } + }); + + if (policyTextarea && policyPreset?.value === 'custom') { + validatePolicyJson(); + } + + const lifecycleCard = document.getElementById('lifecycle-rules-card'); + const lifecycleUrl = lifecycleCard?.dataset.lifecycleUrl; + const lifecycleRulesBody = document.getElementById('lifecycle-rules-body'); + const addLifecycleRuleModalEl = document.getElementById('addLifecycleRuleModal'); + const addLifecycleRuleModal = addLifecycleRuleModalEl ? new bootstrap.Modal(addLifecycleRuleModalEl) : null; + let lifecycleRules = []; + + const loadLifecycleRules = async () => { + if (!lifecycleUrl || !lifecycleRulesBody) return; + lifecycleRulesBody.innerHTML = '
    Loading...'; + try { + const resp = await fetch(lifecycleUrl); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to load lifecycle rules'); + lifecycleRules = data.rules || []; + renderLifecycleRules(); + } catch (err) { + lifecycleRulesBody.innerHTML = `${escapeHtml(err.message)}`; + } + }; + + const renderLifecycleRules = () => { + if (!lifecycleRulesBody) return; + if (lifecycleRules.length === 0) { + lifecycleRulesBody.innerHTML = 'No lifecycle rules configured'; + return; + } + lifecycleRulesBody.innerHTML = lifecycleRules.map((rule, idx) => { + const expiration = rule.Expiration?.Days ? `${rule.Expiration.Days}d` : '-'; + const noncurrent = rule.NoncurrentVersionExpiration?.NoncurrentDays ? `${rule.NoncurrentVersionExpiration.NoncurrentDays}d` : '-'; + const abortMpu = rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation ? `${rule.AbortIncompleteMultipartUpload.DaysAfterInitiation}d` : '-'; + const statusClass = rule.Status === 'Enabled' ? 'bg-success' : 'bg-secondary'; + return ` + ${escapeHtml(rule.ID || '')} + ${escapeHtml(rule.Filter?.Prefix || '*')} + ${escapeHtml(rule.Status)} + ${expiration} + ${noncurrent} + ${abortMpu} + +
    + + +
    + + `; + }).join(''); + }; + + window.editLifecycleRule = (idx) => { + const rule = lifecycleRules[idx]; + if (!rule) return; + document.getElementById('lifecycleRuleId').value = rule.ID || ''; + document.getElementById('lifecycleRuleStatus').value = rule.Status || 'Enabled'; + document.getElementById('lifecycleRulePrefix').value = rule.Filter?.Prefix || ''; + document.getElementById('lifecycleExpirationDays').value = rule.Expiration?.Days || ''; + document.getElementById('lifecycleNoncurrentDays').value = rule.NoncurrentVersionExpiration?.NoncurrentDays || ''; + document.getElementById('lifecycleAbortMpuDays').value = rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation || ''; + window.editingLifecycleIdx = idx; + addLifecycleRuleModal?.show(); + }; + + window.editingLifecycleIdx = null; + + window.deleteLifecycleRule = async (idx) => { + lifecycleRules.splice(idx, 1); + await saveLifecycleRules(); + }; + + const saveLifecycleRules = async () => { + if (!lifecycleUrl) return; + try { + const resp = await fetch(lifecycleUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ rules: lifecycleRules }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to save'); + showMessage({ title: 'Lifecycle rules saved', body: 'Configuration updated successfully.', variant: 'success' }); + renderLifecycleRules(); + } catch (err) { + showMessage({ title: 'Save failed', body: err.message, variant: 'danger' }); + } + }; + + document.getElementById('addLifecycleRuleConfirm')?.addEventListener('click', async () => { + const ruleId = document.getElementById('lifecycleRuleId')?.value?.trim(); + const status = document.getElementById('lifecycleRuleStatus')?.value || 'Enabled'; + const prefix = document.getElementById('lifecycleRulePrefix')?.value?.trim() || ''; + const expDays = parseInt(document.getElementById('lifecycleExpirationDays')?.value) || 0; + const ncDays = parseInt(document.getElementById('lifecycleNoncurrentDays')?.value) || 0; + const abortDays = parseInt(document.getElementById('lifecycleAbortMpuDays')?.value) || 0; + if (!ruleId) { showMessage({ title: 'Validation error', body: 'Rule ID is required', variant: 'warning' }); return; } + if (expDays === 0 && ncDays === 0 && abortDays === 0) { showMessage({ title: 'Validation error', body: 'At least one action is required', variant: 'warning' }); return; } + const rule = { ID: ruleId, Status: status, Filter: { Prefix: prefix } }; + if (expDays > 0) rule.Expiration = { Days: expDays }; + if (ncDays > 0) rule.NoncurrentVersionExpiration = { NoncurrentDays: ncDays }; + if (abortDays > 0) rule.AbortIncompleteMultipartUpload = { DaysAfterInitiation: abortDays }; + if (typeof window.editingLifecycleIdx === 'number' && window.editingLifecycleIdx !== null) { + lifecycleRules[window.editingLifecycleIdx] = rule; + window.editingLifecycleIdx = null; + } else { + lifecycleRules.push(rule); + } + await saveLifecycleRules(); + addLifecycleRuleModal?.hide(); + document.getElementById('lifecycleRuleId').value = ''; + document.getElementById('lifecycleRulePrefix').value = ''; + document.getElementById('lifecycleExpirationDays').value = ''; + document.getElementById('lifecycleNoncurrentDays').value = ''; + document.getElementById('lifecycleAbortMpuDays').value = ''; + document.getElementById('lifecycleRuleStatus').value = 'Enabled'; + }); + + const corsCard = document.getElementById('cors-rules-card'); + const corsUrl = corsCard?.dataset.corsUrl; + const corsRulesBody = document.getElementById('cors-rules-body'); + const addCorsRuleModalEl = document.getElementById('addCorsRuleModal'); + const addCorsRuleModal = addCorsRuleModalEl ? new bootstrap.Modal(addCorsRuleModalEl) : null; + let corsRules = []; + + const loadCorsRules = async () => { + if (!corsUrl || !corsRulesBody) return; + corsRulesBody.innerHTML = '
    Loading...'; + try { + const resp = await fetch(corsUrl); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to load CORS rules'); + corsRules = data.rules || []; + renderCorsRules(); + } catch (err) { + corsRulesBody.innerHTML = `${escapeHtml(err.message)}`; + } + }; + + const renderCorsRules = () => { + if (!corsRulesBody) return; + if (corsRules.length === 0) { + corsRulesBody.innerHTML = 'No CORS rules configured'; + return; + } + corsRulesBody.innerHTML = corsRules.map((rule, idx) => { + const origins = (rule.AllowedOrigins || []).map(o => `${escapeHtml(o)}`).join(', '); + const methods = (rule.AllowedMethods || []).map(m => `${escapeHtml(m)}`).join(' '); + const headers = (rule.AllowedHeaders || []).slice(0, 3).map(h => `${escapeHtml(h)}`).join(', '); + return ` + ${origins || 'None'} + ${methods || 'None'} + ${headers || '*'} + ${rule.MaxAgeSeconds || '-'} + +
    + + +
    + + `; + }).join(''); + }; + + window.editCorsRule = (idx) => { + const rule = corsRules[idx]; + if (!rule) return; + document.getElementById('corsAllowedOrigins').value = (rule.AllowedOrigins || []).join('\n'); + document.getElementById('corsAllowedHeaders').value = (rule.AllowedHeaders || []).join('\n'); + document.getElementById('corsExposeHeaders').value = (rule.ExposeHeaders || []).join('\n'); + document.getElementById('corsMaxAge').value = rule.MaxAgeSeconds || ''; + document.getElementById('corsMethodGet').checked = (rule.AllowedMethods || []).includes('GET'); + document.getElementById('corsMethodPut').checked = (rule.AllowedMethods || []).includes('PUT'); + document.getElementById('corsMethodPost').checked = (rule.AllowedMethods || []).includes('POST'); + document.getElementById('corsMethodDelete').checked = (rule.AllowedMethods || []).includes('DELETE'); + document.getElementById('corsMethodHead').checked = (rule.AllowedMethods || []).includes('HEAD'); + window.editingCorsIdx = idx; + addCorsRuleModal?.show(); + }; + + window.editingCorsIdx = null; + + window.deleteCorsRule = async (idx) => { + corsRules.splice(idx, 1); + await saveCorsRules(); + }; + + const saveCorsRules = async () => { + if (!corsUrl) return; + try { + const resp = await fetch(corsUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ rules: corsRules }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to save'); + showMessage({ title: 'CORS rules saved', body: 'Configuration updated successfully.', variant: 'success' }); + renderCorsRules(); + } catch (err) { + showMessage({ title: 'Save failed', body: err.message, variant: 'danger' }); + } + }; + + document.getElementById('addCorsRuleConfirm')?.addEventListener('click', async () => { + const originsRaw = document.getElementById('corsAllowedOrigins')?.value?.trim() || ''; + const origins = originsRaw.split('\n').map(s => s.trim()).filter(Boolean); + const methods = []; + if (document.getElementById('corsMethodGet')?.checked) methods.push('GET'); + if (document.getElementById('corsMethodPut')?.checked) methods.push('PUT'); + if (document.getElementById('corsMethodPost')?.checked) methods.push('POST'); + if (document.getElementById('corsMethodDelete')?.checked) methods.push('DELETE'); + if (document.getElementById('corsMethodHead')?.checked) methods.push('HEAD'); + const headersRaw = document.getElementById('corsAllowedHeaders')?.value?.trim() || ''; + const headers = headersRaw.split('\n').map(s => s.trim()).filter(Boolean); + const exposeRaw = document.getElementById('corsExposeHeaders')?.value?.trim() || ''; + const expose = exposeRaw.split('\n').map(s => s.trim()).filter(Boolean); + const maxAge = parseInt(document.getElementById('corsMaxAge')?.value) || 0; + if (origins.length === 0) { showMessage({ title: 'Validation error', body: 'At least one origin is required', variant: 'warning' }); return; } + if (methods.length === 0) { showMessage({ title: 'Validation error', body: 'At least one method is required', variant: 'warning' }); return; } + const rule = { AllowedOrigins: origins, AllowedMethods: methods }; + if (headers.length > 0) rule.AllowedHeaders = headers; + if (expose.length > 0) rule.ExposeHeaders = expose; + if (maxAge > 0) rule.MaxAgeSeconds = maxAge; + if (typeof window.editingCorsIdx === 'number' && window.editingCorsIdx !== null) { + corsRules[window.editingCorsIdx] = rule; + window.editingCorsIdx = null; + } else { + corsRules.push(rule); + } + await saveCorsRules(); + addCorsRuleModal?.hide(); + document.getElementById('corsAllowedOrigins').value = ''; + document.getElementById('corsAllowedHeaders').value = ''; + document.getElementById('corsExposeHeaders').value = ''; + document.getElementById('corsMaxAge').value = ''; + document.getElementById('corsMethodGet').checked = false; + document.getElementById('corsMethodPut').checked = false; + document.getElementById('corsMethodPost').checked = false; + document.getElementById('corsMethodDelete').checked = false; + document.getElementById('corsMethodHead').checked = false; + }); + + const aclCard = document.getElementById('bucket-acl-card'); + const aclUrl = aclCard?.dataset.aclUrl; + const aclOwnerEl = document.getElementById('acl-owner'); + const aclGrantsList = document.getElementById('acl-grants-list'); + const aclLoading = document.getElementById('acl-loading'); + const aclContent = document.getElementById('acl-content'); + const cannedAclSelect = document.getElementById('cannedAclSelect'); + + const loadAcl = async () => { + if (!aclUrl) return; + try { + const resp = await fetch(aclUrl); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to load ACL'); + if (aclOwnerEl) aclOwnerEl.textContent = data.owner || '-'; + if (aclGrantsList) { + const grants = data.grants || []; + if (grants.length === 0) { + aclGrantsList.innerHTML = '
    No grants
    '; + } else { + aclGrantsList.innerHTML = grants.map(g => `
    ${escapeHtml(g.grantee)}${escapeHtml(g.permission)}
    `).join(''); + } + } + if (aclLoading) aclLoading.classList.add('d-none'); + if (aclContent) aclContent.classList.remove('d-none'); + } catch (err) { + if (aclLoading) aclLoading.classList.add('d-none'); + if (aclContent) aclContent.classList.remove('d-none'); + if (aclGrantsList) aclGrantsList.innerHTML = `
    ${escapeHtml(err.message)}
    `; + } + }; + + cannedAclSelect?.addEventListener('change', async () => { + const canned = cannedAclSelect.value; + if (!canned || !aclUrl) return; + try { + const resp = await fetch(aclUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ canned_acl: canned }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to set ACL'); + showMessage({ title: 'ACL updated', body: `Bucket ACL set to "${canned}"`, variant: 'success' }); + await loadAcl(); + } catch (err) { + showMessage({ title: 'ACL update failed', body: err.message, variant: 'danger' }); + } + }); + + document.querySelectorAll('[data-set-acl]').forEach(btn => { + btn.addEventListener('click', async () => { + const canned = btn.dataset.setAcl; + if (!canned || !aclUrl) return; + btn.disabled = true; + const originalText = btn.innerHTML; + btn.innerHTML = ''; + try { + const resp = await fetch(aclUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ canned_acl: canned }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to set ACL'); + showMessage({ title: 'ACL updated', body: `Bucket ACL set to "${canned}"`, variant: 'success' }); + await loadAcl(); + } catch (err) { + showMessage({ title: 'ACL update failed', body: err.message, variant: 'danger' }); + } finally { + btn.innerHTML = originalText; + btn.disabled = false; + } + }); + }); + + document.getElementById('objects-table')?.addEventListener('show.bs.dropdown', function (e) { + const dropdown = e.target.closest('.dropdown'); + const menu = dropdown?.querySelector('.dropdown-menu'); + const btn = e.target; + if (!menu || !btn) return; + const btnRect = btn.getBoundingClientRect(); + menu.style.position = 'fixed'; + menu.style.top = (btnRect.bottom + 4) + 'px'; + menu.style.left = 'auto'; + menu.style.right = (window.innerWidth - btnRect.right) + 'px'; + menu.style.transform = 'none'; + }); + + const previewTagsPanel = document.getElementById('preview-tags'); + const previewTagsList = document.getElementById('preview-tags-list'); + const previewTagsEmpty = document.getElementById('preview-tags-empty'); + const previewTagsCount = document.getElementById('preview-tags-count'); + const previewTagsEditor = document.getElementById('preview-tags-editor'); + const previewTagsInputs = document.getElementById('preview-tags-inputs'); + const editTagsButton = document.getElementById('editTagsButton'); + const addTagRow = document.getElementById('addTagRow'); + const saveTagsButton = document.getElementById('saveTagsButton'); + const cancelTagsButton = document.getElementById('cancelTagsButton'); + let currentObjectTags = []; + let isEditingTags = false; + let savedObjectTags = []; + + const loadObjectTags = async (row) => { + if (!row || !previewTagsPanel) return; + if (previewFailed) { + previewTagsPanel.classList.add('d-none'); + return; + } + const tagsUrl = row.dataset.tagsUrl; + if (!tagsUrl) { + previewTagsPanel.classList.add('d-none'); + return; + } + previewTagsPanel.classList.remove('d-none'); + try { + const resp = await fetch(tagsUrl); + const data = await resp.json(); + currentObjectTags = data.tags || []; + renderObjectTags(); + } catch (err) { + currentObjectTags = []; + renderObjectTags(); + } + }; + + const renderObjectTags = () => { + if (!previewTagsList || !previewTagsEmpty || !previewTagsCount) return; + previewTagsCount.textContent = currentObjectTags.length; + if (currentObjectTags.length === 0) { + previewTagsList.innerHTML = ''; + previewTagsEmpty.classList.remove('d-none'); + } else { + previewTagsEmpty.classList.add('d-none'); + previewTagsList.innerHTML = currentObjectTags.map(t => `${escapeHtml(t.Key)}${escapeHtml(t.Value)}`).join(''); + } + }; + + const syncTagInputs = () => { + previewTagsInputs?.querySelectorAll('.tag-editor-row').forEach((row, idx) => { + if (idx < currentObjectTags.length) { + currentObjectTags[idx].Key = row.querySelector(`[data-tag-key="${idx}"]`)?.value || ''; + currentObjectTags[idx].Value = row.querySelector(`[data-tag-value="${idx}"]`)?.value || ''; + } + }); + }; + + const renderTagEditor = () => { + if (!previewTagsInputs) return; + previewTagsInputs.innerHTML = currentObjectTags.map((t, idx) => ` +
    + + + +
    + `).join(''); + }; + + window.removeTagRow = (idx) => { + syncTagInputs(); + currentObjectTags.splice(idx, 1); + renderTagEditor(); + }; + + editTagsButton?.addEventListener('click', () => { + savedObjectTags = currentObjectTags.map(t => ({ Key: t.Key, Value: t.Value })); + isEditingTags = true; + previewTagsList.classList.add('d-none'); + previewTagsEmpty.classList.add('d-none'); + previewTagsEditor?.classList.remove('d-none'); + const card = previewTagsEditor?.querySelector('.tag-editor-card'); + if (card) { + card.style.opacity = '0'; + card.style.transition = 'opacity 0.2s ease'; + requestAnimationFrame(() => { card.style.opacity = '1'; }); + } + renderTagEditor(); + }); + + cancelTagsButton?.addEventListener('click', () => { + isEditingTags = false; + currentObjectTags = savedObjectTags.map(t => ({ Key: t.Key, Value: t.Value })); + previewTagsEditor?.classList.add('d-none'); + previewTagsList.classList.remove('d-none'); + renderObjectTags(); + }); + + addTagRow?.addEventListener('click', () => { + if (currentObjectTags.length >= 10) { + showMessage({ title: 'Limit reached', body: 'Maximum 10 tags allowed per object.', variant: 'warning' }); + return; + } + syncTagInputs(); + currentObjectTags.push({ Key: '', Value: '' }); + renderTagEditor(); + }); + + saveTagsButton?.addEventListener('click', async () => { + if (!activeRow) return; + const tagsUrl = activeRow.dataset.tagsUrl; + if (!tagsUrl) return; + const inputs = previewTagsInputs?.querySelectorAll('.tag-editor-row'); + const newTags = []; + inputs?.forEach((group, idx) => { + const key = group.querySelector(`[data-tag-key="${idx}"]`)?.value?.trim() || ''; + const value = group.querySelector(`[data-tag-value="${idx}"]`)?.value?.trim() || ''; + if (key) newTags.push({ Key: key, Value: value }); + }); + try { + const resp = await fetch(tagsUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ tags: newTags }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to save tags'); + currentObjectTags = newTags; + isEditingTags = false; + previewTagsEditor?.classList.add('d-none'); + previewTagsList.classList.remove('d-none'); + renderObjectTags(); + showMessage({ title: 'Tags saved', body: 'Object tags updated successfully.', variant: 'success' }); + } catch (err) { + showMessage({ title: 'Save failed', body: err.message, variant: 'danger' }); + } + }); + + const copyMoveModalEl = document.getElementById('copyMoveModal'); + const copyMoveModal = copyMoveModalEl ? new bootstrap.Modal(copyMoveModalEl) : null; + const copyMoveActionLabel = document.getElementById('copyMoveActionLabel'); + const copyMoveConfirmLabel = document.getElementById('copyMoveConfirmLabel'); + const copyMoveSource = document.getElementById('copyMoveSource'); + const copyMoveDestBucket = document.getElementById('copyMoveDestBucket'); + const copyMoveDestKey = document.getElementById('copyMoveDestKey'); + const copyMoveConfirm = document.getElementById('copyMoveConfirm'); + const bucketsForCopyUrl = objectsContainer?.dataset.bucketsForCopyUrl; + let copyMoveAction = 'copy'; + let copyMoveSourceKey = ''; + + window.openCopyMoveModal = async (action, key) => { + copyMoveAction = action; + copyMoveSourceKey = key; + if (copyMoveActionLabel) copyMoveActionLabel.textContent = action === 'move' ? 'Move' : 'Copy'; + if (copyMoveConfirmLabel) copyMoveConfirmLabel.textContent = action === 'move' ? 'Move' : 'Copy'; + if (copyMoveSource) copyMoveSource.textContent = key; + if (copyMoveDestKey) copyMoveDestKey.value = key; + if (copyMoveDestBucket) { + copyMoveDestBucket.innerHTML = ''; + try { + const resp = await fetch(bucketsForCopyUrl); + const data = await resp.json(); + const buckets = data.buckets || []; + copyMoveDestBucket.innerHTML = buckets.map(b => ``).join(''); + } catch { + copyMoveDestBucket.innerHTML = ''; + } + } + copyMoveModal?.show(); + }; + + copyMoveConfirm?.addEventListener('click', async () => { + const destBucket = copyMoveDestBucket?.value; + const destKey = copyMoveDestKey?.value?.trim(); + if (!destBucket || !destKey) { showMessage({ title: 'Validation error', body: 'Destination bucket and key are required', variant: 'warning' }); return; } + const actionUrl = copyMoveAction === 'move' + ? urlTemplates?.move?.replace('KEY_PLACEHOLDER', encodeURIComponent(copyMoveSourceKey).replace(/%2F/g, '/')) + : urlTemplates?.copy?.replace('KEY_PLACEHOLDER', encodeURIComponent(copyMoveSourceKey).replace(/%2F/g, '/')); + if (!actionUrl) { showMessage({ title: 'Error', body: 'Copy/move URL not configured', variant: 'danger' }); return; } + try { + const resp = await fetch(actionUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': window.getCsrfToken ? window.getCsrfToken() : '' }, + body: JSON.stringify({ dest_bucket: destBucket, dest_key: destKey }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || `Failed to ${copyMoveAction} object`); + showMessage({ title: `Object ${copyMoveAction === 'move' ? 'moved' : 'copied'}`, body: `Successfully ${copyMoveAction === 'move' ? 'moved' : 'copied'} to ${destBucket}/${destKey}`, variant: 'success' }); + copyMoveModal?.hide(); + if (copyMoveAction === 'move') { + previewEmpty.classList.remove('d-none'); + previewPanel.classList.add('d-none'); + activeRow = null; + loadObjects(false); + } + } catch (err) { + showMessage({ title: `${copyMoveAction === 'move' ? 'Move' : 'Copy'} failed`, body: err.message, variant: 'danger' }); + } + }); + + const originalSelectRow = selectRow; + selectRow = async (row) => { + await originalSelectRow(row); + loadObjectTags(row); + }; + + if (lifecycleCard) loadLifecycleRules(); + + const lifecycleHistoryCard = document.getElementById('lifecycle-history-card'); + const lifecycleHistoryBody = document.getElementById('lifecycle-history-body'); + const lifecycleHistoryPagination = document.getElementById('lifecycle-history-pagination'); + const showMoreHistoryBtn = document.getElementById('show-more-history'); + const historyShownCount = document.getElementById('history-shown-count'); + let historyExpanded = false; + + const loadLifecycleHistory = async () => { + if (!lifecycleHistoryCard || !lifecycleHistoryBody) return; + + const endpoint = lifecycleHistoryCard.dataset.historyEndpoint; + const limit = historyExpanded ? 50 : 5; + + lifecycleHistoryBody.innerHTML = '
    Loading...'; + + try { + const resp = await fetch(`${endpoint}?limit=${limit}`); + if (!resp.ok) throw new Error('Failed to fetch history'); + const data = await resp.json(); + + if (!data.enabled) { + lifecycleHistoryBody.innerHTML = 'Lifecycle enforcement is not enabled'; + return; + } + + const executions = data.executions || []; + const total = data.total || 0; + + if (executions.length === 0) { + lifecycleHistoryBody.innerHTML = 'No executions recorded yet'; + lifecycleHistoryPagination.style.display = 'none'; + return; + } + + lifecycleHistoryBody.innerHTML = executions.map(e => { + const date = new Date(e.timestamp * 1000); + const hasErrors = e.errors && e.errors.length > 0; + const hasActivity = e.objects_deleted > 0 || e.versions_deleted > 0 || e.uploads_aborted > 0; + let statusBadge; + if (hasErrors) { + statusBadge = 'Errors'; + } else if (hasActivity) { + statusBadge = 'Success'; + } else { + statusBadge = 'No action'; + } + const errorTooltip = hasErrors ? ` title="${escapeHtml(e.errors.join('; '))}"` : ''; + return ` + ${date.toLocaleString()} + ${e.objects_deleted} + ${e.versions_deleted} + ${e.uploads_aborted} + ${statusBadge} + `; + }).join(''); + + if (total > 5 && !historyExpanded) { + lifecycleHistoryPagination.style.display = ''; + historyShownCount.textContent = `Showing ${Math.min(5, total)} of ${total}`; + } else { + lifecycleHistoryPagination.style.display = 'none'; + } + } catch (err) { + console.error('Failed to load lifecycle history:', err); + lifecycleHistoryBody.innerHTML = 'Failed to load history'; + } + }; + + showMoreHistoryBtn?.addEventListener('click', () => { + historyExpanded = !historyExpanded; + showMoreHistoryBtn.textContent = historyExpanded ? 'Show less' : 'Show more...'; + loadLifecycleHistory(); + }); + + if (lifecycleHistoryCard) { + loadLifecycleHistory(); + if (window.pollingManager) { + window.pollingManager.start('lifecycle', loadLifecycleHistory); + } + } + + if (corsCard) loadCorsRules(); + if (aclCard) loadAcl(); + + function updateVersioningBadge(enabled) { + var badge = document.querySelector('.badge.rounded-pill'); + if (!badge) return; + badge.classList.remove('text-bg-success', 'text-bg-secondary'); + badge.classList.add(enabled ? 'text-bg-success' : 'text-bg-secondary'); + var icon = '' + + '' + + ''; + badge.innerHTML = icon + (enabled ? 'Versioning On' : 'Versioning Off'); + versioningEnabled = enabled; + } + + function interceptForm(formId, options) { + var form = document.getElementById(formId); + if (!form) return; + + form.addEventListener('submit', function (e) { + e.preventDefault(); + window.UICore.submitFormAjax(form, { + successMessage: options.successMessage || 'Operation completed', + onSuccess: function (data) { + if (options.onSuccess) options.onSuccess(data); + if (options.closeModal) { + var modal = bootstrap.Modal.getInstance(document.getElementById(options.closeModal)); + if (modal) modal.hide(); + } + if (options.reload) { + setTimeout(function () { location.reload(); }, 500); + } + } + }); + }); + } + + function updateVersioningCard(enabled) { + var card = document.getElementById('bucket-versioning-card'); + if (!card) return; + var cardBody = card.querySelector('.card-body'); + if (!cardBody) return; + + var enabledHtml = '' + + ''; + + var disabledHtml = '' + + '
    ' + + '' + + '' + + '
    '; + + cardBody.innerHTML = enabled ? enabledHtml : disabledHtml; + + var archivedCardEl = document.getElementById('archived-objects-card'); + if (archivedCardEl) { + archivedCardEl.style.display = enabled ? '' : 'none'; + } else if (enabled) { + var endpoint = window.BucketDetailConfig?.endpoints?.archivedObjects || ''; + if (endpoint) { + var html = '
    ' + + '
    ' + + '
    ' + + '' + + '' + + 'Archived Objects
    ' + + '
    ' + + '0 items' + + '
    ' + + '
    ' + + '

    Objects that have been deleted while versioning is enabled. Their previous versions remain available until you restore or purge them.

    ' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '' + + 'KeyLatest VersionVersionsActions
    ' + + '' + + '' + + 'No archived objects
    '; + card.insertAdjacentHTML('afterend', html); + archivedCard = document.getElementById('archived-objects-card'); + archivedBody = archivedCard.querySelector('[data-archived-body]'); + archivedCountBadge = archivedCard.querySelector('[data-archived-count]'); + archivedRefreshButton = archivedCard.querySelector('[data-archived-refresh]'); + archivedEndpoint = endpoint; + archivedRefreshButton.addEventListener('click', function() { loadArchivedObjects(); }); + loadArchivedObjects(); + } + } + + var dropZone = document.getElementById('objects-drop-zone'); + if (dropZone) { + dropZone.setAttribute('data-versioning', enabled ? 'true' : 'false'); + } + + var bulkPurgeWrap = document.getElementById('bulkDeletePurgeWrap'); + if (bulkPurgeWrap) { + bulkPurgeWrap.classList.toggle('d-none', !enabled); + } + var singleDeleteVerWrap = document.getElementById('deleteObjectVersioningWrap'); + if (singleDeleteVerWrap) { + singleDeleteVerWrap.classList.toggle('d-none', !enabled); + } + + if (!enabled) { + var newForm = document.getElementById('enableVersioningForm'); + if (newForm) { + newForm.setAttribute('action', window.BucketDetailConfig?.endpoints?.versioning || ''); + newForm.addEventListener('submit', function (e) { + e.preventDefault(); + window.UICore.submitFormAjax(newForm, { + successMessage: 'Versioning enabled', + onSuccess: function () { + updateVersioningBadge(true); + updateVersioningCard(true); + } + }); + }); + } + } + } + + function updateEncryptionCard(enabled, algorithm) { + var encCard = document.getElementById('bucket-encryption-card'); + if (!encCard) return; + var alertContainer = encCard.querySelector('.alert'); + if (alertContainer) { + if (enabled) { + alertContainer.className = 'alert alert-success d-flex align-items-start mb-4'; + var algoText = algorithm === 'aws:kms' ? 'KMS' : 'AES-256'; + alertContainer.innerHTML = '' + + '' + + '' + + '
    Default encryption enabled (' + algoText + ')' + + '

    All new objects uploaded to this bucket will be automatically encrypted.

    '; + } else { + alertContainer.className = 'alert alert-secondary d-flex align-items-start mb-4'; + alertContainer.innerHTML = '' + + '' + + '
    Default encryption disabled' + + '

    Objects are stored without default encryption. You can enable server-side encryption below.

    '; + } + } + var disableBtn = document.getElementById('disableEncryptionBtn'); + if (disableBtn) { + disableBtn.style.display = enabled ? '' : 'none'; + } + } + + function updateQuotaCard(hasQuota, maxBytes, maxObjects) { + var quotaCard = document.getElementById('bucket-quota-card'); + if (!quotaCard) return; + var alertContainer = quotaCard.querySelector('.alert'); + if (alertContainer) { + if (hasQuota) { + alertContainer.className = 'alert alert-info d-flex align-items-start mb-4'; + var quotaParts = []; + if (maxBytes) quotaParts.push(formatBytes(maxBytes) + ' storage'); + if (maxObjects) quotaParts.push(maxObjects.toLocaleString() + ' objects'); + alertContainer.innerHTML = '' + + '' + + '
    Storage quota active' + + '

    This bucket is limited to ' + quotaParts.join(' and ') + '.

    '; + } else { + alertContainer.className = 'alert alert-secondary d-flex align-items-start mb-4'; + alertContainer.innerHTML = '' + + '' + + '' + + '
    No storage quota' + + '

    This bucket has no storage or object count limits. Set limits below to control usage.

    '; + } + } + var removeBtn = document.getElementById('removeQuotaBtn'); + if (removeBtn) { + removeBtn.style.display = hasQuota ? '' : 'none'; + } + var maxMbInput = document.getElementById('max_mb'); + var maxObjInput = document.getElementById('max_objects'); + if (maxMbInput) maxMbInput.value = maxBytes ? Math.floor(maxBytes / 1048576) : ''; + if (maxObjInput) maxObjInput.value = maxObjects || ''; + } + + function updatePolicyCard(hasPolicy, preset) { + var policyCard = document.querySelector('#permissions-pane .card'); + if (!policyCard) return; + var alertContainer = policyCard.querySelector('.alert'); + if (alertContainer) { + if (hasPolicy) { + alertContainer.className = 'alert alert-info d-flex align-items-start mb-4'; + alertContainer.innerHTML = '' + + '' + + '
    Policy attached' + + '

    A bucket policy is attached to this bucket. Access is granted via both IAM and bucket policy rules.

    '; + } else { + alertContainer.className = 'alert alert-secondary d-flex align-items-start mb-4'; + alertContainer.innerHTML = '' + + '' + + '' + + '
    IAM only' + + '

    No bucket policy is attached. Access is controlled by IAM policies only.

    '; + } + } + document.querySelectorAll('.preset-btn').forEach(function (btn) { + btn.classList.remove('active'); + if (btn.dataset.preset === preset) btn.classList.add('active'); + }); + var presetInputEl = document.getElementById('policyPreset'); + if (presetInputEl) presetInputEl.value = preset; + var deletePolicyBtn = document.getElementById('deletePolicyBtn'); + if (deletePolicyBtn) { + deletePolicyBtn.style.display = hasPolicy ? '' : 'none'; + } + } + + interceptForm('enableVersioningForm', { + successMessage: 'Versioning enabled', + onSuccess: function (data) { + updateVersioningBadge(true); + updateVersioningCard(true); + } + }); + + interceptForm('suspendVersioningForm', { + successMessage: 'Versioning suspended', + closeModal: 'suspendVersioningModal', + onSuccess: function (data) { + updateVersioningBadge(false); + updateVersioningCard(false); + } + }); + + interceptForm('encryptionForm', { + successMessage: 'Encryption settings saved', + onSuccess: function (data) { + updateEncryptionCard(data.enabled !== false, data.algorithm || 'AES256'); + } + }); + + interceptForm('quotaForm', { + successMessage: 'Quota settings saved', + onSuccess: function (data) { + updateQuotaCard(data.has_quota, data.max_bytes, data.max_objects); + } + }); + + interceptForm('websiteForm', { + successMessage: 'Website settings saved', + onSuccess: function (data) { + updateWebsiteCard(data.enabled !== false, data.index_document, data.error_document); + } + }); + + interceptForm('bucketPolicyForm', { + successMessage: 'Bucket policy saved', + onSuccess: function (data) { + var policyModeEl = document.getElementById('policyMode'); + var policyPresetEl = document.getElementById('policyPreset'); + var preset = policyModeEl && policyModeEl.value === 'delete' ? 'private' : + (policyPresetEl?.value || 'custom'); + updatePolicyCard(preset !== 'private', preset); + } + }); + + var deletePolicyForm = document.getElementById('deletePolicyForm'); + if (deletePolicyForm) { + deletePolicyForm.addEventListener('submit', function (e) { + e.preventDefault(); + window.UICore.submitFormAjax(deletePolicyForm, { + successMessage: 'Bucket policy deleted', + onSuccess: function (data) { + var modal = bootstrap.Modal.getInstance(document.getElementById('deletePolicyModal')); + if (modal) modal.hide(); + updatePolicyCard(false, 'private'); + var policyTextarea = document.getElementById('policyDocument'); + if (policyTextarea) policyTextarea.value = ''; + } + }); + }); + } + + var confirmDisableEncBtn = document.getElementById('confirmDisableEncryptionBtn'); + if (confirmDisableEncBtn) { + confirmDisableEncBtn.addEventListener('click', function () { + var form = document.getElementById('encryptionForm'); + if (!form) return; + document.getElementById('encryptionAction').value = 'disable'; + var modalEl = document.getElementById('disableEncryptionModal'); + var modal = modalEl ? bootstrap.Modal.getInstance(modalEl) : null; + window.UICore.submitFormAjax(form, { + successMessage: 'Encryption disabled', + onSuccess: function (data) { + document.getElementById('encryptionAction').value = 'enable'; + if (modal) modal.hide(); + updateEncryptionCard(false, null); + }, + onError: function () { + document.getElementById('encryptionAction').value = 'enable'; + if (modal) modal.hide(); + } + }); + }); + } + + var removeQuotaBtn = document.getElementById('removeQuotaBtn'); + if (removeQuotaBtn) { + removeQuotaBtn.addEventListener('click', function () { + var form = document.getElementById('quotaForm'); + if (!form) return; + document.getElementById('quotaAction').value = 'remove'; + window.UICore.submitFormAjax(form, { + successMessage: 'Quota removed', + onSuccess: function (data) { + document.getElementById('quotaAction').value = 'set'; + updateQuotaCard(false, null, null); + } + }); + }); + } + + function updateWebsiteCard(enabled, indexDoc, errorDoc) { + var card = document.getElementById('bucket-website-card'); + if (!card) return; + var alertContainer = card.querySelector('.alert'); + if (alertContainer) { + if (enabled) { + alertContainer.className = 'alert alert-success d-flex align-items-start mb-4'; + var detail = 'Index: ' + escapeHtml(indexDoc || 'index.html') + ''; + if (errorDoc) detail += '
    Error: ' + escapeHtml(errorDoc) + ''; + alertContainer.innerHTML = '' + + '' + + '
    Website hosting is enabled' + + '

    ' + detail + '

    '; + } else { + alertContainer.className = 'alert alert-secondary d-flex align-items-start mb-4'; + alertContainer.innerHTML = '' + + '' + + '' + + '
    Website hosting is disabled' + + '

    Enable website hosting to serve bucket contents as a static website.

    '; + } + } + var disableBtn = document.getElementById('disableWebsiteBtn'); + if (disableBtn) { + disableBtn.style.display = enabled ? '' : 'none'; + } + var submitBtn = document.getElementById('websiteSubmitBtn'); + if (submitBtn) { + submitBtn.classList.remove('btn-primary', 'btn-success'); + submitBtn.classList.add(enabled ? 'btn-primary' : 'btn-success'); + } + var submitLabel = document.getElementById('websiteSubmitLabel'); + if (submitLabel) { + submitLabel.textContent = enabled ? 'Save Website Settings' : 'Enable Website Hosting'; + } + } + + var disableWebsiteBtn = document.getElementById('disableWebsiteBtn'); + if (disableWebsiteBtn) { + disableWebsiteBtn.addEventListener('click', function () { + var form = document.getElementById('websiteForm'); + if (!form) return; + document.getElementById('websiteAction').value = 'disable'; + window.UICore.submitFormAjax(form, { + successMessage: 'Website hosting disabled', + onSuccess: function (data) { + document.getElementById('websiteAction').value = 'enable'; + updateWebsiteCard(false, null, null); + } + }); + }); + } + + function reloadReplicationPane() { + var replicationPane = document.getElementById('replication-pane'); + if (!replicationPane) return; + fetch(window.location.pathname + '?tab=replication', { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function (resp) { return resp.text(); }) + .then(function (html) { + var parser = new DOMParser(); + var doc = parser.parseFromString(html, 'text/html'); + var newPane = doc.getElementById('replication-pane'); + if (newPane) { + replicationPane.innerHTML = newPane.innerHTML; + initReplicationForms(); + initReplicationStats(); + } + }) + .catch(function (err) { + console.error('Failed to reload replication pane:', err); + }); + } + + function initReplicationForms() { + document.querySelectorAll('form[action*="replication"]').forEach(function (form) { + if (form.dataset.ajaxBound) return; + form.dataset.ajaxBound = 'true'; + var actionInput = form.querySelector('input[name="action"]'); + if (!actionInput) return; + var action = actionInput.value; + + form.addEventListener('submit', function (e) { + e.preventDefault(); + var msg = action === 'pause' ? 'Replication paused' : + action === 'resume' ? 'Replication resumed' : + action === 'delete' ? 'Replication disabled' : + action === 'create' ? 'Replication configured' : 'Operation completed'; + window.UICore.submitFormAjax(form, { + successMessage: msg, + onSuccess: function (data) { + var modal = bootstrap.Modal.getInstance(document.getElementById('disableReplicationModal')); + if (modal) modal.hide(); + reloadReplicationPane(); + } + }); + }); + }); + } + + function initReplicationStats() { + var statsContainer = document.getElementById('replication-stats-cards'); + if (!statsContainer) return; + var statusEndpoint = statsContainer.dataset.statusEndpoint; + if (!statusEndpoint) return; + + var syncedEl = statsContainer.querySelector('[data-stat="synced"]'); + var pendingEl = statsContainer.querySelector('[data-stat="pending"]'); + var orphanedEl = statsContainer.querySelector('[data-stat="orphaned"]'); + var bytesEl = statsContainer.querySelector('[data-stat="bytes"]'); + + fetch(statusEndpoint) + .then(function (resp) { return resp.json(); }) + .then(function (data) { + if (syncedEl) syncedEl.textContent = data.objects_synced || 0; + if (pendingEl) pendingEl.textContent = data.objects_pending || 0; + if (orphanedEl) orphanedEl.textContent = data.objects_orphaned || 0; + if (bytesEl) bytesEl.textContent = formatBytes(data.bytes_synced || 0); + }) + .catch(function (err) { + console.error('Failed to load replication stats:', err); + }); + } + + initReplicationForms(); + initReplicationStats(); + + var deleteBucketForm = document.getElementById('deleteBucketForm'); + if (deleteBucketForm) { + deleteBucketForm.addEventListener('submit', function (e) { + e.preventDefault(); + window.UICore.submitFormAjax(deleteBucketForm, { + onSuccess: function () { + sessionStorage.setItem('flashMessage', JSON.stringify({ title: 'Bucket deleted', variant: 'success' })); + window.location.href = window.BucketDetailConfig?.endpoints?.bucketsOverview || '/ui/buckets'; + } + }); + }); + } + + window.BucketDetailConfig = window.BucketDetailConfig || {}; + +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-operations.js b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-operations.js new file mode 100644 index 0000000..d5791d4 --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-operations.js @@ -0,0 +1,192 @@ +window.BucketDetailOperations = (function() { + 'use strict'; + + let showMessage = function() {}; + let escapeHtml = function(s) { return s; }; + + function init(config) { + showMessage = config.showMessage || showMessage; + escapeHtml = config.escapeHtml || escapeHtml; + } + + async function loadLifecycleRules(card, endpoint) { + if (!card || !endpoint) return; + const body = card.querySelector('[data-lifecycle-body]'); + if (!body) return; + + try { + const response = await fetch(endpoint); + const data = await response.json(); + + if (!response.ok) { + body.innerHTML = `${escapeHtml(data.error || 'Failed to load')}`; + return; + } + + const rules = data.rules || []; + if (rules.length === 0) { + body.innerHTML = 'No lifecycle rules configured'; + return; + } + + body.innerHTML = rules.map(rule => { + const actions = []; + if (rule.expiration_days) actions.push(`Delete after ${rule.expiration_days} days`); + if (rule.noncurrent_days) actions.push(`Delete old versions after ${rule.noncurrent_days} days`); + if (rule.abort_mpu_days) actions.push(`Abort incomplete MPU after ${rule.abort_mpu_days} days`); + + return ` + + ${escapeHtml(rule.id)} + ${escapeHtml(rule.prefix || '(all)')} + ${actions.map(a => `
    ${escapeHtml(a)}
    `).join('')} + + ${escapeHtml(rule.status)} + + + + + + `; + }).join(''); + } catch (err) { + body.innerHTML = `${escapeHtml(err.message)}`; + } + } + + async function loadCorsRules(card, endpoint) { + if (!card || !endpoint) return; + const body = document.getElementById('cors-rules-body'); + if (!body) return; + + try { + const response = await fetch(endpoint); + const data = await response.json(); + + if (!response.ok) { + body.innerHTML = `${escapeHtml(data.error || 'Failed to load')}`; + return; + } + + const rules = data.rules || []; + if (rules.length === 0) { + body.innerHTML = 'No CORS rules configured'; + return; + } + + body.innerHTML = rules.map((rule, idx) => ` + + ${(rule.allowed_origins || []).map(o => `${escapeHtml(o)}`).join('')} + ${(rule.allowed_methods || []).map(m => `${escapeHtml(m)}`).join('')} + ${(rule.allowed_headers || []).slice(0, 3).join(', ')}${(rule.allowed_headers || []).length > 3 ? '...' : ''} + ${rule.max_age_seconds || 0}s + + + + + `).join(''); + } catch (err) { + body.innerHTML = `${escapeHtml(err.message)}`; + } + } + + async function loadAcl(card, endpoint) { + if (!card || !endpoint) return; + const body = card.querySelector('[data-acl-body]'); + if (!body) return; + + try { + const response = await fetch(endpoint); + const data = await response.json(); + + if (!response.ok) { + body.innerHTML = `${escapeHtml(data.error || 'Failed to load')}`; + return; + } + + const grants = data.grants || []; + if (grants.length === 0) { + body.innerHTML = 'No ACL grants configured'; + return; + } + + body.innerHTML = grants.map(grant => { + const grantee = grant.grantee_type === 'CanonicalUser' + ? grant.display_name || grant.grantee_id + : grant.grantee_uri || grant.grantee_type; + return ` + + ${escapeHtml(grantee)} + ${escapeHtml(grant.permission)} + ${escapeHtml(grant.grantee_type)} + + `; + }).join(''); + } catch (err) { + body.innerHTML = `${escapeHtml(err.message)}`; + } + } + + async function deleteLifecycleRule(ruleId) { + if (!confirm(`Delete lifecycle rule "${ruleId}"?`)) return; + const card = document.getElementById('lifecycle-rules-card'); + if (!card) return; + const endpoint = card.dataset.lifecycleUrl; + const csrfToken = window.getCsrfToken ? window.getCsrfToken() : ''; + + try { + const resp = await fetch(endpoint, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, + body: JSON.stringify({ rule_id: ruleId }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to delete'); + showMessage({ title: 'Rule deleted', body: `Lifecycle rule "${ruleId}" has been deleted.`, variant: 'success' }); + loadLifecycleRules(card, endpoint); + } catch (err) { + showMessage({ title: 'Delete failed', body: err.message, variant: 'danger' }); + } + } + + async function deleteCorsRule(index) { + if (!confirm('Delete this CORS rule?')) return; + const card = document.getElementById('cors-rules-card'); + if (!card) return; + const endpoint = card.dataset.corsUrl; + const csrfToken = window.getCsrfToken ? window.getCsrfToken() : ''; + + try { + const resp = await fetch(endpoint, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, + body: JSON.stringify({ rule_index: index }) + }); + const data = await resp.json(); + if (!resp.ok) throw new Error(data.error || 'Failed to delete'); + showMessage({ title: 'Rule deleted', body: 'CORS rule has been deleted.', variant: 'success' }); + loadCorsRules(card, endpoint); + } catch (err) { + showMessage({ title: 'Delete failed', body: err.message, variant: 'danger' }); + } + } + + return { + init: init, + loadLifecycleRules: loadLifecycleRules, + loadCorsRules: loadCorsRules, + loadAcl: loadAcl, + deleteLifecycleRule: deleteLifecycleRule, + deleteCorsRule: deleteCorsRule + }; +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-upload.js b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-upload.js new file mode 100644 index 0000000..6e0c6a7 --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-upload.js @@ -0,0 +1,600 @@ +window.BucketDetailUpload = (function() { + 'use strict'; + + const MULTIPART_THRESHOLD = 8 * 1024 * 1024; + const CHUNK_SIZE = 8 * 1024 * 1024; + const MAX_PART_RETRIES = 3; + const RETRY_BASE_DELAY_MS = 1000; + + let state = { + isUploading: false, + uploadProgress: { current: 0, total: 0, currentFile: '' } + }; + + let elements = {}; + let callbacks = {}; + + function init(config) { + elements = { + uploadForm: config.uploadForm, + uploadFileInput: config.uploadFileInput, + uploadModal: config.uploadModal, + uploadModalEl: config.uploadModalEl, + uploadSubmitBtn: config.uploadSubmitBtn, + uploadCancelBtn: config.uploadCancelBtn, + uploadBtnText: config.uploadBtnText, + uploadDropZone: config.uploadDropZone, + uploadDropZoneLabel: config.uploadDropZoneLabel, + uploadProgressStack: config.uploadProgressStack, + uploadKeyPrefix: config.uploadKeyPrefix, + singleFileOptions: config.singleFileOptions, + bulkUploadProgress: config.bulkUploadProgress, + bulkUploadStatus: config.bulkUploadStatus, + bulkUploadCounter: config.bulkUploadCounter, + bulkUploadProgressBar: config.bulkUploadProgressBar, + bulkUploadCurrentFile: config.bulkUploadCurrentFile, + bulkUploadResults: config.bulkUploadResults, + bulkUploadSuccessAlert: config.bulkUploadSuccessAlert, + bulkUploadErrorAlert: config.bulkUploadErrorAlert, + bulkUploadSuccessCount: config.bulkUploadSuccessCount, + bulkUploadErrorCount: config.bulkUploadErrorCount, + bulkUploadErrorList: config.bulkUploadErrorList, + floatingProgress: config.floatingProgress, + floatingProgressBar: config.floatingProgressBar, + floatingProgressStatus: config.floatingProgressStatus, + floatingProgressTitle: config.floatingProgressTitle, + floatingProgressExpand: config.floatingProgressExpand + }; + + callbacks = { + showMessage: config.showMessage || function() {}, + formatBytes: config.formatBytes || function(b) { return b + ' bytes'; }, + escapeHtml: config.escapeHtml || function(s) { return s; }, + onUploadComplete: config.onUploadComplete || function() {}, + hasFolders: config.hasFolders || function() { return false; }, + getCurrentPrefix: config.getCurrentPrefix || function() { return ''; } + }; + + setupEventListeners(); + setupBeforeUnload(); + } + + function isUploading() { + return state.isUploading; + } + + function setupBeforeUnload() { + window.addEventListener('beforeunload', (e) => { + if (state.isUploading) { + e.preventDefault(); + e.returnValue = 'Upload in progress. Are you sure you want to leave?'; + return e.returnValue; + } + }); + } + + function showFloatingProgress() { + if (elements.floatingProgress) { + elements.floatingProgress.classList.remove('d-none'); + } + } + + function hideFloatingProgress() { + if (elements.floatingProgress) { + elements.floatingProgress.classList.add('d-none'); + } + } + + function updateFloatingProgress(current, total, currentFile) { + state.uploadProgress = { current, total, currentFile: currentFile || '' }; + if (elements.floatingProgressBar && total > 0) { + const percent = Math.round((current / total) * 100); + elements.floatingProgressBar.style.width = `${percent}%`; + } + if (elements.floatingProgressStatus) { + if (currentFile) { + elements.floatingProgressStatus.textContent = `${current}/${total} files - ${currentFile}`; + } else { + elements.floatingProgressStatus.textContent = `${current}/${total} files completed`; + } + } + if (elements.floatingProgressTitle) { + elements.floatingProgressTitle.textContent = `Uploading ${total} file${total !== 1 ? 's' : ''}...`; + } + } + + function refreshUploadDropLabel() { + if (!elements.uploadDropZoneLabel || !elements.uploadFileInput) return; + const files = elements.uploadFileInput.files; + if (!files || files.length === 0) { + elements.uploadDropZoneLabel.textContent = 'No file selected'; + if (elements.singleFileOptions) elements.singleFileOptions.classList.remove('d-none'); + return; + } + elements.uploadDropZoneLabel.textContent = files.length === 1 ? files[0].name : `${files.length} files selected`; + if (elements.singleFileOptions) { + elements.singleFileOptions.classList.toggle('d-none', files.length > 1); + } + } + + function updateUploadBtnText() { + if (!elements.uploadBtnText || !elements.uploadFileInput) return; + const files = elements.uploadFileInput.files; + if (!files || files.length <= 1) { + elements.uploadBtnText.textContent = 'Upload'; + } else { + elements.uploadBtnText.textContent = `Upload ${files.length} files`; + } + } + + function resetUploadUI() { + if (elements.bulkUploadProgress) elements.bulkUploadProgress.classList.add('d-none'); + if (elements.bulkUploadResults) elements.bulkUploadResults.classList.add('d-none'); + if (elements.bulkUploadSuccessAlert) elements.bulkUploadSuccessAlert.classList.remove('d-none'); + if (elements.bulkUploadErrorAlert) elements.bulkUploadErrorAlert.classList.add('d-none'); + if (elements.bulkUploadErrorList) elements.bulkUploadErrorList.innerHTML = ''; + if (elements.uploadSubmitBtn) elements.uploadSubmitBtn.disabled = false; + if (elements.uploadFileInput) elements.uploadFileInput.disabled = false; + if (elements.uploadProgressStack) elements.uploadProgressStack.innerHTML = ''; + if (elements.uploadDropZone) { + elements.uploadDropZone.classList.remove('upload-locked'); + elements.uploadDropZone.style.pointerEvents = ''; + } + state.isUploading = false; + hideFloatingProgress(); + } + + function setUploadLockState(locked) { + if (elements.uploadDropZone) { + elements.uploadDropZone.classList.toggle('upload-locked', locked); + elements.uploadDropZone.style.pointerEvents = locked ? 'none' : ''; + } + if (elements.uploadFileInput) { + elements.uploadFileInput.disabled = locked; + } + } + + function createProgressItem(file) { + const item = document.createElement('div'); + item.className = 'upload-progress-item'; + item.dataset.state = 'uploading'; + item.innerHTML = ` +
    +
    +
    ${callbacks.escapeHtml(file.name)}
    +
    ${callbacks.formatBytes(file.size)}
    +
    +
    Preparing...
    +
    +
    +
    +
    +
    +
    + 0 B + 0% +
    +
    + `; + return item; + } + + function updateProgressItem(item, { loaded, total, status, progressState, error }) { + if (progressState) item.dataset.state = progressState; + const statusEl = item.querySelector('.upload-status'); + const progressBar = item.querySelector('.progress-bar'); + const progressLoaded = item.querySelector('.progress-loaded'); + const progressPercent = item.querySelector('.progress-percent'); + + if (status) { + statusEl.textContent = status; + statusEl.className = 'upload-status text-end ms-2'; + if (progressState === 'success') statusEl.classList.add('success'); + if (progressState === 'error') statusEl.classList.add('error'); + } + if (typeof loaded === 'number' && typeof total === 'number' && total > 0) { + const percent = Math.round((loaded / total) * 100); + progressBar.style.width = `${percent}%`; + progressLoaded.textContent = `${callbacks.formatBytes(loaded)} / ${callbacks.formatBytes(total)}`; + progressPercent.textContent = `${percent}%`; + } + if (error) { + const progressContainer = item.querySelector('.progress-container'); + if (progressContainer) { + progressContainer.innerHTML = `
    ${callbacks.escapeHtml(error)}
    `; + } + } + } + + function uploadPartXHR(url, chunk, csrfToken, baseBytes, fileSize, progressItem, partNumber, totalParts) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('PUT', url, true); + xhr.setRequestHeader('X-CSRFToken', csrfToken || ''); + + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + updateProgressItem(progressItem, { + status: `Part ${partNumber}/${totalParts}`, + loaded: baseBytes + e.loaded, + total: fileSize + }); + } + }); + + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + resolve(JSON.parse(xhr.responseText)); + } catch { + reject(new Error(`Part ${partNumber}: invalid response`)); + } + } else { + try { + const data = JSON.parse(xhr.responseText); + reject(new Error(data.error || `Part ${partNumber} failed (${xhr.status})`)); + } catch { + reject(new Error(`Part ${partNumber} failed (${xhr.status})`)); + } + } + }); + + xhr.addEventListener('error', () => reject(new Error(`Part ${partNumber}: network error`))); + xhr.addEventListener('abort', () => reject(new Error(`Part ${partNumber}: aborted`))); + + xhr.send(chunk); + }); + } + + async function uploadPartWithRetry(url, chunk, csrfToken, baseBytes, fileSize, progressItem, partNumber, totalParts) { + let lastError; + for (let attempt = 0; attempt <= MAX_PART_RETRIES; attempt++) { + try { + return await uploadPartXHR(url, chunk, csrfToken, baseBytes, fileSize, progressItem, partNumber, totalParts); + } catch (err) { + lastError = err; + if (attempt < MAX_PART_RETRIES) { + const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt); + updateProgressItem(progressItem, { + status: `Part ${partNumber}/${totalParts} retry ${attempt + 1}/${MAX_PART_RETRIES}...`, + loaded: baseBytes, + total: fileSize + }); + await new Promise(r => setTimeout(r, delay)); + } + } + } + throw lastError; + } + + async function uploadMultipart(file, objectKey, metadata, progressItem, urls) { + const csrfToken = document.querySelector('input[name="csrf_token"]')?.value; + + updateProgressItem(progressItem, { status: 'Initiating...', loaded: 0, total: file.size }); + const initResp = await fetch(urls.initUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken || '' }, + body: JSON.stringify({ object_key: objectKey, metadata }) + }); + if (!initResp.ok) { + const err = await initResp.json().catch(() => ({})); + throw new Error(err.error || 'Failed to initiate upload'); + } + const { upload_id } = await initResp.json(); + + const partUrl = urls.partTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + const completeUrl = urls.completeTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + const abortUrl = urls.abortTemplate.replace('UPLOAD_ID_PLACEHOLDER', upload_id); + + const parts = []; + const totalParts = Math.ceil(file.size / CHUNK_SIZE); + let uploadedBytes = 0; + + try { + for (let partNumber = 1; partNumber <= totalParts; partNumber++) { + const start = (partNumber - 1) * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + const partData = await uploadPartWithRetry( + `${partUrl}?partNumber=${partNumber}`, + chunk, csrfToken, uploadedBytes, file.size, + progressItem, partNumber, totalParts + ); + + parts.push({ part_number: partNumber, etag: partData.etag }); + uploadedBytes += (end - start); + + updateProgressItem(progressItem, { + loaded: uploadedBytes, + total: file.size + }); + } + + updateProgressItem(progressItem, { status: 'Completing...', loaded: file.size, total: file.size }); + const completeResp = await fetch(completeUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken || '' }, + body: JSON.stringify({ parts }) + }); + + if (!completeResp.ok) { + const err = await completeResp.json().catch(() => ({})); + throw new Error(err.error || 'Failed to complete upload'); + } + + return await completeResp.json(); + } catch (err) { + try { + await fetch(abortUrl, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken || '' } }); + } catch {} + throw err; + } + } + + async function uploadRegular(file, objectKey, metadata, progressItem, formAction) { + return new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append('object', file); + formData.append('object_key', objectKey); + if (metadata) formData.append('metadata', JSON.stringify(metadata)); + const csrfToken = document.querySelector('input[name="csrf_token"]')?.value; + if (csrfToken) formData.append('csrf_token', csrfToken); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', formAction, true); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('X-CSRFToken', csrfToken || ''); + + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + updateProgressItem(progressItem, { + status: 'Uploading...', + loaded: e.loaded, + total: e.total + }); + } + }); + + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + const data = JSON.parse(xhr.responseText); + if (data.status === 'error') { + reject(new Error(data.message || 'Upload failed')); + } else { + resolve(data); + } + } catch { + resolve({}); + } + } else { + try { + const data = JSON.parse(xhr.responseText); + reject(new Error(data.message || `Upload failed (${xhr.status})`)); + } catch { + reject(new Error(`Upload failed (${xhr.status})`)); + } + } + }); + + xhr.addEventListener('error', () => reject(new Error('Network error'))); + xhr.addEventListener('abort', () => reject(new Error('Upload aborted'))); + + xhr.send(formData); + }); + } + + async function uploadSingleFile(file, keyPrefix, metadata, progressItem, urls) { + const objectKey = keyPrefix ? `${keyPrefix}${file.name}` : file.name; + const shouldUseMultipart = file.size >= MULTIPART_THRESHOLD && urls.initUrl; + + if (!progressItem && elements.uploadProgressStack) { + progressItem = createProgressItem(file); + elements.uploadProgressStack.appendChild(progressItem); + } + + try { + let result; + if (shouldUseMultipart) { + updateProgressItem(progressItem, { status: 'Multipart upload...', loaded: 0, total: file.size }); + result = await uploadMultipart(file, objectKey, metadata, progressItem, urls); + } else { + updateProgressItem(progressItem, { status: 'Uploading...', loaded: 0, total: file.size }); + result = await uploadRegular(file, objectKey, metadata, progressItem, urls.formAction); + } + updateProgressItem(progressItem, { progressState: 'success', status: 'Complete', loaded: file.size, total: file.size }); + return result; + } catch (err) { + updateProgressItem(progressItem, { progressState: 'error', status: 'Failed', error: err.message }); + throw err; + } + } + + async function performBulkUpload(files, urls) { + if (state.isUploading || !files || files.length === 0) return; + + state.isUploading = true; + setUploadLockState(true); + const keyPrefix = (elements.uploadKeyPrefix?.value || '').trim(); + const metadataRaw = elements.uploadForm?.querySelector('textarea[name="metadata"]')?.value?.trim(); + let metadata = null; + if (metadataRaw) { + try { + metadata = JSON.parse(metadataRaw); + } catch { + callbacks.showMessage({ title: 'Invalid metadata', body: 'Metadata must be valid JSON.', variant: 'danger' }); + resetUploadUI(); + return; + } + } + + if (elements.bulkUploadProgress) elements.bulkUploadProgress.classList.remove('d-none'); + if (elements.bulkUploadResults) elements.bulkUploadResults.classList.add('d-none'); + if (elements.uploadSubmitBtn) elements.uploadSubmitBtn.disabled = true; + if (elements.uploadFileInput) elements.uploadFileInput.disabled = true; + + const successFiles = []; + const errorFiles = []; + const total = files.length; + + updateFloatingProgress(0, total, files[0]?.name || ''); + + for (let i = 0; i < total; i++) { + const file = files[i]; + const current = i + 1; + + if (elements.bulkUploadCounter) elements.bulkUploadCounter.textContent = `${current}/${total}`; + if (elements.bulkUploadCurrentFile) elements.bulkUploadCurrentFile.textContent = `Uploading: ${file.name}`; + if (elements.bulkUploadProgressBar) { + const percent = Math.round((current / total) * 100); + elements.bulkUploadProgressBar.style.width = `${percent}%`; + } + updateFloatingProgress(i, total, file.name); + + try { + await uploadSingleFile(file, keyPrefix, metadata, null, urls); + successFiles.push(file.name); + } catch (error) { + errorFiles.push({ name: file.name, error: error.message || 'Unknown error' }); + } + } + updateFloatingProgress(total, total); + + if (elements.bulkUploadProgress) elements.bulkUploadProgress.classList.add('d-none'); + if (elements.bulkUploadResults) elements.bulkUploadResults.classList.remove('d-none'); + + if (elements.bulkUploadSuccessCount) elements.bulkUploadSuccessCount.textContent = successFiles.length; + if (successFiles.length === 0 && elements.bulkUploadSuccessAlert) { + elements.bulkUploadSuccessAlert.classList.add('d-none'); + } + + if (errorFiles.length > 0) { + if (elements.bulkUploadErrorCount) elements.bulkUploadErrorCount.textContent = errorFiles.length; + if (elements.bulkUploadErrorAlert) elements.bulkUploadErrorAlert.classList.remove('d-none'); + if (elements.bulkUploadErrorList) { + elements.bulkUploadErrorList.innerHTML = errorFiles + .map(f => `
  • ${callbacks.escapeHtml(f.name)}: ${callbacks.escapeHtml(f.error)}
  • `) + .join(''); + } + } + + state.isUploading = false; + setUploadLockState(false); + + if (successFiles.length > 0) { + if (elements.uploadBtnText) elements.uploadBtnText.textContent = 'Refreshing...'; + callbacks.onUploadComplete(successFiles, errorFiles); + } else { + if (elements.uploadSubmitBtn) elements.uploadSubmitBtn.disabled = false; + if (elements.uploadFileInput) elements.uploadFileInput.disabled = false; + } + } + + function setupEventListeners() { + if (elements.uploadFileInput) { + elements.uploadFileInput.addEventListener('change', () => { + if (state.isUploading) return; + refreshUploadDropLabel(); + updateUploadBtnText(); + resetUploadUI(); + }); + } + + if (elements.uploadDropZone) { + elements.uploadDropZone.addEventListener('click', () => { + if (state.isUploading) return; + elements.uploadFileInput?.click(); + }); + } + + if (elements.floatingProgressExpand) { + elements.floatingProgressExpand.addEventListener('click', () => { + if (elements.uploadModal) { + elements.uploadModal.show(); + } + }); + } + + if (elements.uploadModalEl) { + elements.uploadModalEl.addEventListener('hide.bs.modal', () => { + if (state.isUploading) { + showFloatingProgress(); + } + }); + + elements.uploadModalEl.addEventListener('hidden.bs.modal', () => { + if (!state.isUploading) { + resetUploadUI(); + if (elements.uploadFileInput) elements.uploadFileInput.value = ''; + refreshUploadDropLabel(); + updateUploadBtnText(); + } + }); + + elements.uploadModalEl.addEventListener('show.bs.modal', () => { + if (state.isUploading) { + hideFloatingProgress(); + } + if (callbacks.hasFolders() && callbacks.getCurrentPrefix()) { + if (elements.uploadKeyPrefix) { + elements.uploadKeyPrefix.value = callbacks.getCurrentPrefix(); + } + } else if (elements.uploadKeyPrefix) { + elements.uploadKeyPrefix.value = ''; + } + }); + } + } + + function wireDropTarget(target, options) { + const { highlightClass = '', autoOpenModal = false } = options || {}; + if (!target) return; + + const preventDefaults = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + + ['dragenter', 'dragover'].forEach((eventName) => { + target.addEventListener(eventName, (event) => { + preventDefaults(event); + if (state.isUploading) return; + if (highlightClass) { + target.classList.add(highlightClass); + } + }); + }); + + ['dragleave', 'drop'].forEach((eventName) => { + target.addEventListener(eventName, (event) => { + preventDefaults(event); + if (highlightClass) { + target.classList.remove(highlightClass); + } + }); + }); + + target.addEventListener('drop', (event) => { + if (state.isUploading) return; + if (!event.dataTransfer?.files?.length || !elements.uploadFileInput) { + return; + } + elements.uploadFileInput.files = event.dataTransfer.files; + elements.uploadFileInput.dispatchEvent(new Event('change', { bubbles: true })); + if (autoOpenModal && elements.uploadModal) { + elements.uploadModal.show(); + } + }); + } + + return { + init: init, + isUploading: isUploading, + performBulkUpload: performBulkUpload, + wireDropTarget: wireDropTarget, + resetUploadUI: resetUploadUI, + refreshUploadDropLabel: refreshUploadDropLabel, + updateUploadBtnText: updateUploadBtnText + }; +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-utils.js b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-utils.js new file mode 100644 index 0000000..43cc91e --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/bucket-detail-utils.js @@ -0,0 +1,120 @@ +window.BucketDetailUtils = (function() { + 'use strict'; + + function setupJsonAutoIndent(textarea) { + if (!textarea) return; + + textarea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + + const start = this.selectionStart; + const end = this.selectionEnd; + const value = this.value; + + const lineStart = value.lastIndexOf('\n', start - 1) + 1; + const currentLine = value.substring(lineStart, start); + + const indentMatch = currentLine.match(/^(\s*)/); + let indent = indentMatch ? indentMatch[1] : ''; + + const trimmedLine = currentLine.trim(); + const lastChar = trimmedLine.slice(-1); + + let newIndent = indent; + let insertAfter = ''; + + if (lastChar === '{' || lastChar === '[') { + newIndent = indent + ' '; + + const charAfterCursor = value.substring(start, start + 1).trim(); + if ((lastChar === '{' && charAfterCursor === '}') || + (lastChar === '[' && charAfterCursor === ']')) { + insertAfter = '\n' + indent; + } + } else if (lastChar === ',' || lastChar === ':') { + newIndent = indent; + } + + const insertion = '\n' + newIndent + insertAfter; + const newValue = value.substring(0, start) + insertion + value.substring(end); + + this.value = newValue; + + const newCursorPos = start + 1 + newIndent.length; + this.selectionStart = this.selectionEnd = newCursorPos; + + this.dispatchEvent(new Event('input', { bubbles: true })); + } + + if (e.key === 'Tab') { + e.preventDefault(); + const start = this.selectionStart; + const end = this.selectionEnd; + + if (e.shiftKey) { + const lineStart = this.value.lastIndexOf('\n', start - 1) + 1; + const lineContent = this.value.substring(lineStart, start); + if (lineContent.startsWith(' ')) { + this.value = this.value.substring(0, lineStart) + + this.value.substring(lineStart + 2); + this.selectionStart = this.selectionEnd = Math.max(lineStart, start - 2); + } + } else { + this.value = this.value.substring(0, start) + ' ' + this.value.substring(end); + this.selectionStart = this.selectionEnd = start + 2; + } + + this.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + } + + function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return `${bytes} bytes`; + const units = ['bytes', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; + } + + function escapeHtml(value) { + if (value === null || value === undefined) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function fallbackCopy(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + textArea.style.top = '-9999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + let success = false; + try { + success = document.execCommand('copy'); + } catch { + success = false; + } + document.body.removeChild(textArea); + return success; + } + + return { + setupJsonAutoIndent: setupJsonAutoIndent, + formatBytes: formatBytes, + escapeHtml: escapeHtml, + fallbackCopy: fallbackCopy + }; +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/connections-management.js b/rust/myfsio-engine/crates/myfsio-server/static/js/connections-management.js new file mode 100644 index 0000000..8d22a6c --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/connections-management.js @@ -0,0 +1,343 @@ +window.ConnectionsManagement = (function() { + 'use strict'; + + var endpoints = {}; + var csrfToken = ''; + + function init(config) { + endpoints = config.endpoints || {}; + csrfToken = config.csrfToken || ''; + + setupEventListeners(); + checkAllConnectionHealth(); + } + + function togglePassword(id) { + var input = document.getElementById(id); + if (input) { + input.type = input.type === 'password' ? 'text' : 'password'; + } + } + + async function testConnection(formId, resultId) { + var form = document.getElementById(formId); + var resultDiv = document.getElementById(resultId); + if (!form || !resultDiv) return; + + var formData = new FormData(form); + var data = {}; + formData.forEach(function(value, key) { + if (key !== 'csrf_token') { + data[key] = value; + } + }); + + resultDiv.innerHTML = '
    Testing connection...
    '; + + var controller = new AbortController(); + var timeoutId = setTimeout(function() { controller.abort(); }, 20000); + + try { + var response = await fetch(endpoints.test, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(data), + signal: controller.signal + }); + clearTimeout(timeoutId); + + var result = await response.json(); + if (response.ok) { + resultDiv.innerHTML = '
    ' + + '' + + '' + + '' + window.UICore.escapeHtml(result.message) + '
    '; + } else { + resultDiv.innerHTML = '
    ' + + '' + + '' + + '' + window.UICore.escapeHtml(result.message) + '
    '; + } + } catch (error) { + clearTimeout(timeoutId); + var message = error.name === 'AbortError' + ? 'Connection test timed out - endpoint may be unreachable' + : 'Connection failed: Network error'; + resultDiv.innerHTML = '
    ' + + '' + + '' + + '' + message + '
    '; + } + } + + async function checkConnectionHealth(connectionId, statusEl) { + if (!statusEl) return; + + try { + var controller = new AbortController(); + var timeoutId = setTimeout(function() { controller.abort(); }, 10000); + + var response = await fetch(endpoints.healthTemplate.replace('CONNECTION_ID', connectionId), { + signal: controller.signal + }); + clearTimeout(timeoutId); + + var data = await response.json(); + if (data.healthy) { + statusEl.innerHTML = '' + + ''; + statusEl.setAttribute('data-status', 'healthy'); + statusEl.setAttribute('title', 'Connected'); + } else { + statusEl.innerHTML = '' + + ''; + statusEl.setAttribute('data-status', 'unhealthy'); + statusEl.setAttribute('title', data.error || 'Unreachable'); + } + } catch (error) { + statusEl.innerHTML = '' + + ''; + statusEl.setAttribute('data-status', 'unknown'); + statusEl.setAttribute('title', 'Could not check status'); + } + } + + function checkAllConnectionHealth() { + var rows = document.querySelectorAll('tr[data-connection-id]'); + rows.forEach(function(row, index) { + var connectionId = row.getAttribute('data-connection-id'); + var statusEl = row.querySelector('.connection-status'); + if (statusEl) { + setTimeout(function() { + checkConnectionHealth(connectionId, statusEl); + }, index * 200); + } + }); + } + + function updateConnectionCount() { + var countBadge = document.querySelector('.badge.bg-primary.bg-opacity-10.text-primary.fs-6'); + if (countBadge) { + var remaining = document.querySelectorAll('tr[data-connection-id]').length; + countBadge.textContent = remaining + ' connection' + (remaining !== 1 ? 's' : ''); + } + } + + function createConnectionRowHtml(conn) { + var ak = conn.access_key || ''; + var maskedKey = ak.length > 12 ? ak.slice(0, 8) + '...' + ak.slice(-4) : ak; + + return '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '' + window.UICore.escapeHtml(conn.name) + '' + + '
    ' + + '' + window.UICore.escapeHtml(conn.endpoint_url) + '' + + '' + window.UICore.escapeHtml(conn.region) + '' + + '' + window.UICore.escapeHtml(maskedKey) + '' + + '
    ' + + '' + + '' + + '
    '; + } + + function setupEventListeners() { + var testBtn = document.getElementById('testConnectionBtn'); + if (testBtn) { + testBtn.addEventListener('click', function() { + testConnection('createConnectionForm', 'testResult'); + }); + } + + var editTestBtn = document.getElementById('editTestConnectionBtn'); + if (editTestBtn) { + editTestBtn.addEventListener('click', function() { + testConnection('editConnectionForm', 'editTestResult'); + }); + } + + var editModal = document.getElementById('editConnectionModal'); + if (editModal) { + editModal.addEventListener('show.bs.modal', function(event) { + var button = event.relatedTarget; + if (!button) return; + + var id = button.getAttribute('data-id'); + + document.getElementById('edit_name').value = button.getAttribute('data-name') || ''; + document.getElementById('edit_endpoint_url').value = button.getAttribute('data-endpoint') || ''; + document.getElementById('edit_region').value = button.getAttribute('data-region') || ''; + document.getElementById('edit_access_key').value = button.getAttribute('data-access') || ''; + document.getElementById('edit_secret_key').value = ''; + document.getElementById('edit_secret_key').placeholder = '(unchanged — leave blank to keep current)'; + document.getElementById('edit_secret_key').required = false; + document.getElementById('editTestResult').innerHTML = ''; + + var form = document.getElementById('editConnectionForm'); + form.action = endpoints.updateTemplate.replace('CONNECTION_ID', id); + }); + } + + var deleteModal = document.getElementById('deleteConnectionModal'); + if (deleteModal) { + deleteModal.addEventListener('show.bs.modal', function(event) { + var button = event.relatedTarget; + if (!button) return; + + var id = button.getAttribute('data-id'); + var name = button.getAttribute('data-name'); + + document.getElementById('deleteConnectionName').textContent = name; + var form = document.getElementById('deleteConnectionForm'); + form.action = endpoints.deleteTemplate.replace('CONNECTION_ID', id); + }); + } + + var createForm = document.getElementById('createConnectionForm'); + if (createForm) { + createForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.UICore.submitFormAjax(createForm, { + successMessage: 'Connection created', + onSuccess: function(data) { + createForm.reset(); + document.getElementById('testResult').innerHTML = ''; + + if (data.connection) { + var emptyState = document.querySelector('.empty-state'); + if (emptyState) { + var cardBody = emptyState.closest('.card-body'); + if (cardBody) { + cardBody.innerHTML = '
    ' + + '' + + '' + + '' + + '' + + '' + + '
    StatusNameEndpointRegionAccess KeyActions
    '; + } + } + + var tbody = document.querySelector('table tbody'); + if (tbody) { + tbody.insertAdjacentHTML('beforeend', createConnectionRowHtml(data.connection)); + var newRow = tbody.lastElementChild; + var statusEl = newRow.querySelector('.connection-status'); + if (statusEl) { + checkConnectionHealth(data.connection.id, statusEl); + } + } + updateConnectionCount(); + } else { + location.reload(); + } + } + }); + }); + } + + var editForm = document.getElementById('editConnectionForm'); + if (editForm) { + editForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.UICore.submitFormAjax(editForm, { + successMessage: 'Connection updated', + onSuccess: function(data) { + var modal = bootstrap.Modal.getInstance(document.getElementById('editConnectionModal')); + if (modal) modal.hide(); + + var connId = editForm.action.split('/').slice(-2)[0]; + var row = document.querySelector('tr[data-connection-id="' + connId + '"]'); + if (row && data.connection) { + var nameCell = row.querySelector('.fw-medium'); + if (nameCell) nameCell.textContent = data.connection.name; + + var endpointCell = row.querySelector('.text-truncate'); + if (endpointCell) { + endpointCell.textContent = data.connection.endpoint_url; + endpointCell.title = data.connection.endpoint_url; + } + + var regionBadge = row.querySelector('.badge.bg-primary'); + if (regionBadge) regionBadge.textContent = data.connection.region; + + var accessCode = row.querySelector('code.small'); + if (accessCode && data.connection.access_key) { + var ak = data.connection.access_key; + accessCode.textContent = ak.slice(0, 8) + '...' + ak.slice(-4); + } + + var editBtn = row.querySelector('[data-bs-target="#editConnectionModal"]'); + if (editBtn) { + editBtn.setAttribute('data-name', data.connection.name); + editBtn.setAttribute('data-endpoint', data.connection.endpoint_url); + editBtn.setAttribute('data-region', data.connection.region); + editBtn.setAttribute('data-access', data.connection.access_key); + } + + var deleteBtn = row.querySelector('[data-bs-target="#deleteConnectionModal"]'); + if (deleteBtn) { + deleteBtn.setAttribute('data-name', data.connection.name); + } + + var statusEl = row.querySelector('.connection-status'); + if (statusEl) { + checkConnectionHealth(connId, statusEl); + } + } + } + }); + }); + } + + var deleteForm = document.getElementById('deleteConnectionForm'); + if (deleteForm) { + deleteForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.UICore.submitFormAjax(deleteForm, { + successMessage: 'Connection deleted', + onSuccess: function(data) { + var modal = bootstrap.Modal.getInstance(document.getElementById('deleteConnectionModal')); + if (modal) modal.hide(); + + var connId = deleteForm.action.split('/').slice(-2)[0]; + var row = document.querySelector('tr[data-connection-id="' + connId + '"]'); + if (row) { + row.remove(); + } + + updateConnectionCount(); + + if (document.querySelectorAll('tr[data-connection-id]').length === 0) { + location.reload(); + } + } + }); + }); + } + } + + return { + init: init, + togglePassword: togglePassword, + testConnection: testConnection, + checkConnectionHealth: checkConnectionHealth + }; +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/iam-management.js b/rust/myfsio-engine/crates/myfsio-server/static/js/iam-management.js new file mode 100644 index 0000000..ce9b8f3 --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/iam-management.js @@ -0,0 +1,846 @@ +window.IAMManagement = (function() { + 'use strict'; + + var users = []; + var currentUserKey = null; + var endpoints = {}; + var csrfToken = ''; + var iamLocked = false; + + var policyModal = null; + var editUserModal = null; + var deleteUserModal = null; + var rotateSecretModal = null; + var expiryModal = null; + var currentRotateKey = null; + var currentEditKey = null; + var currentDeleteKey = null; + var currentEditAccessKey = null; + var currentDeleteAccessKey = null; + var currentExpiryKey = null; + var currentExpiryAccessKey = null; + + var ALL_S3_ACTIONS = [ + 'list', 'read', 'write', 'delete', 'share', 'policy', + 'replication', 'lifecycle', 'cors', + 'create_bucket', 'delete_bucket', + 'versioning', 'tagging', 'encryption', 'quota', + 'object_lock', 'notification', 'logging', 'website' + ]; + + var policyTemplates = { + full: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'create_bucket', 'delete_bucket', 'replication', 'lifecycle', 'cors', 'versioning', 'tagging', 'encryption', 'quota', 'object_lock', 'notification', 'logging', 'website', 'iam:*'] }], + readonly: [{ bucket: '*', actions: ['list', 'read'] }], + writer: [{ bucket: '*', actions: ['list', 'read', 'write'] }], + operator: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'create_bucket', 'delete_bucket'] }], + bucketadmin: [{ bucket: '*', actions: ['list', 'read', 'write', 'delete', 'share', 'policy', 'create_bucket', 'delete_bucket', 'versioning', 'tagging', 'encryption', 'cors', 'lifecycle', 'quota', 'object_lock', 'notification', 'logging', 'website', 'replication'] }] + }; + + function isAdminUser(policies) { + if (!policies || !policies.length) return false; + return policies.some(function(p) { + return p.actions && (p.actions.indexOf('iam:*') >= 0 || p.actions.indexOf('*') >= 0); + }); + } + + function getPermissionLevel(actions) { + if (!actions || !actions.length) return 'Custom (0)'; + if (actions.indexOf('*') >= 0) return 'Full Access'; + if (actions.length >= ALL_S3_ACTIONS.length) { + var hasAll = ALL_S3_ACTIONS.every(function(a) { return actions.indexOf(a) >= 0; }); + if (hasAll) return 'Full Access'; + } + var has = function(a) { return actions.indexOf(a) >= 0; }; + if (has('list') && has('read') && has('write') && has('delete')) return 'Read + Write + Delete'; + if (has('list') && has('read') && has('write')) return 'Read + Write'; + if (has('list') && has('read')) return 'Read Only'; + return 'Custom (' + actions.length + ')'; + } + + function getBucketLabel(bucket) { + return bucket === '*' ? 'All Buckets' : bucket; + } + + function buildUserUrl(template, userId) { + return template.replace('USER_ID', encodeURIComponent(userId)); + } + + function getUserByIdentifier(identifier) { + return users.find(function(u) { + return u.user_id === identifier || u.access_key === identifier; + }) || null; + } + + function getUserById(userId) { + return users.find(function(u) { return u.user_id === userId; }) || null; + } + + function init(config) { + users = config.users || []; + currentUserKey = config.currentUserKey || null; + endpoints = config.endpoints || {}; + csrfToken = config.csrfToken || ''; + iamLocked = config.iamLocked || false; + + if (iamLocked) return; + + initModals(); + setupJsonAutoIndent(); + setupCopyButtons(); + setupPolicyEditor(); + setupCreateUserModal(); + setupEditUserModal(); + setupDeleteUserModal(); + setupRotateSecretModal(); + setupExpiryModal(); + setupFormHandlers(); + setupSearch(); + setupCopyAccessKeyButtons(); + } + + function initModals() { + var policyModalEl = document.getElementById('policyEditorModal'); + var editModalEl = document.getElementById('editUserModal'); + var deleteModalEl = document.getElementById('deleteUserModal'); + var rotateModalEl = document.getElementById('rotateSecretModal'); + var expiryModalEl = document.getElementById('expiryModal'); + + if (policyModalEl) policyModal = new bootstrap.Modal(policyModalEl); + if (editModalEl) editUserModal = new bootstrap.Modal(editModalEl); + if (deleteModalEl) deleteUserModal = new bootstrap.Modal(deleteModalEl); + if (rotateModalEl) rotateSecretModal = new bootstrap.Modal(rotateModalEl); + if (expiryModalEl) expiryModal = new bootstrap.Modal(expiryModalEl); + } + + function setupJsonAutoIndent() { + window.UICore.setupJsonAutoIndent(document.getElementById('policyEditorDocument')); + window.UICore.setupJsonAutoIndent(document.getElementById('createUserPolicies')); + } + + function setupCopyButtons() { + document.querySelectorAll('.config-copy').forEach(function(button) { + button.addEventListener('click', async function() { + var targetId = button.dataset.copyTarget; + var target = document.getElementById(targetId); + if (!target) return; + await window.UICore.copyToClipboard(target.innerText, button, 'Copy JSON'); + }); + }); + + var accessKeyCopyButton = document.querySelector('[data-access-key-copy]'); + if (accessKeyCopyButton) { + accessKeyCopyButton.addEventListener('click', async function() { + var accessKeyInput = document.getElementById('disclosedAccessKeyValue'); + if (!accessKeyInput) return; + await window.UICore.copyToClipboard(accessKeyInput.value, accessKeyCopyButton, 'Copy'); + }); + } + + var secretCopyButton = document.querySelector('[data-secret-copy]'); + if (secretCopyButton) { + secretCopyButton.addEventListener('click', async function() { + var secretInput = document.getElementById('disclosedSecretValue'); + if (!secretInput) return; + await window.UICore.copyToClipboard(secretInput.value, secretCopyButton, 'Copy'); + }); + } + } + + function getUserPolicies(identifier) { + var user = getUserByIdentifier(identifier); + return user ? JSON.stringify(user.policies, null, 2) : ''; + } + + function applyPolicyTemplate(name, textareaEl) { + if (policyTemplates[name] && textareaEl) { + textareaEl.value = JSON.stringify(policyTemplates[name], null, 2); + } + } + + function setupPolicyEditor() { + var userLabelEl = document.getElementById('policyEditorUserLabel'); + var userInputEl = document.getElementById('policyEditorUserId'); + var textareaEl = document.getElementById('policyEditorDocument'); + + document.querySelectorAll('[data-policy-template]').forEach(function(button) { + button.addEventListener('click', function() { + applyPolicyTemplate(button.dataset.policyTemplate, textareaEl); + }); + }); + + document.querySelectorAll('[data-policy-editor]').forEach(function(button) { + button.addEventListener('click', function() { + var userId = button.dataset.userId; + var accessKey = button.dataset.accessKey || userId; + if (!userId) return; + + userLabelEl.textContent = accessKey; + userInputEl.value = userId; + textareaEl.value = getUserPolicies(userId); + + policyModal.show(); + }); + }); + } + + function generateSecureHex(byteCount) { + var arr = new Uint8Array(byteCount); + crypto.getRandomValues(arr); + return Array.from(arr).map(function(b) { return b.toString(16).padStart(2, '0'); }).join(''); + } + + function generateSecureBase64(byteCount) { + var arr = new Uint8Array(byteCount); + crypto.getRandomValues(arr); + var binary = ''; + for (var i = 0; i < arr.length; i++) { + binary += String.fromCharCode(arr[i]); + } + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + + function setupCreateUserModal() { + var createUserPoliciesEl = document.getElementById('createUserPolicies'); + + document.querySelectorAll('[data-create-policy-template]').forEach(function(button) { + button.addEventListener('click', function() { + applyPolicyTemplate(button.dataset.createPolicyTemplate, createUserPoliciesEl); + }); + }); + + var genAccessKeyBtn = document.getElementById('generateAccessKeyBtn'); + if (genAccessKeyBtn) { + genAccessKeyBtn.addEventListener('click', function() { + var input = document.getElementById('createUserAccessKey'); + if (input) input.value = generateSecureHex(8); + }); + } + + var genSecretKeyBtn = document.getElementById('generateSecretKeyBtn'); + if (genSecretKeyBtn) { + genSecretKeyBtn.addEventListener('click', function() { + var input = document.getElementById('createUserSecretKey'); + if (input) input.value = generateSecureBase64(24); + }); + } + } + + function setupEditUserModal() { + var editUserForm = document.getElementById('editUserForm'); + var editUserDisplayName = document.getElementById('editUserDisplayName'); + + document.querySelectorAll('[data-edit-user]').forEach(function(btn) { + btn.addEventListener('click', function() { + var key = btn.dataset.userId; + var accessKey = btn.dataset.accessKey || key; + var name = btn.dataset.displayName; + currentEditKey = key; + currentEditAccessKey = accessKey; + editUserDisplayName.value = name; + editUserForm.action = buildUserUrl(endpoints.updateUser, key); + editUserModal.show(); + }); + }); + } + + function setupDeleteUserModal() { + var deleteUserForm = document.getElementById('deleteUserForm'); + var deleteUserLabel = document.getElementById('deleteUserLabel'); + var deleteSelfWarning = document.getElementById('deleteSelfWarning'); + + document.querySelectorAll('[data-delete-user]').forEach(function(btn) { + btn.addEventListener('click', function() { + var key = btn.dataset.userId; + var accessKey = btn.dataset.accessKey || key; + currentDeleteKey = key; + currentDeleteAccessKey = accessKey; + deleteUserLabel.textContent = accessKey; + deleteUserForm.action = buildUserUrl(endpoints.deleteUser, key); + + if (accessKey === currentUserKey) { + deleteSelfWarning.classList.remove('d-none'); + } else { + deleteSelfWarning.classList.add('d-none'); + } + + deleteUserModal.show(); + }); + }); + } + + function setupRotateSecretModal() { + var rotateUserLabel = document.getElementById('rotateUserLabel'); + var confirmRotateBtn = document.getElementById('confirmRotateBtn'); + var rotateCancelBtn = document.getElementById('rotateCancelBtn'); + var rotateDoneBtn = document.getElementById('rotateDoneBtn'); + var rotateSecretConfirm = document.getElementById('rotateSecretConfirm'); + var rotateSecretResult = document.getElementById('rotateSecretResult'); + var newSecretKeyInput = document.getElementById('newSecretKey'); + var copyNewSecretBtn = document.getElementById('copyNewSecret'); + + document.querySelectorAll('[data-rotate-user]').forEach(function(btn) { + btn.addEventListener('click', function() { + currentRotateKey = btn.dataset.userId; + rotateUserLabel.textContent = btn.dataset.accessKey || currentRotateKey; + + rotateSecretConfirm.classList.remove('d-none'); + rotateSecretResult.classList.add('d-none'); + confirmRotateBtn.classList.remove('d-none'); + rotateCancelBtn.classList.remove('d-none'); + rotateDoneBtn.classList.add('d-none'); + + rotateSecretModal.show(); + }); + }); + + if (confirmRotateBtn) { + confirmRotateBtn.addEventListener('click', async function() { + if (!currentRotateKey) return; + + window.UICore.setButtonLoading(confirmRotateBtn, true, 'Rotating...'); + + try { + var url = buildUserUrl(endpoints.rotateSecret, currentRotateKey); + var response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'X-CSRFToken': csrfToken + } + }); + + if (!response.ok) { + var data = await response.json(); + throw new Error(data.error || 'Failed to rotate secret'); + } + + var data = await response.json(); + newSecretKeyInput.value = data.secret_key; + + rotateSecretConfirm.classList.add('d-none'); + rotateSecretResult.classList.remove('d-none'); + confirmRotateBtn.classList.add('d-none'); + rotateCancelBtn.classList.add('d-none'); + rotateDoneBtn.classList.remove('d-none'); + + } catch (err) { + if (window.showToast) { + window.showToast(err.message, 'Error', 'danger'); + } + rotateSecretModal.hide(); + } finally { + window.UICore.setButtonLoading(confirmRotateBtn, false); + } + }); + } + + if (copyNewSecretBtn) { + copyNewSecretBtn.addEventListener('click', async function() { + await window.UICore.copyToClipboard(newSecretKeyInput.value, copyNewSecretBtn, 'Copy'); + }); + } + + if (rotateDoneBtn) { + rotateDoneBtn.addEventListener('click', function() { + window.location.reload(); + }); + } + } + + function openExpiryModal(key, expiresAt) { + currentExpiryKey = key; + var user = getUserByIdentifier(key); + var label = document.getElementById('expiryUserLabel'); + var input = document.getElementById('expiryDateInput'); + var form = document.getElementById('expiryForm'); + if (label) label.textContent = currentExpiryAccessKey || (user ? user.access_key : key); + if (expiresAt) { + try { + var dt = new Date(expiresAt); + var local = new Date(dt.getTime() - dt.getTimezoneOffset() * 60000); + if (input) input.value = local.toISOString().slice(0, 16); + } catch(e) { + if (input) input.value = ''; + } + } else { + if (input) input.value = ''; + } + if (form) form.action = buildUserUrl(endpoints.updateExpiry, key); + var modalEl = document.getElementById('expiryModal'); + if (modalEl) { + var modal = bootstrap.Modal.getOrCreateInstance(modalEl); + modal.show(); + } + } + + function setupExpiryModal() { + document.querySelectorAll('[data-expiry-user]').forEach(function(btn) { + btn.addEventListener('click', function(e) { + e.preventDefault(); + currentExpiryAccessKey = btn.dataset.accessKey || btn.dataset.userId; + openExpiryModal(btn.dataset.userId, btn.dataset.expiresAt || ''); + }); + }); + + document.querySelectorAll('[data-expiry-preset]').forEach(function(btn) { + btn.addEventListener('click', function() { + var preset = btn.dataset.expiryPreset; + var input = document.getElementById('expiryDateInput'); + if (!input) return; + if (preset === 'clear') { + input.value = ''; + return; + } + var now = new Date(); + var ms = 0; + if (preset === '1h') ms = 3600000; + else if (preset === '24h') ms = 86400000; + else if (preset === '7d') ms = 7 * 86400000; + else if (preset === '30d') ms = 30 * 86400000; + else if (preset === '90d') ms = 90 * 86400000; + var future = new Date(now.getTime() + ms); + var local = new Date(future.getTime() - future.getTimezoneOffset() * 60000); + input.value = local.toISOString().slice(0, 16); + }); + }); + + var expiryForm = document.getElementById('expiryForm'); + if (expiryForm) { + expiryForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.UICore.submitFormAjax(expiryForm, { + successMessage: 'Expiry updated', + onSuccess: function() { + var modalEl = document.getElementById('expiryModal'); + if (modalEl) bootstrap.Modal.getOrCreateInstance(modalEl).hide(); + window.location.reload(); + } + }); + }); + } + } + + function createUserCardHtml(user) { + var userId = user.user_id || ''; + var accessKey = user.access_key || userId; + var displayName = user.display_name || accessKey; + var policies = user.policies || []; + var expiresAt = user.expires_at || ''; + var admin = isAdminUser(policies); + var cardClass = 'card h-100 iam-user-card' + (admin ? ' iam-admin-card' : ''); + var roleBadge = admin + ? 'Admin' + : 'User'; + + var policyBadges = ''; + if (policies && policies.length > 0) { + policyBadges = policies.map(function(p) { + var bucketLabel = getBucketLabel(p.bucket); + var permLevel = getPermissionLevel(p.actions); + return '' + + '' + + '' + + '' + window.UICore.escapeHtml(bucketLabel) + ' · ' + window.UICore.escapeHtml(permLevel) + ''; + }).join(''); + } else { + policyBadges = 'No policies'; + } + + var esc = window.UICore.escapeHtml; + return '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + esc(displayName) + '
    ' + + roleBadge + + '
    ' + + '
    ' + + '' + esc(accessKey) + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    Bucket Permissions
    ' + + '
    ' + policyBadges + '
    ' + + '' + + '
    '; + } + + function attachUserCardHandlers(cardElement, user) { + var userId = user.user_id; + var accessKey = user.access_key; + var displayName = user.display_name; + var expiresAt = user.expires_at || ''; + var editBtn = cardElement.querySelector('[data-edit-user]'); + if (editBtn) { + editBtn.addEventListener('click', function() { + currentEditKey = userId; + currentEditAccessKey = accessKey; + document.getElementById('editUserDisplayName').value = displayName; + document.getElementById('editUserForm').action = buildUserUrl(endpoints.updateUser, userId); + editUserModal.show(); + }); + } + + var deleteBtn = cardElement.querySelector('[data-delete-user]'); + if (deleteBtn) { + deleteBtn.addEventListener('click', function() { + currentDeleteKey = userId; + currentDeleteAccessKey = accessKey; + document.getElementById('deleteUserLabel').textContent = accessKey; + document.getElementById('deleteUserForm').action = buildUserUrl(endpoints.deleteUser, userId); + var deleteSelfWarning = document.getElementById('deleteSelfWarning'); + if (accessKey === currentUserKey) { + deleteSelfWarning.classList.remove('d-none'); + } else { + deleteSelfWarning.classList.add('d-none'); + } + deleteUserModal.show(); + }); + } + + var rotateBtn = cardElement.querySelector('[data-rotate-user]'); + if (rotateBtn) { + rotateBtn.addEventListener('click', function() { + currentRotateKey = userId; + document.getElementById('rotateUserLabel').textContent = accessKey; + document.getElementById('rotateSecretConfirm').classList.remove('d-none'); + document.getElementById('rotateSecretResult').classList.add('d-none'); + document.getElementById('confirmRotateBtn').classList.remove('d-none'); + document.getElementById('rotateCancelBtn').classList.remove('d-none'); + document.getElementById('rotateDoneBtn').classList.add('d-none'); + rotateSecretModal.show(); + }); + } + + var expiryBtn = cardElement.querySelector('[data-expiry-user]'); + if (expiryBtn) { + expiryBtn.addEventListener('click', function(e) { + e.preventDefault(); + currentExpiryAccessKey = accessKey; + openExpiryModal(userId, expiresAt); + }); + } + + var policyBtn = cardElement.querySelector('[data-policy-editor]'); + if (policyBtn) { + policyBtn.addEventListener('click', function() { + document.getElementById('policyEditorUserLabel').textContent = accessKey; + document.getElementById('policyEditorUserId').value = userId; + document.getElementById('policyEditorDocument').value = getUserPolicies(userId); + policyModal.show(); + }); + } + + var copyBtn = cardElement.querySelector('[data-copy-access-key]'); + if (copyBtn) { + copyBtn.addEventListener('click', function() { + copyAccessKey(copyBtn); + }); + } + } + + function updateUserCount() { + var countEl = document.querySelector('.card-header .text-muted.small'); + if (countEl) { + var count = document.querySelectorAll('.iam-user-card').length; + countEl.textContent = count + ' user' + (count !== 1 ? 's' : '') + ' configured'; + } + } + + function setupFormHandlers() { + var createUserForm = document.querySelector('#createUserModal form'); + if (createUserForm) { + createUserForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.UICore.submitFormAjax(createUserForm, { + successMessage: 'User created', + onSuccess: function(data) { + var modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal')); + if (modal) modal.hide(); + createUserForm.reset(); + + var existingAlert = document.querySelector('.alert.alert-info.border-0.shadow-sm'); + if (existingAlert) existingAlert.remove(); + + if (data.secret_key) { + var alertHtml = ''; + var container = document.querySelector('.page-header'); + if (container) { + container.insertAdjacentHTML('afterend', alertHtml); + document.getElementById('copyNewUserAccessKey').addEventListener('click', async function() { + await window.UICore.copyToClipboard(data.access_key, this, 'Copy'); + }); + document.getElementById('copyNewUserSecret').addEventListener('click', async function() { + await window.UICore.copyToClipboard(data.secret_key, this, 'Copy'); + }); + } + } + + var usersGrid = document.querySelector('.row.g-3'); + var emptyState = document.querySelector('.empty-state'); + if (emptyState) { + var emptyCol = emptyState.closest('.col-12'); + if (emptyCol) emptyCol.remove(); + if (!usersGrid) { + var cardBody = document.querySelector('.card-body.px-4.pb-4'); + if (cardBody) { + cardBody.innerHTML = '
    '; + usersGrid = cardBody.querySelector('.row.g-3'); + } + } + } + + if (usersGrid) { + var newUser = { + user_id: data.user_id, + access_key: data.access_key, + display_name: data.display_name, + expires_at: data.expires_at || '', + policies: data.policies || [] + }; + var cardHtml = createUserCardHtml(newUser); + usersGrid.insertAdjacentHTML('beforeend', cardHtml); + var newCard = usersGrid.lastElementChild; + attachUserCardHandlers(newCard, newUser); + users.push(newUser); + updateUserCount(); + } + } + }); + }); + } + + var policyEditorForm = document.getElementById('policyEditorForm'); + if (policyEditorForm) { + policyEditorForm.addEventListener('submit', function(e) { + e.preventDefault(); + var userInputEl = document.getElementById('policyEditorUserId'); + var userId = userInputEl.value; + if (!userId) return; + + var template = policyEditorForm.dataset.actionTemplate; + policyEditorForm.action = template.replace('USER_ID_PLACEHOLDER', encodeURIComponent(userId)); + + window.UICore.submitFormAjax(policyEditorForm, { + successMessage: 'Policies updated', + onSuccess: function(data) { + policyModal.hide(); + + var userCard = document.querySelector('.iam-user-item[data-user-id="' + userId + '"]'); + if (userCard) { + var cardEl = userCard.querySelector('.iam-user-card'); + var badgeContainer = cardEl ? cardEl.querySelector('[data-policy-badges]') : null; + if (badgeContainer && data.policies) { + var badges = data.policies.map(function(p) { + var bl = getBucketLabel(p.bucket); + var pl = getPermissionLevel(p.actions); + return '' + + '' + + '' + + '' + window.UICore.escapeHtml(bl) + ' · ' + window.UICore.escapeHtml(pl) + ''; + }).join(''); + badgeContainer.innerHTML = badges || 'No policies'; + } + if (cardEl) { + var nowAdmin = isAdminUser(data.policies); + cardEl.classList.toggle('iam-admin-card', nowAdmin); + var roleBadgeEl = cardEl.querySelector('[data-role-badge]'); + if (roleBadgeEl) { + if (nowAdmin) { + roleBadgeEl.className = 'iam-role-badge iam-role-admin'; + roleBadgeEl.textContent = 'Admin'; + } else { + roleBadgeEl.className = 'iam-role-badge iam-role-user'; + roleBadgeEl.textContent = 'User'; + } + } + } + } + + var userIndex = users.findIndex(function(u) { return u.user_id === userId; }); + if (userIndex >= 0 && data.policies) { + users[userIndex].policies = data.policies; + } + } + }); + }); + } + + var editUserForm = document.getElementById('editUserForm'); + if (editUserForm) { + editUserForm.addEventListener('submit', function(e) { + e.preventDefault(); + var key = currentEditKey; + window.UICore.submitFormAjax(editUserForm, { + successMessage: 'User updated', + onSuccess: function(data) { + editUserModal.hide(); + + var newName = data.display_name || document.getElementById('editUserDisplayName').value; + var editBtn = document.querySelector('[data-edit-user][data-user-id="' + key + '"]'); + if (editBtn) { + editBtn.setAttribute('data-display-name', newName); + var card = editBtn.closest('.iam-user-card'); + if (card) { + var nameEl = card.querySelector('h6'); + if (nameEl) { + nameEl.textContent = newName; + nameEl.title = newName; + } + var itemWrapper = card.closest('.iam-user-item'); + if (itemWrapper) { + itemWrapper.setAttribute('data-display-name', newName.toLowerCase()); + } + } + } + + var userIndex = users.findIndex(function(u) { return u.user_id === key; }); + if (userIndex >= 0) { + users[userIndex].display_name = newName; + } + + if (currentEditAccessKey === currentUserKey) { + document.querySelectorAll('.sidebar-user .user-name').forEach(function(el) { + var truncated = newName.length > 16 ? newName.substring(0, 16) + '...' : newName; + el.textContent = truncated; + el.title = newName; + }); + document.querySelectorAll('.sidebar-user[data-username]').forEach(function(el) { + el.setAttribute('data-username', newName); + }); + } + } + }); + }); + } + + var deleteUserForm = document.getElementById('deleteUserForm'); + if (deleteUserForm) { + deleteUserForm.addEventListener('submit', function(e) { + e.preventDefault(); + var key = currentDeleteKey; + window.UICore.submitFormAjax(deleteUserForm, { + successMessage: 'User deleted', + onSuccess: function(data) { + deleteUserModal.hide(); + + if (currentDeleteAccessKey === currentUserKey) { + window.location.href = '/ui/'; + return; + } + + var deleteBtn = document.querySelector('[data-delete-user][data-user-id="' + key + '"]'); + if (deleteBtn) { + var cardCol = deleteBtn.closest('[class*="col-"]'); + if (cardCol) { + cardCol.remove(); + } + } + + users = users.filter(function(u) { return u.user_id !== key; }); + updateUserCount(); + } + }); + }); + } + } + + function setupSearch() { + var searchInput = document.getElementById('iam-user-search'); + if (!searchInput) return; + + searchInput.addEventListener('input', function() { + var query = searchInput.value.toLowerCase().trim(); + var items = document.querySelectorAll('.iam-user-item'); + var noResults = document.getElementById('iam-no-results'); + var visibleCount = 0; + + items.forEach(function(item) { + var name = item.getAttribute('data-display-name') || ''; + var key = item.getAttribute('data-access-key-filter') || ''; + var matches = !query || name.indexOf(query) >= 0 || key.indexOf(query) >= 0; + item.classList.toggle('d-none', !matches); + if (matches) visibleCount++; + }); + + if (noResults) { + noResults.classList.toggle('d-none', visibleCount > 0); + } + }); + } + + function copyAccessKey(btn) { + var key = btn.getAttribute('data-copy-access-key'); + if (!key) return; + var originalHtml = btn.innerHTML; + navigator.clipboard.writeText(key).then(function() { + btn.innerHTML = ''; + btn.style.color = '#22c55e'; + setTimeout(function() { + btn.innerHTML = originalHtml; + btn.style.color = ''; + }, 1200); + }).catch(function() {}); + } + + function setupCopyAccessKeyButtons() { + document.querySelectorAll('[data-copy-access-key]').forEach(function(btn) { + btn.addEventListener('click', function() { + copyAccessKey(btn); + }); + }); + } + + return { + init: init + }; +})(); diff --git a/rust/myfsio-engine/crates/myfsio-server/static/js/ui-core.js b/rust/myfsio-engine/crates/myfsio-server/static/js/ui-core.js new file mode 100644 index 0000000..dde8f10 --- /dev/null +++ b/rust/myfsio-engine/crates/myfsio-server/static/js/ui-core.js @@ -0,0 +1,334 @@ +window.UICore = (function() { + 'use strict'; + + function getCsrfToken() { + const meta = document.querySelector('meta[name="csrf-token"]'); + return meta ? meta.getAttribute('content') : ''; + } + + function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return bytes + ' bytes'; + const units = ['bytes', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]; + } + + function escapeHtml(value) { + if (value === null || value === undefined) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + async function submitFormAjax(form, options) { + options = options || {}; + var onSuccess = options.onSuccess || function() {}; + var onError = options.onError || function() {}; + var successMessage = options.successMessage || 'Operation completed'; + + var formData = new FormData(form); + var hasFileInput = !!form.querySelector('input[type="file"]'); + var requestBody = hasFileInput ? formData : new URLSearchParams(formData); + var csrfToken = getCsrfToken(); + var submitBtn = form.querySelector('[type="submit"]'); + var originalHtml = submitBtn ? submitBtn.innerHTML : ''; + + try { + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = 'Saving...'; + } + + var formAction = form.getAttribute('action') || form.action; + var headers = { + 'X-CSRF-Token': csrfToken, + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }; + if (!hasFileInput) { + headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; + } + var response = await fetch(formAction, { + method: form.getAttribute('method') || 'POST', + headers: headers, + body: requestBody, + redirect: 'follow' + }); + + var contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + throw new Error('Server returned an unexpected response. Please try again.'); + } + + var data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'HTTP ' + response.status); + } + + window.showToast(data.message || successMessage, 'Success', 'success'); + onSuccess(data); + + } catch (err) { + window.showToast(err.message, 'Error', 'error'); + onError(err); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalHtml; + } + } + } + + function PollingManager() { + this.intervals = {}; + this.callbacks = {}; + this.timers = {}; + this.defaults = { + replication: 30000, + lifecycle: 60000, + connectionHealth: 60000, + bucketStats: 120000 + }; + this._loadSettings(); + } + + PollingManager.prototype._loadSettings = function() { + try { + var stored = localStorage.getItem('myfsio-polling-intervals'); + if (stored) { + var settings = JSON.parse(stored); + for (var key in settings) { + if (settings.hasOwnProperty(key)) { + this.defaults[key] = settings[key]; + } + } + } + } catch (e) { + console.warn('Failed to load polling settings:', e); + } + }; + + PollingManager.prototype.saveSettings = function(settings) { + try { + for (var key in settings) { + if (settings.hasOwnProperty(key)) { + this.defaults[key] = settings[key]; + } + } + localStorage.setItem('myfsio-polling-intervals', JSON.stringify(this.defaults)); + } catch (e) { + console.warn('Failed to save polling settings:', e); + } + }; + + PollingManager.prototype.start = function(key, callback, interval) { + this.stop(key); + var ms = interval !== undefined ? interval : (this.defaults[key] || 30000); + if (ms <= 0) return; + + this.callbacks[key] = callback; + this.intervals[key] = ms; + + callback(); + + var self = this; + this.timers[key] = setInterval(function() { + if (!document.hidden) { + callback(); + } + }, ms); + }; + + PollingManager.prototype.stop = function(key) { + if (this.timers[key]) { + clearInterval(this.timers[key]); + delete this.timers[key]; + } + }; + + PollingManager.prototype.stopAll = function() { + for (var key in this.timers) { + if (this.timers.hasOwnProperty(key)) { + clearInterval(this.timers[key]); + } + } + this.timers = {}; + }; + + PollingManager.prototype.updateInterval = function(key, newInterval) { + var callback = this.callbacks[key]; + this.defaults[key] = newInterval; + this.saveSettings(this.defaults); + if (callback) { + this.start(key, callback, newInterval); + } + }; + + PollingManager.prototype.getSettings = function() { + var result = {}; + for (var key in this.defaults) { + if (this.defaults.hasOwnProperty(key)) { + result[key] = this.defaults[key]; + } + } + return result; + }; + + var pollingManager = new PollingManager(); + + document.addEventListener('visibilitychange', function() { + if (document.hidden) { + pollingManager.stopAll(); + } else { + for (var key in pollingManager.callbacks) { + if (pollingManager.callbacks.hasOwnProperty(key)) { + pollingManager.start(key, pollingManager.callbacks[key], pollingManager.intervals[key]); + } + } + } + }); + + window.addEventListener('beforeunload', function() { + pollingManager.stopAll(); + }); + + return { + getCsrfToken: getCsrfToken, + formatBytes: formatBytes, + escapeHtml: escapeHtml, + submitFormAjax: submitFormAjax, + PollingManager: PollingManager, + pollingManager: pollingManager + }; +})(); + +window.pollingManager = window.UICore.pollingManager; + +window.UICore.copyToClipboard = async function(text, button, originalText) { + try { + await navigator.clipboard.writeText(text); + if (button) { + var prevText = button.textContent; + button.textContent = 'Copied!'; + setTimeout(function() { + button.textContent = originalText || prevText; + }, 1500); + } + return true; + } catch (err) { + console.error('Copy failed:', err); + return false; + } +}; + +window.UICore.setButtonLoading = function(button, isLoading, loadingText) { + if (!button) return; + if (isLoading) { + button._originalHtml = button.innerHTML; + button._originalDisabled = button.disabled; + button.disabled = true; + button.innerHTML = '' + (loadingText || 'Loading...'); + } else { + button.disabled = button._originalDisabled || false; + button.innerHTML = button._originalHtml || button.innerHTML; + } +}; + +window.UICore.updateBadgeCount = function(selector, count, singular, plural) { + var badge = document.querySelector(selector); + if (badge) { + var label = count === 1 ? (singular || '') : (plural || 's'); + badge.textContent = count + ' ' + label; + } +}; + +window.UICore.setupJsonAutoIndent = function(textarea) { + if (!textarea) return; + + textarea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + + var start = this.selectionStart; + var end = this.selectionEnd; + var value = this.value; + + var lineStart = value.lastIndexOf('\n', start - 1) + 1; + var currentLine = value.substring(lineStart, start); + + var indentMatch = currentLine.match(/^(\s*)/); + var indent = indentMatch ? indentMatch[1] : ''; + + var trimmedLine = currentLine.trim(); + var lastChar = trimmedLine.slice(-1); + + var newIndent = indent; + var insertAfter = ''; + + if (lastChar === '{' || lastChar === '[') { + newIndent = indent + ' '; + + var charAfterCursor = value.substring(start, start + 1).trim(); + if ((lastChar === '{' && charAfterCursor === '}') || + (lastChar === '[' && charAfterCursor === ']')) { + insertAfter = '\n' + indent; + } + } else if (lastChar === ',' || lastChar === ':') { + newIndent = indent; + } + + var insertion = '\n' + newIndent + insertAfter; + var newValue = value.substring(0, start) + insertion + value.substring(end); + + this.value = newValue; + + var newCursorPos = start + 1 + newIndent.length; + this.selectionStart = this.selectionEnd = newCursorPos; + + this.dispatchEvent(new Event('input', { bubbles: true })); + } + + if (e.key === 'Tab') { + e.preventDefault(); + var start = this.selectionStart; + var end = this.selectionEnd; + + if (e.shiftKey) { + var lineStart = this.value.lastIndexOf('\n', start - 1) + 1; + var lineContent = this.value.substring(lineStart, start); + if (lineContent.startsWith(' ')) { + this.value = this.value.substring(0, lineStart) + + this.value.substring(lineStart + 2); + this.selectionStart = this.selectionEnd = Math.max(lineStart, start - 2); + } + } else { + this.value = this.value.substring(0, start) + ' ' + this.value.substring(end); + this.selectionStart = this.selectionEnd = start + 2; + } + + this.dispatchEvent(new Event('input', { bubbles: true })); + } + }); +}; + +document.addEventListener('DOMContentLoaded', function() { + var flashMessage = sessionStorage.getItem('flashMessage'); + if (flashMessage) { + sessionStorage.removeItem('flashMessage'); + try { + var msg = JSON.parse(flashMessage); + if (window.showToast) { + window.showToast(msg.body || msg.title, msg.title, msg.variant || 'info'); + } + } catch (e) {} + } +}); diff --git a/rust/myfsio-engine/crates/myfsio-server/templates/base.html b/rust/myfsio-engine/crates/myfsio-server/templates/base.html index 2701a17..e5b9564 100644 --- a/rust/myfsio-engine/crates/myfsio-server/templates/base.html +++ b/rust/myfsio-engine/crates/myfsio-server/templates/base.html @@ -3,7 +3,7 @@ - {% if principal %}{% endif %} + {% if principal %}{% endif %} MyFSIO Console @@ -145,7 +145,7 @@
    - +
    - +
    @@ -655,7 +649,7 @@ {% else %}
    - +
    @@ -817,7 +811,7 @@ {% for key in kms_keys %} {% endfor %} @@ -832,14 +826,14 @@ Save Encryption Settings - {% if enc_algorithm %} - - {% endif %}
    {% else %} @@ -864,7 +858,6 @@
    {% set max_bytes = bucket_quota.max_bytes %} {% set max_objects = bucket_quota.max_objects %} - {% set has_quota = max_bytes != null or max_objects != null %} {% set current_objects = bucket_stats.objects | default(value=0) %} {% set version_count = bucket_stats.version_count | default(value=0) %} {% set total_objects = bucket_stats.total_objects | default(value=current_objects) %} @@ -879,10 +872,10 @@
    {{ total_objects }}
    Total Objects
    - {% if max_objects != null %} + {% if has_max_objects %}
    {% if max_objects > 0 %}{% set obj_pct = total_objects / max_objects * 100 | int %}{% else %}{% set obj_pct = 0 %}{% endif %} -
    +
    {{ obj_pct }}% of {{ max_objects }} limit
    {% else %} @@ -899,10 +892,10 @@
    {{ total_bytes | filesizeformat }}
    Total Storage
    - {% if max_bytes != null %} + {% if has_max_bytes %}
    {% if max_bytes > 0 %}{% set bytes_pct = total_bytes / max_bytes * 100 | int %}{% else %}{% set bytes_pct = 0 %}{% endif %} -
    +
    {{ bytes_pct }}% of {{ max_bytes | filesizeformat }} limit
    {% else %} @@ -924,14 +917,14 @@
    - Storage quota enabled + Storage quota active

    - {% if max_bytes != null and max_objects != null %} - Limited to {{ max_bytes | filesizeformat }} and {{ max_objects }} objects. - {% elif max_bytes != null %} - Limited to {{ max_bytes | filesizeformat }} storage. - {% else %} - Limited to {{ max_objects }} objects. + {% if has_max_bytes and has_max_objects %} + This bucket is limited to {{ max_bytes | filesizeformat }} storage and {{ max_objects }} objects. + {% elif has_max_bytes %} + This bucket is limited to {{ max_bytes | filesizeformat }} storage. + {% elif has_max_objects %} + This bucket is limited to {{ max_objects }} objects. {% endif %}

    @@ -951,14 +944,14 @@ {% if can_manage_quota %}
    - +
    MB
    @@ -968,7 +961,7 @@
    Maximum number of objects allowed. Leave empty for unlimited.
    @@ -1058,7 +1051,7 @@ {% if can_manage_website %} - +
    @@ -1423,7 +1416,7 @@ Refresh - + - + @@ -2056,7 +2049,7 @@ data-multipart-complete-template="{{ url_for(endpoint="ui.complete_multipart_upload", bucket_name=bucket_name, upload_id="UPLOAD_ID_PLACEHOLDER") }}" data-multipart-abort-template="{{ url_for(endpoint="ui.abort_multipart_upload", bucket_name=bucket_name, upload_id="UPLOAD_ID_PLACEHOLDER") }}" > - +

    VyHgKiI{~5 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 z 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�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