import * as plugins from '../plugins.js'; import type { IStorageConfig, IStorageBackend } from './interfaces.core.js'; /** * 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 { 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 { try { return await this.bucket.fastGet({ path: key }); } catch (error) { return null; } } /** * Store an object */ public async putObject( key: string, data: Buffer, metadata?: Record ): Promise { // Note: SmartBucket doesn't support metadata yet await this.bucket.fastPut({ path: key, contents: data, overwrite: true, // Always overwrite existing objects }); } /** * Delete an object */ public async deleteObject(key: string): Promise { await this.bucket.fastRemove({ path: key }); } /** * List objects with a prefix (recursively) */ public async listObjects(prefix: string): Promise { const paths: string[] = []; for await (const path of this.bucket.listAllObjects(prefix)) { paths.push(path); } return paths; } /** * Check if an object exists */ public async objectExists(key: string): Promise { 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 | 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 { const path = this.getOciBlobPath(digest); return this.getObject(path); } /** * Store OCI blob */ public async putOciBlob(digest: string, data: Buffer): Promise { const path = this.getOciBlobPath(digest); return this.putObject(path, data); } /** * Check if OCI blob exists */ public async ociBlobExists(digest: string): Promise { const path = this.getOciBlobPath(digest); return this.objectExists(path); } /** * Delete OCI blob */ public async deleteOciBlob(digest: string): Promise { const path = this.getOciBlobPath(digest); return this.deleteObject(path); } /** * Get OCI manifest */ public async getOciManifest(repository: string, digest: string): Promise { 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 { 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 { const path = this.getOciManifestPath(repository, digest); return this.objectExists(path); } /** * Delete OCI manifest */ public async deleteOciManifest(repository: string, digest: string): Promise { const path = this.getOciManifestPath(repository, digest); return this.deleteObject(path); } // ======================================================================== // NPM-SPECIFIC HELPERS // ======================================================================== /** * Get NPM packument (package document) */ public async getNpmPackument(packageName: string): Promise { 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 { 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 { const path = this.getNpmPackumentPath(packageName); return this.objectExists(path); } /** * Delete NPM packument */ public async deleteNpmPackument(packageName: string): Promise { const path = this.getNpmPackumentPath(packageName); return this.deleteObject(path); } /** * Get NPM tarball */ public async getNpmTarball(packageName: string, version: string): Promise { const path = this.getNpmTarballPath(packageName, version); return this.getObject(path); } /** * Store NPM tarball */ public async putNpmTarball( packageName: string, version: string, tarball: Buffer ): Promise { 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 { const path = this.getNpmTarballPath(packageName, version); return this.objectExists(path); } /** * Delete NPM tarball */ public async deleteNpmTarball(packageName: string, version: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { const path = this.getMavenMetadataPath(groupId, artifactId); return this.putObject(path, data); } /** * List Maven versions for an artifact * Returns all version directories under the artifact path */ public async listMavenVersions( groupId: string, artifactId: string ): Promise { const groupPath = groupId.replace(/\./g, '/'); const prefix = `maven/artifacts/${groupPath}/${artifactId}/`; const objects = await this.listObjects(prefix); const versions = new Set(); // 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 { 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 { 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 { 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 { 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 { const path = this.getCargoCratePath(crateName, version); return this.getObject(path); } /** * Store Cargo .crate file */ public async putCargoCrate( crateName: string, version: string, crateFile: Buffer ): Promise { 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 { 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 { 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 { 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 { 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 { const path = this.getComposerZipPath(vendorPackage, reference); return this.getObject(path); } /** * Store Composer package ZIP */ public async putComposerPackageZip(vendorPackage: string, reference: string, zipData: Buffer): Promise { 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 { const path = this.getComposerMetadataPath(vendorPackage); return this.objectExists(path); } /** * Delete Composer package metadata */ public async deleteComposerPackageMetadata(vendorPackage: string): Promise { const path = this.getComposerMetadataPath(vendorPackage); return this.deleteObject(path); } /** * Delete Composer package ZIP */ public async deleteComposerPackageZip(vendorPackage: string, reference: string): Promise { const path = this.getComposerZipPath(vendorPackage, reference); return this.deleteObject(path); } /** * List all Composer packages */ public async listComposerPackages(): Promise { const prefix = 'composer/packages/'; const objects = await this.listObjects(prefix); const packages = new Set(); // 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`; } }