Files
smartregistry/ts/classes.registrystorage.ts

312 lines
8.9 KiB
TypeScript
Raw Normal View History

2025-11-19 15:16:20 +00:00
import * as plugins from './plugins.js';
import { IRegistryConfig, IOciManifest, IOciImageIndex, ITagList } from './interfaces.js';
/**
* Storage layer for OCI registry using SmartBucket
*/
export class RegistryStorage {
private smartBucket: plugins.smartbucket.SmartBucket;
private bucket: plugins.smartbucket.Bucket;
private bucketName: string;
constructor(private config: IRegistryConfig['storage']) {
this.bucketName = config.bucketName;
}
/**
* Initialize the storage backend
*/
public async init() {
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, that's fine
});
this.bucket = await this.smartBucket.getBucketByName(this.bucketName);
}
/**
* Store a blob
* @param digest - Content digest (e.g., "sha256:abc123...")
* @param data - Blob data
*/
public async putBlob(digest: string, data: Buffer): Promise<void> {
const path = this.getBlobPath(digest);
await this.bucket.fastPut({
path,
contents: data,
});
}
/**
* Retrieve a blob
* @param digest - Content digest
* @returns Blob data or null if not found
*/
public async getBlob(digest: string): Promise<Buffer | null> {
const path = this.getBlobPath(digest);
try {
return await this.bucket.fastGet({ path });
} catch (error) {
return null;
}
}
/**
* Check if blob exists
* @param digest - Content digest
* @returns true if exists
*/
public async blobExists(digest: string): Promise<boolean> {
const path = this.getBlobPath(digest);
return await this.bucket.fastExists({ path });
}
/**
* Delete a blob
* @param digest - Content digest
*/
public async deleteBlob(digest: string): Promise<void> {
const path = this.getBlobPath(digest);
await this.bucket.fastRemove({ path });
}
/**
* Store a manifest
* @param repository - Repository name (e.g., "library/nginx")
* @param reference - Tag or digest
* @param manifest - Manifest content
* @param contentType - Manifest media type
*/
public async putManifest(
repository: string,
reference: string,
manifest: IOciManifest | IOciImageIndex,
contentType: string
): Promise<string> {
const manifestJson = JSON.stringify(manifest);
const manifestBuffer = Buffer.from(manifestJson, 'utf-8');
// Calculate digest
const digest = await this.calculateDigest(manifestBuffer);
// Store by digest
const digestPath = this.getManifestPath(repository, digest);
await this.bucket.fastPut({
path: digestPath,
contents: manifestBuffer,
meta: { 'Content-Type': contentType },
});
// If reference is a tag (not a digest), create/update tag mapping
if (!reference.startsWith('sha256:')) {
await this.putTag(repository, reference, digest);
}
return digest;
}
/**
* Retrieve a manifest
* @param repository - Repository name
* @param reference - Tag or digest
* @returns Manifest data and content type, or null if not found
*/
public async getManifest(
repository: string,
reference: string
): Promise<{ data: Buffer; contentType: string } | null> {
let digest = reference;
// If reference is a tag, resolve to digest
if (!reference.startsWith('sha256:')) {
const resolvedDigest = await this.getTagDigest(repository, reference);
if (!resolvedDigest) return null;
digest = resolvedDigest;
}
const path = this.getManifestPath(repository, digest);
try {
const data = await this.bucket.fastGet({ path });
// TODO: Retrieve content type from metadata if SmartBucket supports it
const contentType = 'application/vnd.oci.image.manifest.v1+json';
return { data, contentType };
} catch (error) {
return null;
}
}
/**
* Check if manifest exists
* @param repository - Repository name
* @param reference - Tag or digest
* @returns true if exists
*/
public async manifestExists(repository: string, reference: string): Promise<boolean> {
let digest = reference;
// If reference is a tag, resolve to digest
if (!reference.startsWith('sha256:')) {
const resolvedDigest = await this.getTagDigest(repository, reference);
if (!resolvedDigest) return false;
digest = resolvedDigest;
}
const path = this.getManifestPath(repository, digest);
return await this.bucket.fastExists({ path });
}
/**
* Delete a manifest
* @param repository - Repository name
* @param digest - Manifest digest (must be digest, not tag)
*/
public async deleteManifest(repository: string, digest: string): Promise<void> {
const path = this.getManifestPath(repository, digest);
await this.bucket.fastRemove({ path });
}
/**
* Store tag mapping
* @param repository - Repository name
* @param tag - Tag name
* @param digest - Manifest digest
*/
public async putTag(repository: string, tag: string, digest: string): Promise<void> {
const tags = await this.getTags(repository);
tags[tag] = digest;
const path = this.getTagsPath(repository);
await this.bucket.fastPut({
path,
contents: Buffer.from(JSON.stringify(tags, null, 2), 'utf-8'),
});
}
/**
* Get digest for a tag
* @param repository - Repository name
* @param tag - Tag name
* @returns Digest or null if tag doesn't exist
*/
public async getTagDigest(repository: string, tag: string): Promise<string | null> {
const tags = await this.getTags(repository);
return tags[tag] || null;
}
/**
* List all tags for a repository
* @param repository - Repository name
* @returns Tag list
*/
public async listTags(repository: string): Promise<string[]> {
const tags = await this.getTags(repository);
return Object.keys(tags);
}
/**
* Delete a tag
* @param repository - Repository name
* @param tag - Tag name
*/
public async deleteTag(repository: string, tag: string): Promise<void> {
const tags = await this.getTags(repository);
delete tags[tag];
const path = this.getTagsPath(repository);
await this.bucket.fastPut({
path,
contents: Buffer.from(JSON.stringify(tags, null, 2), 'utf-8'),
});
}
/**
* Get all manifests that reference a specific digest (referrers API)
* @param repository - Repository name
* @param digest - Subject digest
* @returns Array of manifest digests
*/
public async getReferrers(repository: string, digest: string): Promise<string[]> {
// This is a simplified implementation
// In production, you'd want to maintain an index
const referrersPath = this.getReferrersPath(repository, digest);
try {
const data = await this.bucket.fastGet({ path: referrersPath });
const referrers = JSON.parse(data.toString('utf-8'));
return referrers;
} catch (error) {
return [];
}
}
/**
* Add a referrer relationship
* @param repository - Repository name
* @param subjectDigest - Digest being referenced
* @param referrerDigest - Digest of the referrer
*/
public async addReferrer(
repository: string,
subjectDigest: string,
referrerDigest: string
): Promise<void> {
const referrers = await this.getReferrers(repository, subjectDigest);
if (!referrers.includes(referrerDigest)) {
referrers.push(referrerDigest);
}
const path = this.getReferrersPath(repository, subjectDigest);
await this.bucket.fastPut({
path,
contents: Buffer.from(JSON.stringify(referrers, null, 2), 'utf-8'),
});
}
// Helper methods
private getBlobPath(digest: string): string {
// Remove algorithm prefix for path (sha256:abc -> abc)
const hash = digest.split(':')[1];
return `blobs/sha256/${hash}`;
}
private getManifestPath(repository: string, digest: string): string {
const hash = digest.split(':')[1];
return `manifests/${repository}/${hash}`;
}
private getTagsPath(repository: string): string {
return `tags/${repository}/tags.json`;
}
private getReferrersPath(repository: string, digest: string): string {
const hash = digest.split(':')[1];
return `referrers/${repository}/${hash}.json`;
}
private async getTags(repository: string): Promise<{ [tag: string]: string }> {
const path = this.getTagsPath(repository);
try {
const data = await this.bucket.fastGet({ path });
return JSON.parse(data.toString('utf-8'));
} catch (error) {
return {};
}
}
private async calculateDigest(data: Buffer): Promise<string> {
const crypto = await import('crypto');
const hash = crypto.createHash('sha256').update(data).digest('hex');
return `sha256:${hash}`;
}
}