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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete an object
|
|
|
|
|
*/
|
|
|
|
|
public async deleteObject(key: string): Promise<void> {
|
|
|
|
|
await this.bucket.fastRemove({ path: key });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List objects with a prefix
|
|
|
|
|
*/
|
|
|
|
|
public async listObjects(prefix: string): Promise<string[]> {
|
|
|
|
|
const baseDir = await this.bucket.getBaseDirectory();
|
|
|
|
|
const dir = prefix ? await baseDir.getSubDirectoryByName(prefix) : baseDir;
|
|
|
|
|
if (!dir) return [];
|
|
|
|
|
|
|
|
|
|
const files = await dir.listFiles();
|
2025-11-19 15:32:00 +00:00
|
|
|
return files.map(f => f.getBasePath());
|
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`;
|
|
|
|
|
}
|
|
|
|
|
}
|