Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3e5cabe3d | |||
| bcc18e3511 | |||
| 3b6cbe7ca8 | |||
| 41e1893d2d | |||
| 1ce31c52ec | |||
| 3bc9c0fc79 | |||
| bf838f938a | |||
| 32bf9bae0e |
@@ -48,7 +48,7 @@ RUN deno cache mod.ts
|
|||||||
# Create storage directory
|
# Create storage directory
|
||||||
RUN mkdir -p /data
|
RUN mkdir -p /data
|
||||||
|
|
||||||
EXPOSE 9000 3000
|
EXPOSE 9000 3000 4433
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
CMD ["deno", "run", "--allow-all", "mod.ts", "server"]
|
CMD ["deno", "run", "--allow-all", "mod.ts", "server"]
|
||||||
|
|||||||
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-24 - 1.8.1 - fix(build)
|
||||||
|
migrate build tool config to .smartconfig.json and bump tooling dependencies
|
||||||
|
|
||||||
|
- Move tsbundle, tsdocker, and tswatch configuration from npmextra.json to .smartconfig.json
|
||||||
|
- Update @git.zone/tsbundle, @git.zone/tsdocker, and @git.zone/tswatch devDependencies
|
||||||
|
- Bump @aws-sdk/client-s3 import to ^3.1016.0
|
||||||
|
|
||||||
|
## 2026-03-24 - 1.8.0 - feat(docs,web)
|
||||||
|
document cluster and erasure coding support and align UI storage provider naming
|
||||||
|
|
||||||
|
- expand the README with cluster mode, erasure coding, multi-drive configuration, and multi-node Docker Compose examples
|
||||||
|
- update web storage integration to use the renamed storage data provider interface and browser component
|
||||||
|
- bump runtime, UI, and tooling dependencies to newer compatible versions
|
||||||
|
|
||||||
|
## 2026-03-22 - 1.7.1 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-03-22 - 1.7.0 - feat(cluster)
|
||||||
|
add cluster configuration support across server, CLI, and admin UI
|
||||||
|
|
||||||
|
- add environment variable and CLI support for cluster, QUIC, seed node, drive, erasure coding, and heartbeat settings
|
||||||
|
- pass cluster-aware configuration into smartstorage and expose the QUIC port in Docker
|
||||||
|
- extend config APIs and admin UI to display cluster, erasure coding, and storage drive configuration
|
||||||
|
- upgrade @push.rocks/smartstorage to ^6.3.1 to support the new cluster capabilities
|
||||||
|
|
||||||
## 2026-03-21 - 1.6.0 - feat(scripts)
|
## 2026-03-21 - 1.6.0 - feat(scripts)
|
||||||
add release script for committing and pushing docker images
|
add release script for committing and pushing docker images
|
||||||
|
|
||||||
|
|||||||
12
deno.json
12
deno.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lossless.zone/objectstorage",
|
"name": "@lossless.zone/objectstorage",
|
||||||
"version": "1.6.0",
|
"version": "1.8.1",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -8,12 +8,12 @@
|
|||||||
"dev": "pnpm run watch"
|
"dev": "pnpm run watch"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@push.rocks/smartstorage": "npm:@push.rocks/smartstorage@^6.0.1",
|
"@push.rocks/smartstorage": "npm:@push.rocks/smartstorage@^6.3.2",
|
||||||
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0",
|
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.5.1",
|
||||||
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.937.0",
|
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.1016.0",
|
||||||
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
||||||
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.3.0",
|
||||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
|
"@api.global/typedserver": "npm:@api.global/typedserver@^8.4.6",
|
||||||
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
||||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1"
|
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lossless.zone/objectstorage",
|
"name": "@lossless.zone/objectstorage",
|
||||||
"version": "1.6.0",
|
"version": "1.8.1",
|
||||||
"description": "object storage server with management UI powered by smartstorage",
|
"description": "object storage server with management UI powered by smartstorage",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@design.estate/dees-catalog": "^3.48.0",
|
"@design.estate/dees-catalog": "^3.49.0",
|
||||||
"@design.estate/dees-element": "^2.2.2"
|
"@design.estate/dees-element": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbundle": "^2.9.0",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsdocker": "^2.0.0",
|
"@git.zone/tsdocker": "^2.2.4",
|
||||||
"@git.zone/tswatch": "^3.2.0"
|
"@git.zone/tswatch": "^3.3.2"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
825
pnpm-lock.yaml
generated
825
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
17
readme.hints.md
Normal file
17
readme.hints.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Project Hints
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
- Deno-based backend with `deno.json` for imports and tasks
|
||||||
|
- Frontend bundled with `@git.zone/tsbundle` (esbuild, base64ts output mode)
|
||||||
|
- Config in `.smartconfig.json` (renamed from npmextra.json as of 2026-03-24)
|
||||||
|
- Tests run with `deno task test` (not tstest)
|
||||||
|
- Docker image built with `@git.zone/tsdocker`
|
||||||
|
|
||||||
|
## Build Tools Config
|
||||||
|
- `.smartconfig.json` contains config for `@git.zone/tsbundle`, `@git.zone/tswatch`, and `@git.zone/tsdocker`
|
||||||
|
- tsbundle uses base64ts output mode for Deno compile embedding
|
||||||
|
- tswatch runs backend watcher with `deno run --allow-all mod.ts server --ephemeral`
|
||||||
|
|
||||||
|
## Dependencies (as of 2026-03-24)
|
||||||
|
- devDependencies: tsbundle@2.10.0, tsdocker@2.2.4, tswatch@3.3.2
|
||||||
|
- No tsconfig.json — uses compilerOptions in deno.json
|
||||||
213
readme.md
213
readme.md
@@ -1,10 +1,10 @@
|
|||||||
# @lossless.zone/objectstorage
|
# @lossless.zone/objectstorage
|
||||||
|
|
||||||
> 🚀 S3-compatible object storage server with a slick management UI — powered by [`smartstorage`](https://code.foss.global/push.rocks/smartstorage).
|
> 🚀 S3-compatible object storage server with clustering, erasure coding, and a slick management UI — powered by [`smartstorage`](https://code.foss.global/push.rocks/smartstorage).
|
||||||
|
|
||||||
**objectstorage** gives you a fully featured, self-hosted S3-compatible storage server with a beautiful web-based management interface — all in a single Docker image. No Java, no bloat, no fuss.
|
**objectstorage** gives you a fully featured, self-hosted S3-compatible storage server with a beautiful web-based management interface — all in a single Docker image. No Java, no bloat, no fuss.
|
||||||
|
|
||||||
Built on Deno for the backend and [`@design.estate/dees-catalog`](https://code.foss.global/design.estate/dees-catalog) for a polished UI, it speaks the S3 protocol out of the box while adding powerful management features on top.
|
Built on Deno for the backend and [`@design.estate/dees-catalog`](https://code.foss.global/design.estate/dees-catalog) for a polished UI, it speaks the S3 protocol out of the box while adding powerful management features on top. Scale from a single node to a distributed cluster with erasure coding and multi-drive support.
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -13,14 +13,18 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Full S3 API compatibility** — Works with any S3 client, SDK, or tool (AWS CLI, boto3, etc.)
|
- **Full S3 API compatibility** — Works with any S3 client, SDK, or tool (AWS CLI, boto3, etc.)
|
||||||
- **Management UI** — Web dashboard for buckets, objects, policies, credentials, and server config
|
- **🔗 Cluster mode** — Distribute storage across multiple nodes with QUIC transport, automatic discovery, and quorum writes/reads
|
||||||
|
- **🛡️ Erasure coding** — Reed-Solomon erasure coding (default 4+2) for data durability with minimal overhead
|
||||||
|
- **💾 Multi-drive support** — Stripe data across multiple disks per node with per-drive health monitoring
|
||||||
|
- **🔄 Self-healing** — Background scanner detects and reconstructs missing or corrupt shards automatically
|
||||||
|
- **Management UI** — Web dashboard for buckets, objects, policies, credentials, cluster config, and storage drives
|
||||||
- **Finder-style object browser** — Column view with file preview, drag-and-drop upload, move/rename, context menus
|
- **Finder-style object browser** — Column view with file preview, drag-and-drop upload, move/rename, context menus
|
||||||
- **Inline code editing** — Built-in Monaco editor with syntax highlighting and save-back-to-storage
|
- **Inline code editing** — Built-in Monaco editor with syntax highlighting and save-back-to-storage
|
||||||
- **PDF viewer** — Render PDFs inline with page navigation, zoom, and thumbnails
|
- **PDF viewer** — Render PDFs inline with page navigation, zoom, and thumbnails
|
||||||
- **Named policy management** — Create reusable IAM-style policies, attach them to multiple buckets
|
- **Named policy management** — Create reusable IAM-style policies, attach them to multiple buckets
|
||||||
- **Credential management** — Add/remove access keys through the UI with live-reload
|
- **Credential management** — Add/remove access keys through the UI with live-reload
|
||||||
- **Single Docker image** — Multi-arch (`amd64` + `arm64`), tiny Alpine-based image
|
- **Single Docker image** — Multi-arch (`amd64` + `arm64`), tiny Alpine-based image
|
||||||
- **Fast** — Rust-powered storage engine via `smartstorage`, Deno runtime for management layer
|
- **Fast** — Rust-powered storage engine via `smartstorage`, streaming I/O with zero-copy and backpressure
|
||||||
- **Secure by default** — JWT-based admin auth, S3 SigV4 authentication, bucket policies
|
- **Secure by default** — JWT-based admin auth, S3 SigV4 authentication, bucket policies
|
||||||
- **🌙 Dark theme** — Automatic dark mode following your system preference
|
- **🌙 Dark theme** — Automatic dark mode following your system preference
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ deno run --allow-all mod.ts server --ephemeral
|
|||||||
|
|
||||||
objectstorage is configured through environment variables, CLI flags, or programmatic config. **Environment variables take precedence** over CLI flags.
|
objectstorage is configured through environment variables, CLI flags, or programmatic config. **Environment variables take precedence** over CLI flags.
|
||||||
|
|
||||||
### Environment Variables
|
### Server Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -71,18 +75,86 @@ objectstorage is configured through environment variables, CLI flags, or program
|
|||||||
| `OBJST_ADMIN_PASSWORD` | Admin UI password | `admin` |
|
| `OBJST_ADMIN_PASSWORD` | Admin UI password | `admin` |
|
||||||
| `OBJST_REGION` | Storage region identifier | `us-east-1` |
|
| `OBJST_REGION` | Storage region identifier | `us-east-1` |
|
||||||
|
|
||||||
|
### Cluster Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `OBJST_CLUSTER_ENABLED` | Enable cluster mode (`true`/`false`) | `false` |
|
||||||
|
| `OBJST_CLUSTER_NODE_ID` | Unique node identifier | auto-generated |
|
||||||
|
| `OBJST_CLUSTER_QUIC_PORT` | QUIC transport port | `4433` |
|
||||||
|
| `OBJST_CLUSTER_SEED_NODES` | Comma-separated seed node addresses | _(empty)_ |
|
||||||
|
| `OBJST_DRIVE_PATHS` | Comma-separated drive mount paths | storage dir |
|
||||||
|
| `OBJST_ERASURE_DATA_SHARDS` | Erasure coding data shards | `4` |
|
||||||
|
| `OBJST_ERASURE_PARITY_SHARDS` | Erasure coding parity shards | `2` |
|
||||||
|
| `OBJST_ERASURE_CHUNK_SIZE` | Erasure chunk size in bytes | `4194304` (4 MB) |
|
||||||
|
| `OBJST_HEARTBEAT_INTERVAL_MS` | Cluster heartbeat interval | `5000` |
|
||||||
|
| `OBJST_HEARTBEAT_TIMEOUT_MS` | Cluster heartbeat timeout | `30000` |
|
||||||
|
|
||||||
### CLI Flags
|
### CLI Flags
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
deno run --allow-all mod.ts server [options]
|
deno run --allow-all mod.ts server [options]
|
||||||
|
|
||||||
Options:
|
Server Options:
|
||||||
--storage-port <port> Storage API port (default: 9000)
|
--storage-port <port> Storage API port (default: 9000)
|
||||||
--ui-port <port> Management UI port (default: 3000)
|
--ui-port <port> Management UI port (default: 3000)
|
||||||
--storage-dir <path> Storage directory (default: /data)
|
--storage-dir <path> Storage directory (default: /data)
|
||||||
--ephemeral Use ./.nogit/objstdata for storage (dev mode)
|
--ephemeral Use ./.nogit/objstdata for storage (dev mode)
|
||||||
|
|
||||||
|
Clustering Options:
|
||||||
|
--cluster-enabled Enable cluster mode
|
||||||
|
--cluster-node-id <id> Unique node identifier
|
||||||
|
--cluster-quic-port <port> QUIC transport port (default: 4433)
|
||||||
|
--cluster-seed-nodes <list> Comma-separated seed node addresses
|
||||||
|
--drive-paths <list> Comma-separated drive mount paths
|
||||||
|
--erasure-data-shards <n> Erasure coding data shards (default: 4)
|
||||||
|
--erasure-parity-shards <n> Erasure coding parity shards (default: 2)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🔗 Cluster Mode
|
||||||
|
|
||||||
|
objectstorage supports distributed storage across multiple nodes with automatic failover and data redundancy.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. **Enable clustering** on each node with `OBJST_CLUSTER_ENABLED=true`
|
||||||
|
2. **Point nodes at each other** using `OBJST_CLUSTER_SEED_NODES` — nodes discover the full cluster from any seed
|
||||||
|
3. **Configure drives** per node with `OBJST_DRIVE_PATHS` — each drive is independently managed
|
||||||
|
4. **Erasure coding** splits objects into data + parity shards across drives and nodes
|
||||||
|
|
||||||
|
### Example: 3-node cluster
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Node 1
|
||||||
|
docker run -d --name objst-node1 \
|
||||||
|
-p 9000:9000 -p 3000:3000 -p 4433:4433/udp \
|
||||||
|
-v /mnt/disk1:/drive1 -v /mnt/disk2:/drive2 \
|
||||||
|
-e OBJST_CLUSTER_ENABLED=true \
|
||||||
|
-e OBJST_CLUSTER_NODE_ID=node-1 \
|
||||||
|
-e OBJST_CLUSTER_SEED_NODES=node2:4433,node3:4433 \
|
||||||
|
-e OBJST_DRIVE_PATHS=/drive1,/drive2 \
|
||||||
|
-e OBJST_ACCESS_KEY=myadminkey \
|
||||||
|
-e OBJST_SECRET_KEY=mysupersecret \
|
||||||
|
code.foss.global/lossless.zone/objectstorage:latest
|
||||||
|
|
||||||
|
# Node 2 and Node 3 — same pattern, different node IDs and seed nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erasure coding presets
|
||||||
|
|
||||||
|
| Config | Data Shards | Parity Shards | Overhead | Fault Tolerance |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Default | 4 | 2 | 50% | 2 failures |
|
||||||
|
| High durability | 6 | 3 | 50% | 3 failures |
|
||||||
|
| Minimal | 2 | 1 | 50% | 1 failure |
|
||||||
|
|
||||||
|
### Inter-node transport
|
||||||
|
|
||||||
|
Cluster communication uses **QUIC** (UDP port 4433 by default) with:
|
||||||
|
- Auto-generated TLS certificates
|
||||||
|
- Multiplexed streams with flow-control backpressure
|
||||||
|
- Heartbeat-based failure detection (default: 5s interval, 30s timeout)
|
||||||
|
|
||||||
## 🖥️ Management UI
|
## 🖥️ Management UI
|
||||||
|
|
||||||
The web-based management UI is served on the UI port (default: `3000`). Log in with username `admin` and the configured admin password.
|
The web-based management UI is served on the UI port (default: `3000`). Log in with username `admin` and the configured admin password.
|
||||||
@@ -147,7 +219,7 @@ Click "Add Key" to create new access credentials. They're immediately available
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
View your server's current configuration at a glance — ports, region, storage directory, and auth/CORS status.
|
View your server's current configuration at a glance — ports, region, storage directory, auth/CORS status, cluster configuration, erasure coding settings, and storage drive paths. The config view also includes an environment variable reference guide for cluster setup.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -279,7 +351,7 @@ pnpm run build:docker
|
|||||||
pnpm run start:docker
|
pnpm run start:docker
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose (standalone)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -299,45 +371,117 @@ volumes:
|
|||||||
objstdata:
|
objstdata:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker Compose (3-node cluster)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
node1:
|
||||||
|
image: code.foss.global/lossless.zone/objectstorage:latest
|
||||||
|
ports:
|
||||||
|
- "9001:9000"
|
||||||
|
- "3001:3000"
|
||||||
|
- "4433:4433/udp"
|
||||||
|
volumes:
|
||||||
|
- node1-drive1:/drive1
|
||||||
|
- node1-drive2:/drive2
|
||||||
|
environment:
|
||||||
|
OBJST_CLUSTER_ENABLED: "true"
|
||||||
|
OBJST_CLUSTER_NODE_ID: node-1
|
||||||
|
OBJST_CLUSTER_QUIC_PORT: "4433"
|
||||||
|
OBJST_CLUSTER_SEED_NODES: node2:4433,node3:4433
|
||||||
|
OBJST_DRIVE_PATHS: /drive1,/drive2
|
||||||
|
OBJST_ACCESS_KEY: myadminkey
|
||||||
|
OBJST_SECRET_KEY: mysupersecret
|
||||||
|
|
||||||
|
node2:
|
||||||
|
image: code.foss.global/lossless.zone/objectstorage:latest
|
||||||
|
ports:
|
||||||
|
- "9002:9000"
|
||||||
|
- "3002:3000"
|
||||||
|
- "4434:4433/udp"
|
||||||
|
volumes:
|
||||||
|
- node2-drive1:/drive1
|
||||||
|
- node2-drive2:/drive2
|
||||||
|
environment:
|
||||||
|
OBJST_CLUSTER_ENABLED: "true"
|
||||||
|
OBJST_CLUSTER_NODE_ID: node-2
|
||||||
|
OBJST_CLUSTER_QUIC_PORT: "4433"
|
||||||
|
OBJST_CLUSTER_SEED_NODES: node1:4433,node3:4433
|
||||||
|
OBJST_DRIVE_PATHS: /drive1,/drive2
|
||||||
|
OBJST_ACCESS_KEY: myadminkey
|
||||||
|
OBJST_SECRET_KEY: mysupersecret
|
||||||
|
|
||||||
|
node3:
|
||||||
|
image: code.foss.global/lossless.zone/objectstorage:latest
|
||||||
|
ports:
|
||||||
|
- "9003:9000"
|
||||||
|
- "3003:3000"
|
||||||
|
- "4435:4433/udp"
|
||||||
|
volumes:
|
||||||
|
- node3-drive1:/drive1
|
||||||
|
- node3-drive2:/drive2
|
||||||
|
environment:
|
||||||
|
OBJST_CLUSTER_ENABLED: "true"
|
||||||
|
OBJST_CLUSTER_NODE_ID: node-3
|
||||||
|
OBJST_CLUSTER_QUIC_PORT: "4433"
|
||||||
|
OBJST_CLUSTER_SEED_NODES: node1:4433,node2:4433
|
||||||
|
OBJST_DRIVE_PATHS: /drive1,/drive2
|
||||||
|
OBJST_ACCESS_KEY: myadminkey
|
||||||
|
OBJST_SECRET_KEY: mysupersecret
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node1-drive1:
|
||||||
|
node1-drive2:
|
||||||
|
node2-drive1:
|
||||||
|
node2-drive2:
|
||||||
|
node3-drive1:
|
||||||
|
node3-drive2:
|
||||||
|
```
|
||||||
|
|
||||||
### Image Details
|
### Image Details
|
||||||
|
|
||||||
- **Base**: `alpine:edge` with Deno runtime
|
- **Base**: `alpine:edge` with Deno runtime
|
||||||
- **Architectures**: `linux/amd64`, `linux/arm64`
|
- **Architectures**: `linux/amd64`, `linux/arm64`
|
||||||
- **Size**: ~150 MB compressed
|
- **Size**: ~150 MB compressed
|
||||||
- **Init system**: `tini` for proper signal handling
|
- **Init system**: `tini` for proper signal handling
|
||||||
- **Exposed ports**: `9000` (S3), `3000` (UI)
|
- **Exposed ports**: `9000` (S3), `3000` (UI), `4433` (QUIC cluster transport)
|
||||||
- **Volume**: `/data` — all bucket data and config persisted here
|
- **Volume**: `/data` — all bucket data and config persisted here
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
│ objectstorage │
|
│ objectstorage │
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────┐ ┌───────────────────────┐ │
|
│ ┌──────────────┐ ┌────────────────────────────────────┐ │
|
||||||
│ │ Management │ │ Storage Engine │ │
|
│ │ Management │ │ Storage Engine (smartstorage) │ │
|
||||||
│ │ UI (port │ │ (smartstorage/ │ │
|
│ │ UI (port │ │ Rust binary via ruststorage │ │
|
||||||
│ │ 3000) │ │ ruststorage) │ │
|
│ │ 3000) │ │ (port 9000) │ │
|
||||||
│ │ │ │ (port 9000) │ │
|
│ │ │ │ │ │
|
||||||
│ │ dees-catalog │ │ • S3 API compat │ │
|
│ │ dees-catalog │ │ • S3 API (path-style routing) │ │
|
||||||
│ │ SPA bundle │ │ • SigV4 auth │ │
|
│ │ SPA bundle │ │ • SigV4 authentication │ │
|
||||||
│ └──────┬───────┘ │ • Bucket policies │ │
|
│ └──────┬───────┘ │ • Bucket policies │ │
|
||||||
│ │ │ • Rust binary engine │ │
|
│ │ │ • Streaming I/O (zero-copy) │ │
|
||||||
│ ┌──────▼───────┐ └───────────────────────┘ │
|
│ ┌──────▼───────┐ │ • Multipart upload support │ │
|
||||||
│ │ OpsServer │ │
|
│ │ OpsServer │ └─────────────┬──────────────────────┘ │
|
||||||
│ │ (TypedReq │──── AWS SDK S3 Client ────────│
|
│ │ (TypedReq │ │ │
|
||||||
│ │ handlers) │ (manages own storage) │
|
│ │ handlers) │── S3 Client ──────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ • Admin auth │ │
|
│ │ • Admin auth │ ┌────────────────────────────────────┐ │
|
||||||
│ │ • CRUD APIs │ │
|
│ │ • CRUD APIs │ │ Cluster Layer (optional) │ │
|
||||||
│ │ • Policy mgr │ │
|
│ │ • Policy mgr │ │ │ │
|
||||||
│ └──────────────┘ │
|
│ └──────────────┘ │ • QUIC transport (port 4433) │ │
|
||||||
|
│ │ • Reed-Solomon erasure coding │ │
|
||||||
|
│ │ • Quorum writes / reads │ │
|
||||||
|
│ │ • Heartbeat failure detection │ │
|
||||||
|
│ │ • Self-healing shard repair │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────────────────────────────────────┐│
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
│ │ /data (persistent volume) ││
|
│ │ Storage Drives │ │
|
||||||
│ │ buckets/ .policies/ .objectstorage/ ││
|
│ │ /drive1 /drive2 /drive3 ... (or single /data) │ │
|
||||||
│ └──────────────────────────────────────────────┘│
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
└─────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
@@ -345,6 +489,8 @@ volumes:
|
|||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Storage Engine** | [`@push.rocks/smartstorage`](https://code.foss.global/push.rocks/smartstorage) (Rust binary via `ruststorage`) |
|
| **Storage Engine** | [`@push.rocks/smartstorage`](https://code.foss.global/push.rocks/smartstorage) (Rust binary via `ruststorage`) |
|
||||||
|
| **Cluster Transport** | QUIC via `quinn` (auto-TLS, multiplexed streams, backpressure) |
|
||||||
|
| **Erasure Coding** | Reed-Solomon (configurable data + parity shards) |
|
||||||
| **Runtime** | Deno |
|
| **Runtime** | Deno |
|
||||||
| **Management API** | [`@api.global/typedrequest`](https://code.foss.global/api.global/typedrequest) + [`@api.global/typedserver`](https://code.foss.global/api.global/typedserver) |
|
| **Management API** | [`@api.global/typedrequest`](https://code.foss.global/api.global/typedrequest) + [`@api.global/typedserver`](https://code.foss.global/api.global/typedserver) |
|
||||||
| **Auth** | JWT via [`@push.rocks/smartjwt`](https://code.foss.global/push.rocks/smartjwt), S3 SigV4 |
|
| **Auth** | JWT via [`@push.rocks/smartjwt`](https://code.foss.global/push.rocks/smartjwt), S3 SigV4 |
|
||||||
@@ -367,13 +513,16 @@ pnpm run build
|
|||||||
# Type check backend
|
# Type check backend
|
||||||
deno check mod.ts
|
deno check mod.ts
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
# Run in development mode
|
# Run in development mode
|
||||||
deno run --allow-all mod.ts server --ephemeral
|
deno run --allow-all mod.ts server --ephemeral
|
||||||
```
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@lossless.zone/objectstorage',
|
name: '@lossless.zone/objectstorage',
|
||||||
version: '1.6.0',
|
version: '1.8.1',
|
||||||
description: 'object storage server with management UI powered by smartstorage'
|
description: 'object storage server with management UI powered by smartstorage'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,37 @@ export class ObjectStorageContainer {
|
|||||||
const envRegion = Deno.env.get('OBJST_REGION');
|
const envRegion = Deno.env.get('OBJST_REGION');
|
||||||
if (envRegion) this.config.region = envRegion;
|
if (envRegion) this.config.region = envRegion;
|
||||||
|
|
||||||
|
// Cluster environment variables
|
||||||
|
const envClusterEnabled = Deno.env.get('OBJST_CLUSTER_ENABLED');
|
||||||
|
if (envClusterEnabled) this.config.clusterEnabled = envClusterEnabled === 'true' || envClusterEnabled === '1';
|
||||||
|
|
||||||
|
const envClusterNodeId = Deno.env.get('OBJST_CLUSTER_NODE_ID');
|
||||||
|
if (envClusterNodeId) this.config.clusterNodeId = envClusterNodeId;
|
||||||
|
|
||||||
|
const envClusterQuicPort = Deno.env.get('OBJST_CLUSTER_QUIC_PORT');
|
||||||
|
if (envClusterQuicPort) this.config.clusterQuicPort = parseInt(envClusterQuicPort, 10);
|
||||||
|
|
||||||
|
const envClusterSeedNodes = Deno.env.get('OBJST_CLUSTER_SEED_NODES');
|
||||||
|
if (envClusterSeedNodes) this.config.clusterSeedNodes = envClusterSeedNodes.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
const envDrivePaths = Deno.env.get('OBJST_DRIVE_PATHS');
|
||||||
|
if (envDrivePaths) this.config.drivePaths = envDrivePaths.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
const envErasureDataShards = Deno.env.get('OBJST_ERASURE_DATA_SHARDS');
|
||||||
|
if (envErasureDataShards) this.config.erasureDataShards = parseInt(envErasureDataShards, 10);
|
||||||
|
|
||||||
|
const envErasureParityShards = Deno.env.get('OBJST_ERASURE_PARITY_SHARDS');
|
||||||
|
if (envErasureParityShards) this.config.erasureParityShards = parseInt(envErasureParityShards, 10);
|
||||||
|
|
||||||
|
const envErasureChunkSize = Deno.env.get('OBJST_ERASURE_CHUNK_SIZE');
|
||||||
|
if (envErasureChunkSize) this.config.erasureChunkSizeBytes = parseInt(envErasureChunkSize, 10);
|
||||||
|
|
||||||
|
const envHeartbeatInterval = Deno.env.get('OBJST_HEARTBEAT_INTERVAL_MS');
|
||||||
|
if (envHeartbeatInterval) this.config.clusterHeartbeatIntervalMs = parseInt(envHeartbeatInterval, 10);
|
||||||
|
|
||||||
|
const envHeartbeatTimeout = Deno.env.get('OBJST_HEARTBEAT_TIMEOUT_MS');
|
||||||
|
if (envHeartbeatTimeout) this.config.clusterHeartbeatTimeoutMs = parseInt(envHeartbeatTimeout, 10);
|
||||||
|
|
||||||
this.opsServer = new OpsServer(this);
|
this.opsServer = new OpsServer(this);
|
||||||
this.policyManager = new PolicyManager(this);
|
this.policyManager = new PolicyManager(this);
|
||||||
}
|
}
|
||||||
@@ -49,9 +80,17 @@ export class ObjectStorageContainer {
|
|||||||
console.log(` UI port: ${this.config.uiPort}`);
|
console.log(` UI port: ${this.config.uiPort}`);
|
||||||
console.log(` Storage: ${this.config.storageDirectory}`);
|
console.log(` Storage: ${this.config.storageDirectory}`);
|
||||||
console.log(` Region: ${this.config.region}`);
|
console.log(` Region: ${this.config.region}`);
|
||||||
|
console.log(` Cluster: ${this.config.clusterEnabled ? 'enabled' : 'disabled'}`);
|
||||||
|
if (this.config.clusterEnabled) {
|
||||||
|
console.log(` Node ID: ${this.config.clusterNodeId || '(auto-generated)'}`);
|
||||||
|
console.log(` QUIC Port: ${this.config.clusterQuicPort}`);
|
||||||
|
console.log(` Seed Nodes: ${this.config.clusterSeedNodes.join(', ') || '(none)'}`);
|
||||||
|
console.log(` Drives: ${this.config.drivePaths.length > 0 ? this.config.drivePaths.join(', ') : this.config.storageDirectory}`);
|
||||||
|
console.log(` Erasure: ${this.config.erasureDataShards}+${this.config.erasureParityShards}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Start smartstorage
|
// Build smartstorage config
|
||||||
this.smartstorageInstance = await plugins.smartstorage.SmartStorage.createAndStart({
|
const smartstorageConfig: any = {
|
||||||
server: {
|
server: {
|
||||||
port: this.config.objstPort,
|
port: this.config.objstPort,
|
||||||
address: '0.0.0.0',
|
address: '0.0.0.0',
|
||||||
@@ -64,7 +103,31 @@ export class ObjectStorageContainer {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
credentials: this.config.accessCredentials,
|
credentials: this.config.accessCredentials,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (this.config.clusterEnabled) {
|
||||||
|
smartstorageConfig.cluster = {
|
||||||
|
enabled: true,
|
||||||
|
nodeId: this.config.clusterNodeId || crypto.randomUUID().slice(0, 8),
|
||||||
|
quicPort: this.config.clusterQuicPort,
|
||||||
|
seedNodes: this.config.clusterSeedNodes,
|
||||||
|
erasure: {
|
||||||
|
dataShards: this.config.erasureDataShards,
|
||||||
|
parityShards: this.config.erasureParityShards,
|
||||||
|
chunkSizeBytes: this.config.erasureChunkSizeBytes,
|
||||||
|
},
|
||||||
|
drives: {
|
||||||
|
paths: this.config.drivePaths.length > 0
|
||||||
|
? this.config.drivePaths
|
||||||
|
: [this.config.storageDirectory],
|
||||||
|
},
|
||||||
|
heartbeatIntervalMs: this.config.clusterHeartbeatIntervalMs,
|
||||||
|
heartbeatTimeoutMs: this.config.clusterHeartbeatTimeoutMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start smartstorage
|
||||||
|
this.smartstorageInstance = await plugins.smartstorage.SmartStorage.createAndStart(smartstorageConfig);
|
||||||
|
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
console.log(`Storage server started on port ${this.config.objstPort}`);
|
console.log(`Storage server started on port ${this.config.objstPort}`);
|
||||||
|
|||||||
42
ts/cli.ts
42
ts/cli.ts
@@ -29,6 +29,27 @@ export async function runCli(): Promise<void> {
|
|||||||
// Use a temp directory for storage
|
// Use a temp directory for storage
|
||||||
configOverrides.storageDirectory = './.nogit/objstdata';
|
configOverrides.storageDirectory = './.nogit/objstdata';
|
||||||
break;
|
break;
|
||||||
|
case '--cluster-enabled':
|
||||||
|
configOverrides.clusterEnabled = true;
|
||||||
|
break;
|
||||||
|
case '--cluster-node-id':
|
||||||
|
configOverrides.clusterNodeId = args[++i];
|
||||||
|
break;
|
||||||
|
case '--cluster-quic-port':
|
||||||
|
configOverrides.clusterQuicPort = parseInt(args[++i], 10);
|
||||||
|
break;
|
||||||
|
case '--cluster-seed-nodes':
|
||||||
|
configOverrides.clusterSeedNodes = args[++i].split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
break;
|
||||||
|
case '--drive-paths':
|
||||||
|
configOverrides.drivePaths = args[++i].split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
break;
|
||||||
|
case '--erasure-data-shards':
|
||||||
|
configOverrides.erasureDataShards = parseInt(args[++i], 10);
|
||||||
|
break;
|
||||||
|
case '--erasure-parity-shards':
|
||||||
|
configOverrides.erasureParityShards = parseInt(args[++i], 10);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,12 +80,21 @@ ObjectStorage - S3-compatible object storage server with management UI
|
|||||||
Usage:
|
Usage:
|
||||||
objectstorage server [options]
|
objectstorage server [options]
|
||||||
|
|
||||||
Options:
|
Server Options:
|
||||||
--ephemeral Use local .nogit/objstdata for storage
|
--ephemeral Use local .nogit/objstdata for storage
|
||||||
--storage-port PORT Storage API port (default: 9000, env: OBJST_PORT)
|
--storage-port PORT Storage API port (default: 9000, env: OBJST_PORT)
|
||||||
--ui-port PORT Management UI port (default: 3000, env: UI_PORT)
|
--ui-port PORT Management UI port (default: 3000, env: UI_PORT)
|
||||||
--storage-dir DIR Storage directory (default: /data, env: OBJST_STORAGE_DIR)
|
--storage-dir DIR Storage directory (default: /data, env: OBJST_STORAGE_DIR)
|
||||||
|
|
||||||
|
Clustering Options:
|
||||||
|
--cluster-enabled Enable cluster mode (env: OBJST_CLUSTER_ENABLED)
|
||||||
|
--cluster-node-id ID Unique node identifier (env: OBJST_CLUSTER_NODE_ID)
|
||||||
|
--cluster-quic-port PORT QUIC transport port (default: 4433, env: OBJST_CLUSTER_QUIC_PORT)
|
||||||
|
--cluster-seed-nodes LIST Comma-separated seed node addresses (env: OBJST_CLUSTER_SEED_NODES)
|
||||||
|
--drive-paths LIST Comma-separated drive mount paths (env: OBJST_DRIVE_PATHS)
|
||||||
|
--erasure-data-shards N Erasure coding data shards (default: 4, env: OBJST_ERASURE_DATA_SHARDS)
|
||||||
|
--erasure-parity-shards N Erasure coding parity shards (default: 2, env: OBJST_ERASURE_PARITY_SHARDS)
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
OBJST_PORT Storage API port
|
OBJST_PORT Storage API port
|
||||||
UI_PORT Management UI port
|
UI_PORT Management UI port
|
||||||
@@ -73,5 +103,15 @@ Environment Variables:
|
|||||||
OBJST_SECRET_KEY Secret key (default: admin)
|
OBJST_SECRET_KEY Secret key (default: admin)
|
||||||
OBJST_ADMIN_PASSWORD Admin UI password (default: admin)
|
OBJST_ADMIN_PASSWORD Admin UI password (default: admin)
|
||||||
OBJST_REGION Storage region (default: us-east-1)
|
OBJST_REGION Storage region (default: us-east-1)
|
||||||
|
OBJST_CLUSTER_ENABLED Enable clustering (true/false)
|
||||||
|
OBJST_CLUSTER_NODE_ID Unique node identifier
|
||||||
|
OBJST_CLUSTER_QUIC_PORT QUIC transport port (default: 4433)
|
||||||
|
OBJST_CLUSTER_SEED_NODES Comma-separated seed node addresses
|
||||||
|
OBJST_DRIVE_PATHS Comma-separated drive mount paths
|
||||||
|
OBJST_ERASURE_DATA_SHARDS Erasure data shards (default: 4)
|
||||||
|
OBJST_ERASURE_PARITY_SHARDS Erasure parity shards (default: 2)
|
||||||
|
OBJST_ERASURE_CHUNK_SIZE Erasure chunk size in bytes (default: 4194304)
|
||||||
|
OBJST_HEARTBEAT_INTERVAL_MS Cluster heartbeat interval (default: 5000)
|
||||||
|
OBJST_HEARTBEAT_TIMEOUT_MS Cluster heartbeat timeout (default: 30000)
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ export class ConfigHandler {
|
|||||||
storageDirectory: containerConfig.storageDirectory,
|
storageDirectory: containerConfig.storageDirectory,
|
||||||
authEnabled: true,
|
authEnabled: true,
|
||||||
corsEnabled: false,
|
corsEnabled: false,
|
||||||
|
clusterEnabled: containerConfig.clusterEnabled,
|
||||||
|
clusterNodeId: containerConfig.clusterNodeId,
|
||||||
|
clusterQuicPort: containerConfig.clusterQuicPort,
|
||||||
|
clusterSeedNodes: containerConfig.clusterSeedNodes,
|
||||||
|
erasureDataShards: containerConfig.erasureDataShards,
|
||||||
|
erasureParityShards: containerConfig.erasureParityShards,
|
||||||
|
erasureChunkSizeBytes: containerConfig.erasureChunkSizeBytes,
|
||||||
|
drivePaths: containerConfig.drivePaths,
|
||||||
|
clusterHeartbeatIntervalMs: containerConfig.clusterHeartbeatIntervalMs,
|
||||||
|
clusterHeartbeatTimeoutMs: containerConfig.clusterHeartbeatTimeoutMs,
|
||||||
};
|
};
|
||||||
return { config };
|
return { config };
|
||||||
},
|
},
|
||||||
|
|||||||
24
ts/types.ts
24
ts/types.ts
@@ -5,6 +5,20 @@ export interface IObjectStorageConfig {
|
|||||||
accessCredentials: Array<{ accessKeyId: string; secretAccessKey: string }>;
|
accessCredentials: Array<{ accessKeyId: string; secretAccessKey: string }>;
|
||||||
adminPassword: string;
|
adminPassword: string;
|
||||||
region: string;
|
region: string;
|
||||||
|
// Cluster
|
||||||
|
clusterEnabled: boolean;
|
||||||
|
clusterNodeId: string;
|
||||||
|
clusterQuicPort: number;
|
||||||
|
clusterSeedNodes: string[];
|
||||||
|
// Erasure coding
|
||||||
|
erasureDataShards: number;
|
||||||
|
erasureParityShards: number;
|
||||||
|
erasureChunkSizeBytes: number;
|
||||||
|
// Multi-drive
|
||||||
|
drivePaths: string[];
|
||||||
|
// Cluster heartbeat
|
||||||
|
clusterHeartbeatIntervalMs: number;
|
||||||
|
clusterHeartbeatTimeoutMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultConfig: IObjectStorageConfig = {
|
export const defaultConfig: IObjectStorageConfig = {
|
||||||
@@ -14,4 +28,14 @@ export const defaultConfig: IObjectStorageConfig = {
|
|||||||
accessCredentials: [{ accessKeyId: 'admin', secretAccessKey: 'admin' }],
|
accessCredentials: [{ accessKeyId: 'admin', secretAccessKey: 'admin' }],
|
||||||
adminPassword: 'admin',
|
adminPassword: 'admin',
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
|
clusterEnabled: false,
|
||||||
|
clusterNodeId: '',
|
||||||
|
clusterQuicPort: 4433,
|
||||||
|
clusterSeedNodes: [],
|
||||||
|
erasureDataShards: 4,
|
||||||
|
erasureParityShards: 2,
|
||||||
|
erasureChunkSizeBytes: 4194304,
|
||||||
|
drivePaths: [],
|
||||||
|
clusterHeartbeatIntervalMs: 5000,
|
||||||
|
clusterHeartbeatTimeoutMs: 30000,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -19,6 +19,20 @@ export interface IServerConfig {
|
|||||||
storageDirectory: string;
|
storageDirectory: string;
|
||||||
authEnabled: boolean;
|
authEnabled: boolean;
|
||||||
corsEnabled: boolean;
|
corsEnabled: boolean;
|
||||||
|
// Cluster
|
||||||
|
clusterEnabled: boolean;
|
||||||
|
clusterNodeId: string;
|
||||||
|
clusterQuicPort: number;
|
||||||
|
clusterSeedNodes: string[];
|
||||||
|
// Erasure coding
|
||||||
|
erasureDataShards: number;
|
||||||
|
erasureParityShards: number;
|
||||||
|
erasureChunkSizeBytes: number;
|
||||||
|
// Multi-drive
|
||||||
|
drivePaths: string[];
|
||||||
|
// Cluster heartbeat
|
||||||
|
clusterHeartbeatIntervalMs: number;
|
||||||
|
clusterHeartbeatTimeoutMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IObjstCredential {
|
export interface IObjstCredential {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@lossless.zone/objectstorage',
|
name: '@lossless.zone/objectstorage',
|
||||||
version: '1.6.0',
|
version: '1.8.1',
|
||||||
description: 'object storage server with management UI powered by smartstorage'
|
description: 'object storage server with management UI powered by smartstorage'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.ts';
|
||||||
import * as appstate from './appstate.js';
|
import * as appstate from './appstate.ts';
|
||||||
import * as interfaces from '../ts_interfaces/index.js';
|
import * as interfaces from '../ts_interfaces/index.ts';
|
||||||
import type { IS3DataProvider } from '@design.estate/dees-catalog';
|
import type { IStorageDataProvider } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
const getIdentity = (): interfaces.data.IIdentity => {
|
const getIdentity = (): interfaces.data.IIdentity => {
|
||||||
return appstate.loginStatePart.getState().identity!;
|
return appstate.loginStatePart.getState().identity!;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDataProvider = (): IS3DataProvider => ({
|
export const createDataProvider = (): IStorageDataProvider => ({
|
||||||
async listObjects(bucket: string, prefix?: string, delimiter?: string) {
|
async listObjects(bucket: string, prefix?: string, delimiter?: string) {
|
||||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_ListObjects
|
interfaces.requests.IReq_ListObjects
|
||||||
|
|||||||
@@ -36,12 +36,84 @@ export class ObjstViewConfig extends DeesElement {
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.sectionSpacer {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
.infoPanel {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a4a')};
|
||||||
|
}
|
||||||
|
.infoPanel h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
}
|
||||||
|
.infoPanel p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.infoPanel .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.infoPanel .label {
|
||||||
|
min-width: 260px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#1565c0', '#64b5f6')};
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a237e30')};
|
||||||
|
}
|
||||||
|
.infoPanel .value {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.driveList {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.driveList .driveItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.driveList .driveIndex {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: ${cssManager.bdTheme('#e8eaf6', '#1a237e40')};
|
||||||
|
color: ${cssManager.bdTheme('#3f51b5', '#7986cb')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.driveList .drivePath {
|
||||||
|
font-family: monospace;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#e8e8e8', '#252540')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const config = this.configState.config;
|
const config = this.configState.config;
|
||||||
|
|
||||||
const tiles: IStatsTile[] = [
|
const serverTiles: IStatsTile[] = [
|
||||||
{
|
{
|
||||||
id: 'objstPort',
|
id: 'objstPort',
|
||||||
title: 'Storage API Port',
|
title: 'Storage API Port',
|
||||||
@@ -92,20 +164,166 @@ export class ObjstViewConfig extends DeesElement {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
const clusterTiles: IStatsTile[] = [
|
||||||
<objst-sectionheading>Configuration</objst-sectionheading>
|
|
||||||
<dees-statsgrid
|
|
||||||
.tiles=${tiles}
|
|
||||||
.gridActions=${[
|
|
||||||
{
|
{
|
||||||
|
id: 'clusterStatus',
|
||||||
|
title: 'Cluster Status',
|
||||||
|
value: config?.clusterEnabled ? 'Enabled' : 'Disabled',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:network',
|
||||||
|
color: config?.clusterEnabled ? '#4caf50' : '#ff9800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nodeId',
|
||||||
|
title: 'Node ID',
|
||||||
|
value: config?.clusterNodeId || '(auto)',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:fingerprint',
|
||||||
|
color: '#607d8b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quicPort',
|
||||||
|
title: 'QUIC Port',
|
||||||
|
value: config?.clusterQuicPort ?? 4433,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:radio',
|
||||||
|
color: '#00bcd4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'seedNodes',
|
||||||
|
title: 'Seed Nodes',
|
||||||
|
value: config?.clusterSeedNodes?.length ?? 0,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:gitBranch',
|
||||||
|
color: '#3f51b5',
|
||||||
|
description: config?.clusterSeedNodes?.length
|
||||||
|
? config.clusterSeedNodes.join(', ')
|
||||||
|
: 'No seed nodes configured',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heartbeatInterval',
|
||||||
|
title: 'Heartbeat Interval',
|
||||||
|
value: `${config?.clusterHeartbeatIntervalMs ?? 5000}ms`,
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:heartPulse',
|
||||||
|
color: '#e91e63',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heartbeatTimeout',
|
||||||
|
title: 'Heartbeat Timeout',
|
||||||
|
value: `${config?.clusterHeartbeatTimeoutMs ?? 30000}ms`,
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:timer',
|
||||||
|
color: '#ff5722',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const erasureTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'dataShards',
|
||||||
|
title: 'Data Shards',
|
||||||
|
value: config?.erasureDataShards ?? 4,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:layers',
|
||||||
|
color: '#2196f3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'parityShards',
|
||||||
|
title: 'Parity Shards',
|
||||||
|
value: config?.erasureParityShards ?? 2,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:shieldCheck',
|
||||||
|
color: '#4caf50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chunkSize',
|
||||||
|
title: 'Chunk Size',
|
||||||
|
value: this.formatBytes(config?.erasureChunkSizeBytes ?? 4194304),
|
||||||
|
type: 'text',
|
||||||
|
icon: 'lucide:puzzle',
|
||||||
|
color: '#9c27b0',
|
||||||
|
description: `${config?.erasureDataShards ?? 4}+${config?.erasureParityShards ?? 2} = ${Math.round(((config?.erasureParityShards ?? 2) / (config?.erasureDataShards ?? 4)) * 100)}% overhead`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const drivePaths = config?.drivePaths?.length
|
||||||
|
? config.drivePaths
|
||||||
|
: config?.storageDirectory
|
||||||
|
? [config.storageDirectory]
|
||||||
|
: ['/data'];
|
||||||
|
|
||||||
|
const driveTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'driveCount',
|
||||||
|
title: 'Drive Count',
|
||||||
|
value: drivePaths.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:hardDrive',
|
||||||
|
color: '#3f51b5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const refreshAction = {
|
||||||
name: 'Refresh',
|
name: 'Refresh',
|
||||||
iconName: 'lucide:refreshCw',
|
iconName: 'lucide:refreshCw',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
]}
|
|
||||||
|
return html`
|
||||||
|
<objst-sectionheading>Server Configuration</objst-sectionheading>
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${serverTiles}
|
||||||
|
.gridActions=${[refreshAction]}
|
||||||
></dees-statsgrid>
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
<div class="sectionSpacer">
|
||||||
|
<objst-sectionheading>Cluster Configuration</objst-sectionheading>
|
||||||
|
</div>
|
||||||
|
<dees-statsgrid .tiles=${clusterTiles}></dees-statsgrid>
|
||||||
|
|
||||||
|
${config?.clusterEnabled ? html`
|
||||||
|
<div class="sectionSpacer">
|
||||||
|
<objst-sectionheading>Erasure Coding</objst-sectionheading>
|
||||||
|
</div>
|
||||||
|
<dees-statsgrid .tiles=${erasureTiles}></dees-statsgrid>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="sectionSpacer">
|
||||||
|
<objst-sectionheading>Storage Drives</objst-sectionheading>
|
||||||
|
</div>
|
||||||
|
<dees-statsgrid .tiles=${driveTiles}></dees-statsgrid>
|
||||||
|
<div class="driveList">
|
||||||
|
${drivePaths.map((path, i) => html`
|
||||||
|
<div class="driveItem">
|
||||||
|
<div class="driveIndex">${i + 1}</div>
|
||||||
|
<span class="drivePath">${path}</span>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="infoPanel">
|
||||||
|
<h2>Configuration Reference</h2>
|
||||||
|
<p>Cluster and drive settings are applied at server startup. To change them, set the environment variables and restart the server.</p>
|
||||||
|
<div class="row"><span class="label">OBJST_CLUSTER_ENABLED</span><span class="value">Enable clustering (true/false)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_CLUSTER_NODE_ID</span><span class="value">Unique node identifier</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_CLUSTER_QUIC_PORT</span><span class="value">QUIC transport port (default: 4433)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_CLUSTER_SEED_NODES</span><span class="value">Comma-separated seed node addresses</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_DRIVE_PATHS</span><span class="value">Comma-separated drive mount paths</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_ERASURE_DATA_SHARDS</span><span class="value">Data shards for erasure coding (default: 4)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_ERASURE_PARITY_SHARDS</span><span class="value">Parity shards for erasure coding (default: 2)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_ERASURE_CHUNK_SIZE</span><span class="value">Chunk size in bytes (default: 4194304)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_HEARTBEAT_INTERVAL_MS</span><span class="value">Heartbeat interval in ms (default: 5000)</span></div>
|
||||||
|
<div class="row"><span class="label">OBJST_HEARTBEAT_TIMEOUT_MS</span><span class="value">Heartbeat timeout in ms (default: 30000)</span></div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,10 +123,10 @@ export class ObjstViewObjects extends DeesElement {
|
|||||||
${this.selectedBucket
|
${this.selectedBucket
|
||||||
? html`
|
? html`
|
||||||
<div class="browser-container">
|
<div class="browser-container">
|
||||||
<dees-s3-browser
|
<dees-storage-browser
|
||||||
.dataProvider=${this.dataProvider}
|
.dataProvider=${this.dataProvider}
|
||||||
.bucketName=${this.selectedBucket}
|
.bucketName=${this.selectedBucket}
|
||||||
></dees-s3-browser>
|
></dees-storage-browser>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
|
|||||||
Reference in New Issue
Block a user