Phase4
This commit is contained in:
23
packages/storage/package.json
Normal file
23
packages/storage/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
90
packages/storage/src/client.ts
Normal file
90
packages/storage/src/client.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
28
packages/storage/src/config.ts
Normal file
28
packages/storage/src/config.ts
Normal 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'],
|
||||
});
|
||||
}
|
||||
15
packages/storage/src/index.ts
Normal file
15
packages/storage/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export {
|
||||
storageConfigSchema,
|
||||
loadStorageConfig,
|
||||
type StorageConfig,
|
||||
} from './config';
|
||||
|
||||
export {
|
||||
createS3Client,
|
||||
backupObjectKey,
|
||||
worldUploadObjectKey,
|
||||
createPresignedPutUrl,
|
||||
createPresignedGetUrl,
|
||||
deleteStoredObject,
|
||||
headStoredObject,
|
||||
} from './client';
|
||||
17
packages/storage/src/keys.test.ts
Normal file
17
packages/storage/src/keys.test.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
9
packages/storage/tsconfig.json
Normal file
9
packages/storage/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@hexahost/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user