Files
smartregistry/ts/core/classes.registrystorage.ts

605 lines
18 KiB
TypeScript
Raw Normal View History

2025-11-19 15:17:32 +00:00
import * as plugins from '../plugins.js';
2025-11-19 15:32:00 +00:00
import type { IStorageConfig, IStorageBackend } from './interfaces.core.js';
2025-11-19 15:17:32 +00:00
/**
* Storage abstraction layer for registry
* Provides a unified interface over SmartBucket
*/
export class RegistryStorage implements IStorageBackend {
private smartBucket: plugins.smartbucket.SmartBucket;
private bucket: plugins.smartbucket.Bucket;
private bucketName: string;
constructor(private config: IStorageConfig) {
this.bucketName = config.bucketName;
}
/**
* Initialize the storage backend
*/
public async init(): Promise<void> {
this.smartBucket = new plugins.smartbucket.SmartBucket({
accessKey: this.config.accessKey,
accessSecret: this.config.accessSecret,
endpoint: this.config.endpoint,
port: this.config.port || 443,
useSsl: this.config.useSsl !== false,
region: this.config.region || 'us-east-1',
});
// Ensure bucket exists
await this.smartBucket.createBucket(this.bucketName).catch(() => {
// Bucket may already exist
});
this.bucket = await this.smartBucket.getBucketByName(this.bucketName);
}
/**
* Get an object from storage
*/
public async getObject(key: string): Promise<Buffer | null> {
try {
return await this.bucket.fastGet({ path: key });
} catch (error) {
return null;
}
}
/**
* Store an object
*/
public async putObject(
key: string,
data: Buffer,
metadata?: Record<string, string>
): Promise<void> {
2025-11-19 15:32:00 +00:00
// Note: SmartBucket doesn't support metadata yet
2025-11-19 15:17:32 +00:00
await this.bucket.fastPut({
path: key,
contents: data,
2025-11-20 13:27:26 +00:00
overwrite: true, // Always overwrite existing objects
2025-11-19 15:17:32 +00:00
});
}
/**
* Delete an object
*/
public async deleteObject(key: string): Promise<void> {
await this.bucket.fastRemove({ path: key });
}
/**
2025-11-20 13:57:05 +00:00
* List objects with a prefix (recursively)
2025-11-19 15:17:32 +00:00
*/
public async listObjects(prefix: string): Promise<string[]> {
2025-11-20 13:57:05 +00:00
const paths: string[] = [];
for await (const path of this.bucket.listAllObjects(prefix)) {
paths.push(path);
2025-11-20 13:57:05 +00:00
}
return paths;
2025-11-19 15:17:32 +00:00
}
/**
* Check if an object exists
*/
public async objectExists(key: string): Promise<boolean> {
return await this.bucket.fastExists({ path: key });
}
/**
* Get object metadata
* Note: SmartBucket may not support metadata retrieval, returning empty object
*/
public async getMetadata(key: string): Promise<Record<string, string> | null> {
// SmartBucket doesn't expose metadata retrieval directly
// This is a limitation we'll document
const exists = await this.objectExists(key);
return exists ? {} : null;
}
// ========================================================================
// OCI-SPECIFIC HELPERS
// ========================================================================
/**
* Get OCI blob by digest
*/
public async getOciBlob(digest: string): Promise<Buffer | null> {
const path = this.getOciBlobPath(digest);
return this.getObject(path);
}
/**
* Store OCI blob
*/
public async putOciBlob(digest: string, data: Buffer): Promise<void> {
const path = this.getOciBlobPath(digest);
return this.putObject(path, data);
}
/**
* Check if OCI blob exists
*/
public async ociBlobExists(digest: string): Promise<boolean> {
const path = this.getOciBlobPath(digest);
return this.objectExists(path);
}
/**
* Delete OCI blob
*/
public async deleteOciBlob(digest: string): Promise<void> {
const path = this.getOciBlobPath(digest);
return this.deleteObject(path);
}
/**
* Get OCI manifest
*/
public async getOciManifest(repository: string, digest: string): Promise<Buffer | null> {
const path = this.getOciManifestPath(repository, digest);
return this.getObject(path);
}
/**
* Store OCI manifest
*/
public async putOciManifest(
repository: string,
digest: string,
data: Buffer,
contentType: string
): Promise<void> {
const path = this.getOciManifestPath(repository, digest);
return this.putObject(path, data, { 'Content-Type': contentType });
}
/**
* Check if OCI manifest exists
*/
public async ociManifestExists(repository: string, digest: string): Promise<boolean> {
const path = this.getOciManifestPath(repository, digest);
return this.objectExists(path);
}
/**
* Delete OCI manifest
*/
public async deleteOciManifest(repository: string, digest: string): Promise<void> {
const path = this.getOciManifestPath(repository, digest);
return this.deleteObject(path);
}
// ========================================================================
// NPM-SPECIFIC HELPERS
// ========================================================================
/**
* Get NPM packument (package document)
*/
public async getNpmPackument(packageName: string): Promise<any | null> {
const path = this.getNpmPackumentPath(packageName);
const data = await this.getObject(path);
return data ? JSON.parse(data.toString('utf-8')) : null;
}
/**
* Store NPM packument
*/
public async putNpmPackument(packageName: string, packument: any): Promise<void> {
const path = this.getNpmPackumentPath(packageName);
const data = Buffer.from(JSON.stringify(packument, null, 2), 'utf-8');
return this.putObject(path, data, { 'Content-Type': 'application/json' });
}
/**
* Check if NPM packument exists
*/
public async npmPackumentExists(packageName: string): Promise<boolean> {
const path = this.getNpmPackumentPath(packageName);
return this.objectExists(path);
}
/**
* Delete NPM packument
*/
public async deleteNpmPackument(packageName: string): Promise<void> {
const path = this.getNpmPackumentPath(packageName);
return this.deleteObject(path);
}
/**
* Get NPM tarball
*/
public async getNpmTarball(packageName: string, version: string): Promise<Buffer | null> {
const path = this.getNpmTarballPath(packageName, version);
return this.getObject(path);
}
/**
* Store NPM tarball
*/
public async putNpmTarball(
packageName: string,
version: string,
tarball: Buffer
): Promise<void> {
const path = this.getNpmTarballPath(packageName, version);
return this.putObject(path, tarball, { 'Content-Type': 'application/octet-stream' });
}
/**
* Check if NPM tarball exists
*/
public async npmTarballExists(packageName: string, version: string): Promise<boolean> {
const path = this.getNpmTarballPath(packageName, version);
return this.objectExists(path);
}
/**
* Delete NPM tarball
*/
public async deleteNpmTarball(packageName: string, version: string): Promise<void> {
const path = this.getNpmTarballPath(packageName, version);
return this.deleteObject(path);
}
// ========================================================================
// PATH HELPERS
// ========================================================================
private getOciBlobPath(digest: string): string {
const hash = digest.split(':')[1];
return `oci/blobs/sha256/${hash}`;
}
private getOciManifestPath(repository: string, digest: string): string {
const hash = digest.split(':')[1];
return `oci/manifests/${repository}/${hash}`;
}
private getNpmPackumentPath(packageName: string): string {
return `npm/packages/${packageName}/index.json`;
}
private getNpmTarballPath(packageName: string, version: string): string {
const safeName = packageName.replace('@', '').replace('/', '-');
return `npm/packages/${packageName}/${safeName}-${version}.tgz`;
}
// ========================================================================
// MAVEN STORAGE METHODS
// ========================================================================
/**
* Get Maven artifact
*/
public async getMavenArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<Buffer | null> {
const path = this.getMavenArtifactPath(groupId, artifactId, version, filename);
return this.getObject(path);
}
/**
* Store Maven artifact
*/
public async putMavenArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string,
data: Buffer
): Promise<void> {
const path = this.getMavenArtifactPath(groupId, artifactId, version, filename);
return this.putObject(path, data);
}
/**
* Check if Maven artifact exists
*/
public async mavenArtifactExists(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<boolean> {
const path = this.getMavenArtifactPath(groupId, artifactId, version, filename);
return this.objectExists(path);
}
/**
* Delete Maven artifact
*/
public async deleteMavenArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<void> {
const path = this.getMavenArtifactPath(groupId, artifactId, version, filename);
return this.deleteObject(path);
}
/**
* Get Maven metadata (maven-metadata.xml)
*/
public async getMavenMetadata(
groupId: string,
artifactId: string
): Promise<Buffer | null> {
const path = this.getMavenMetadataPath(groupId, artifactId);
return this.getObject(path);
}
/**
* Store Maven metadata (maven-metadata.xml)
*/
public async putMavenMetadata(
groupId: string,
artifactId: string,
data: Buffer
): Promise<void> {
const path = this.getMavenMetadataPath(groupId, artifactId);
return this.putObject(path, data);
}
/**
* Delete Maven metadata (maven-metadata.xml)
*/
public async deleteMavenMetadata(
groupId: string,
artifactId: string
): Promise<void> {
const path = this.getMavenMetadataPath(groupId, artifactId);
return this.deleteObject(path);
}
/**
* List Maven versions for an artifact
* Returns all version directories under the artifact path
*/
public async listMavenVersions(
groupId: string,
artifactId: string
): Promise<string[]> {
const groupPath = groupId.replace(/\./g, '/');
const prefix = `maven/artifacts/${groupPath}/${artifactId}/`;
const objects = await this.listObjects(prefix);
const versions = new Set<string>();
// Extract version from paths like: maven/artifacts/com/example/my-lib/1.0.0/my-lib-1.0.0.jar
for (const obj of objects) {
const relativePath = obj.substring(prefix.length);
const parts = relativePath.split('/');
if (parts.length >= 1 && parts[0]) {
versions.add(parts[0]);
}
}
return Array.from(versions).sort();
}
// ========================================================================
// MAVEN PATH HELPERS
// ========================================================================
private getMavenArtifactPath(
groupId: string,
artifactId: string,
version: string,
filename: string
): string {
const groupPath = groupId.replace(/\./g, '/');
return `maven/artifacts/${groupPath}/${artifactId}/${version}/${filename}`;
}
private getMavenMetadataPath(groupId: string, artifactId: string): string {
const groupPath = groupId.replace(/\./g, '/');
return `maven/metadata/${groupPath}/${artifactId}/maven-metadata.xml`;
}
// ========================================================================
// CARGO-SPECIFIC HELPERS
// ========================================================================
/**
* Get Cargo config.json
*/
public async getCargoConfig(): Promise<any | null> {
const data = await this.getObject('cargo/config.json');
return data ? JSON.parse(data.toString('utf-8')) : null;
}
/**
* Store Cargo config.json
*/
public async putCargoConfig(config: any): Promise<void> {
const data = Buffer.from(JSON.stringify(config, null, 2), 'utf-8');
return this.putObject('cargo/config.json', data, { 'Content-Type': 'application/json' });
}
/**
* Get Cargo index file (newline-delimited JSON)
*/
public async getCargoIndex(crateName: string): Promise<any[] | null> {
const path = this.getCargoIndexPath(crateName);
const data = await this.getObject(path);
if (!data) return null;
// Parse newline-delimited JSON
const lines = data.toString('utf-8').split('\n').filter(line => line.trim());
return lines.map(line => JSON.parse(line));
}
/**
* Store Cargo index file
*/
public async putCargoIndex(crateName: string, entries: any[]): Promise<void> {
const path = this.getCargoIndexPath(crateName);
// Convert to newline-delimited JSON
const data = Buffer.from(entries.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
return this.putObject(path, data, { 'Content-Type': 'text/plain' });
}
/**
* Get Cargo .crate file
*/
public async getCargoCrate(crateName: string, version: string): Promise<Buffer | null> {
const path = this.getCargoCratePath(crateName, version);
return this.getObject(path);
}
/**
* Store Cargo .crate file
*/
public async putCargoCrate(
crateName: string,
version: string,
crateFile: Buffer
): Promise<void> {
const path = this.getCargoCratePath(crateName, version);
return this.putObject(path, crateFile, { 'Content-Type': 'application/gzip' });
}
/**
* Check if Cargo crate exists
*/
public async cargoCrateExists(crateName: string, version: string): Promise<boolean> {
const path = this.getCargoCratePath(crateName, version);
return this.objectExists(path);
}
/**
* Delete Cargo crate (for cleanup, not for unpublishing)
*/
public async deleteCargoCrate(crateName: string, version: string): Promise<void> {
const path = this.getCargoCratePath(crateName, version);
return this.deleteObject(path);
}
// ========================================================================
// CARGO PATH HELPERS
// ========================================================================
private getCargoIndexPath(crateName: string): string {
const lower = crateName.toLowerCase();
const len = lower.length;
if (len === 1) {
return `cargo/index/1/${lower}`;
} else if (len === 2) {
return `cargo/index/2/${lower}`;
} else if (len === 3) {
return `cargo/index/3/${lower.charAt(0)}/${lower}`;
} else {
// 4+ characters: {first-two}/{second-two}/{name}
const prefix1 = lower.substring(0, 2);
const prefix2 = lower.substring(2, 4);
return `cargo/index/${prefix1}/${prefix2}/${lower}`;
}
}
private getCargoCratePath(crateName: string, version: string): string {
return `cargo/crates/${crateName}/${crateName}-${version}.crate`;
}
// ========================================================================
// COMPOSER-SPECIFIC HELPERS
// ========================================================================
/**
* Get Composer package metadata
*/
public async getComposerPackageMetadata(vendorPackage: string): Promise<any | null> {
const path = this.getComposerMetadataPath(vendorPackage);
const data = await this.getObject(path);
return data ? JSON.parse(data.toString('utf-8')) : null;
}
/**
* Store Composer package metadata
*/
public async putComposerPackageMetadata(vendorPackage: string, metadata: any): Promise<void> {
const path = this.getComposerMetadataPath(vendorPackage);
const data = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
return this.putObject(path, data, { 'Content-Type': 'application/json' });
}
/**
* Get Composer package ZIP
*/
public async getComposerPackageZip(vendorPackage: string, reference: string): Promise<Buffer | null> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.getObject(path);
}
/**
* Store Composer package ZIP
*/
public async putComposerPackageZip(vendorPackage: string, reference: string, zipData: Buffer): Promise<void> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.putObject(path, zipData, { 'Content-Type': 'application/zip' });
}
/**
* Check if Composer package metadata exists
*/
public async composerPackageMetadataExists(vendorPackage: string): Promise<boolean> {
const path = this.getComposerMetadataPath(vendorPackage);
return this.objectExists(path);
}
/**
* Delete Composer package metadata
*/
public async deleteComposerPackageMetadata(vendorPackage: string): Promise<void> {
const path = this.getComposerMetadataPath(vendorPackage);
return this.deleteObject(path);
}
/**
* Delete Composer package ZIP
*/
public async deleteComposerPackageZip(vendorPackage: string, reference: string): Promise<void> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.deleteObject(path);
}
/**
* List all Composer packages
*/
public async listComposerPackages(): Promise<string[]> {
const prefix = 'composer/packages/';
const objects = await this.listObjects(prefix);
const packages = new Set<string>();
// Extract vendor/package from paths like: composer/packages/vendor/package/metadata.json
for (const obj of objects) {
const match = obj.match(/^composer\/packages\/([^\/]+\/[^\/]+)\/metadata\.json$/);
if (match) {
packages.add(match[1]);
}
}
return Array.from(packages).sort();
}
// ========================================================================
// COMPOSER PATH HELPERS
// ========================================================================
private getComposerMetadataPath(vendorPackage: string): string {
return `composer/packages/${vendorPackage}/metadata.json`;
}
private getComposerZipPath(vendorPackage: string, reference: string): string {
return `composer/packages/${vendorPackage}/${reference}.zip`;
}
2025-11-19 15:17:32 +00:00
}