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); } /** * Delete Maven metadata (maven-metadata.xml) */ public async deleteMavenMetadata( groupId: string, artifactId: string ): Promise { 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 { 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`; } // ======================================================================== // PYPI STORAGE METHODS // ======================================================================== /** * Get PyPI package metadata */ public async getPypiPackageMetadata(packageName: string): Promise { const path = this.getPypiMetadataPath(packageName); const data = await this.getObject(path); return data ? JSON.parse(data.toString('utf-8')) : null; } /** * Store PyPI package metadata */ public async putPypiPackageMetadata(packageName: string, metadata: any): Promise { const path = this.getPypiMetadataPath(packageName); const data = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8'); return this.putObject(path, data, { 'Content-Type': 'application/json' }); } /** * Check if PyPI package metadata exists */ public async pypiPackageMetadataExists(packageName: string): Promise { const path = this.getPypiMetadataPath(packageName); return this.objectExists(path); } /** * Delete PyPI package metadata */ public async deletePypiPackageMetadata(packageName: string): Promise { const path = this.getPypiMetadataPath(packageName); return this.deleteObject(path); } /** * Get PyPI Simple API index (HTML) */ public async getPypiSimpleIndex(packageName: string): Promise { const path = this.getPypiSimpleIndexPath(packageName); const data = await this.getObject(path); return data ? data.toString('utf-8') : null; } /** * Store PyPI Simple API index (HTML) */ public async putPypiSimpleIndex(packageName: string, html: string): Promise { const path = this.getPypiSimpleIndexPath(packageName); const data = Buffer.from(html, 'utf-8'); return this.putObject(path, data, { 'Content-Type': 'text/html; charset=utf-8' }); } /** * Get PyPI root Simple API index (HTML) */ public async getPypiSimpleRootIndex(): Promise { const path = this.getPypiSimpleRootIndexPath(); const data = await this.getObject(path); return data ? data.toString('utf-8') : null; } /** * Store PyPI root Simple API index (HTML) */ public async putPypiSimpleRootIndex(html: string): Promise { const path = this.getPypiSimpleRootIndexPath(); const data = Buffer.from(html, 'utf-8'); return this.putObject(path, data, { 'Content-Type': 'text/html; charset=utf-8' }); } /** * Get PyPI package file (wheel, sdist) */ public async getPypiPackageFile(packageName: string, filename: string): Promise { const path = this.getPypiPackageFilePath(packageName, filename); return this.getObject(path); } /** * Store PyPI package file (wheel, sdist) */ public async putPypiPackageFile( packageName: string, filename: string, data: Buffer ): Promise { const path = this.getPypiPackageFilePath(packageName, filename); return this.putObject(path, data, { 'Content-Type': 'application/octet-stream' }); } /** * Check if PyPI package file exists */ public async pypiPackageFileExists(packageName: string, filename: string): Promise { const path = this.getPypiPackageFilePath(packageName, filename); return this.objectExists(path); } /** * Delete PyPI package file */ public async deletePypiPackageFile(packageName: string, filename: string): Promise { const path = this.getPypiPackageFilePath(packageName, filename); return this.deleteObject(path); } /** * List all PyPI packages */ public async listPypiPackages(): Promise { const prefix = 'pypi/metadata/'; const objects = await this.listObjects(prefix); const packages = new Set(); // Extract package names from paths like: pypi/metadata/package-name/metadata.json for (const obj of objects) { const match = obj.match(/^pypi\/metadata\/([^\/]+)\/metadata\.json$/); if (match) { packages.add(match[1]); } } return Array.from(packages).sort(); } /** * List all versions of a PyPI package */ public async listPypiPackageVersions(packageName: string): Promise { const prefix = `pypi/packages/${packageName}/`; const objects = await this.listObjects(prefix); const versions = new Set(); // Extract versions from filenames for (const obj of objects) { const filename = obj.split('/').pop(); if (!filename) continue; // Extract version from wheel filename: package-1.0.0-py3-none-any.whl // or sdist filename: package-1.0.0.tar.gz const wheelMatch = filename.match(/^[^-]+-([^-]+)-.*\.whl$/); const sdistMatch = filename.match(/^[^-]+-([^.]+)\.(tar\.gz|zip)$/); if (wheelMatch) versions.add(wheelMatch[1]); else if (sdistMatch) versions.add(sdistMatch[1]); } return Array.from(versions).sort(); } /** * Delete entire PyPI package (all versions and files) */ public async deletePypiPackage(packageName: string): Promise { // Delete metadata await this.deletePypiPackageMetadata(packageName); // Delete Simple API index const simpleIndexPath = this.getPypiSimpleIndexPath(packageName); try { await this.deleteObject(simpleIndexPath); } catch (error) { // Ignore if doesn't exist } // Delete all package files const prefix = `pypi/packages/${packageName}/`; const objects = await this.listObjects(prefix); for (const obj of objects) { await this.deleteObject(obj); } } /** * Delete specific version of a PyPI package */ public async deletePypiPackageVersion(packageName: string, version: string): Promise { const prefix = `pypi/packages/${packageName}/`; const objects = await this.listObjects(prefix); // Delete all files matching this version for (const obj of objects) { const filename = obj.split('/').pop(); if (!filename) continue; // Check if filename contains this version const wheelMatch = filename.match(/^[^-]+-([^-]+)-.*\.whl$/); const sdistMatch = filename.match(/^[^-]+-([^.]+)\.(tar\.gz|zip)$/); const fileVersion = wheelMatch?.[1] || sdistMatch?.[1]; if (fileVersion === version) { await this.deleteObject(obj); } } // Update metadata to remove this version const metadata = await this.getPypiPackageMetadata(packageName); if (metadata && metadata.versions) { delete metadata.versions[version]; await this.putPypiPackageMetadata(packageName, metadata); } } // ======================================================================== // PYPI PATH HELPERS // ======================================================================== private getPypiMetadataPath(packageName: string): string { return `pypi/metadata/${packageName}/metadata.json`; } private getPypiSimpleIndexPath(packageName: string): string { return `pypi/simple/${packageName}/index.html`; } private getPypiSimpleRootIndexPath(): string { return `pypi/simple/index.html`; } private getPypiPackageFilePath(packageName: string, filename: string): string { return `pypi/packages/${packageName}/${filename}`; } }