Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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"]
|
||||||
|
|||||||
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lossless.zone/objectstorage",
|
"name": "@lossless.zone/objectstorage",
|
||||||
"version": "1.6.0",
|
"version": "1.7.1",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"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.1",
|
||||||
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0",
|
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0",
|
||||||
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.937.0",
|
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.937.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",
|
||||||
|
|||||||
19
deno.lock
generated
19
deno.lock
generated
@@ -17,7 +17,7 @@
|
|||||||
"npm:@push.rocks/smartbucket@^4.3.0": "4.5.1",
|
"npm:@push.rocks/smartbucket@^4.3.0": "4.5.1",
|
||||||
"npm:@push.rocks/smartguard@^3.1.0": "3.1.0",
|
"npm:@push.rocks/smartguard@^3.1.0": "3.1.0",
|
||||||
"npm:@push.rocks/smartjwt@^2.2.1": "2.2.1",
|
"npm:@push.rocks/smartjwt@^2.2.1": "2.2.1",
|
||||||
"npm:@push.rocks/smartstorage@^6.0.1": "6.0.1"
|
"npm:@push.rocks/smartstorage@^6.3.1": "6.3.1"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@std/assert@1.0.19": {
|
"@std/assert@1.0.19": {
|
||||||
@@ -1951,14 +1951,14 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartstate/-/smartstate-2.2.1.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartstate/-/smartstate-2.2.1.tgz"
|
||||||
},
|
},
|
||||||
"@push.rocks/smartstorage@6.0.1": {
|
"@push.rocks/smartstorage@6.3.1": {
|
||||||
"integrity": "sha512-W5PEVwO0J2K9YUZRTbKXadC11h6/IBzzqU+P0TIE/xpJZC4K1duEXwEhxGWcbfhCkPRRa51xH8Z5mAmzzm8qxA==",
|
"integrity": "sha512-Emxwyeb/BH//q32IB2Z34Clz0yR3mjWKgd+qhnAKA6UADRmF3DMemdz5cQVkmpQeD1YE8hu633gHmynsLTFMRA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@push.rocks/smartpath@6.0.0",
|
"@push.rocks/smartpath@6.0.0",
|
||||||
"@push.rocks/smartrust",
|
"@push.rocks/smartrust",
|
||||||
"@tsclass/tsclass@9.4.0"
|
"@tsclass/tsclass@9.5.0"
|
||||||
],
|
],
|
||||||
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartstorage/-/smartstorage-6.0.1.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartstorage/-/smartstorage-6.3.1.tgz"
|
||||||
},
|
},
|
||||||
"@push.rocks/smartstream@2.0.8": {
|
"@push.rocks/smartstream@2.0.8": {
|
||||||
"integrity": "sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==",
|
"integrity": "sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==",
|
||||||
@@ -3130,6 +3130,13 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://verdaccio.lossless.digital/@tsclass/tsclass/-/tsclass-9.4.0.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@tsclass/tsclass/-/tsclass-9.4.0.tgz"
|
||||||
},
|
},
|
||||||
|
"@tsclass/tsclass@9.5.0": {
|
||||||
|
"integrity": "sha512-HwMVwkrBnEFMjwOsMkGwWN/q+XEczSpf4a/PBAXgkDdV6sXdxAMFXUH1tW8Y5ecuvXFYMvFry4X57MCCT7Dm8A==",
|
||||||
|
"dependencies": [
|
||||||
|
"type-fest@5.4.4"
|
||||||
|
],
|
||||||
|
"tarball": "https://verdaccio.lossless.digital/@tsclass/tsclass/-/tsclass-9.5.0.tgz"
|
||||||
|
},
|
||||||
"@tybys/wasm-util@0.10.1": {
|
"@tybys/wasm-util@0.10.1": {
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -6050,7 +6057,7 @@
|
|||||||
"npm:@push.rocks/smartbucket@^4.3.0",
|
"npm:@push.rocks/smartbucket@^4.3.0",
|
||||||
"npm:@push.rocks/smartguard@^3.1.0",
|
"npm:@push.rocks/smartguard@^3.1.0",
|
||||||
"npm:@push.rocks/smartjwt@^2.2.1",
|
"npm:@push.rocks/smartjwt@^2.2.1",
|
||||||
"npm:@push.rocks/smartstorage@^6.0.1"
|
"npm:@push.rocks/smartstorage@^6.3.1"
|
||||||
],
|
],
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lossless.zone/objectstorage",
|
"name": "@lossless.zone/objectstorage",
|
||||||
"version": "1.6.0",
|
"version": "1.7.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",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@lossless.zone/objectstorage',
|
name: '@lossless.zone/objectstorage',
|
||||||
version: '1.6.0',
|
version: '1.7.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.7.1',
|
||||||
description: 'object storage server with management UI powered by smartstorage'
|
description: 'object storage server with management UI powered by smartstorage'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user