Phase4
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 13s

This commit is contained in:
smueller
2026-06-26 12:32:27 +02:00
parent 9b061c3ee7
commit 4262464cd5
101 changed files with 4024 additions and 74 deletions

View File

@@ -0,0 +1,23 @@
{
"name": "@hexahost/storage",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/s3-request-presigner": "^3.750.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@hexahost/typescript-config": "workspace:*",
"@types/node": "^22.10.2",
"typescript": "^5.7.3",
"vitest": "^3.0.8"
}
}

View File

@@ -0,0 +1,90 @@
import {
DeleteObjectCommand,
HeadObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { StorageConfig } from './config';
export function createS3Client(config: StorageConfig): S3Client {
return new S3Client({
region: config.S3_REGION,
endpoint: config.S3_ENDPOINT,
forcePathStyle: config.S3_FORCE_PATH_STYLE,
credentials: {
accessKeyId: config.S3_ACCESS_KEY,
secretAccessKey: config.S3_SECRET_KEY,
},
});
}
export function backupObjectKey(serverId: string, backupId: string): string {
return `servers/${serverId}/backups/${backupId}.tar.gz`;
}
export function worldUploadObjectKey(serverId: string, uploadId: string): string {
return `servers/${serverId}/uploads/${uploadId}/world.tar.gz`;
}
export async function createPresignedPutUrl(
client: S3Client,
bucket: string,
key: string,
expiresInSeconds: number,
contentType = 'application/gzip',
): Promise<string> {
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
return getSignedUrl(client, command, { expiresIn: expiresInSeconds });
}
export async function createPresignedGetUrl(
client: S3Client,
bucket: string,
key: string,
expiresInSeconds: number,
): Promise<string> {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
return getSignedUrl(client, command, { expiresIn: expiresInSeconds });
}
export async function deleteStoredObject(
client: S3Client,
bucket: string,
key: string,
): Promise<void> {
await client.send(
new DeleteObjectCommand({
Bucket: bucket,
Key: key,
}),
);
}
export async function headStoredObject(
client: S3Client,
bucket: string,
key: string,
): Promise<{ sizeBytes: number; etag?: string }> {
const response = await client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: key,
}),
);
return {
sizeBytes: Number(response.ContentLength ?? 0),
etag: response.ETag,
};
}

View File

@@ -0,0 +1,28 @@
import { z } from 'zod';
export const storageConfigSchema = z.object({
S3_ENDPOINT: z.string().url(),
S3_REGION: z.string().min(1).default('us-east-1'),
S3_BUCKET: z.string().min(1),
S3_ACCESS_KEY: z.string().min(1),
S3_SECRET_KEY: z.string().min(1),
S3_FORCE_PATH_STYLE: z
.string()
.optional()
.transform((value) => value === 'true' || value === '1'),
});
export type StorageConfig = z.infer<typeof storageConfigSchema>;
export function loadStorageConfig(
env: Record<string, string | undefined> = process.env,
): StorageConfig {
return storageConfigSchema.parse({
S3_ENDPOINT: env['S3_ENDPOINT'],
S3_REGION: env['S3_REGION'],
S3_BUCKET: env['S3_BUCKET'],
S3_ACCESS_KEY: env['S3_ACCESS_KEY'],
S3_SECRET_KEY: env['S3_SECRET_KEY'],
S3_FORCE_PATH_STYLE: env['S3_FORCE_PATH_STYLE'],
});
}

View File

@@ -0,0 +1,15 @@
export {
storageConfigSchema,
loadStorageConfig,
type StorageConfig,
} from './config';
export {
createS3Client,
backupObjectKey,
worldUploadObjectKey,
createPresignedPutUrl,
createPresignedGetUrl,
deleteStoredObject,
headStoredObject,
} from './client';

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { backupObjectKey, worldUploadObjectKey } from './client';
describe('storage object keys', () => {
it('builds backup keys', () => {
expect(backupObjectKey('srv-1', 'bak-1')).toBe(
'servers/srv-1/backups/bak-1.tar.gz',
);
});
it('builds world upload keys', () => {
expect(worldUploadObjectKey('srv-1', 'up-1')).toBe(
'servers/srv-1/uploads/up-1/world.tar.gz',
);
});
});

View File

@@ -0,0 +1,9 @@
{
"extends": "@hexahost/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}