1062 lines
32 KiB
TypeScript
1062 lines
32 KiB
TypeScript
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<void> {
|
|
// Pass config as IS3Descriptor to SmartBucket (bucketName is extra, SmartBucket ignores it)
|
|
this.smartBucket = new plugins.smartbucket.SmartBucket(this.config as plugins.tsclass.storage.IS3Descriptor);
|
|
|
|
// 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> {
|
|
// 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<void> {
|
|
await this.bucket.fastRemove({ path: key });
|
|
}
|
|
|
|
/**
|
|
* List objects with a prefix (recursively)
|
|
*/
|
|
public async listObjects(prefix: string): Promise<string[]> {
|
|
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<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`;
|
|
}
|
|
|
|
// ========================================================================
|
|
// PYPI STORAGE METHODS
|
|
// ========================================================================
|
|
|
|
/**
|
|
* Get PyPI package metadata
|
|
*/
|
|
public async getPypiPackageMetadata(packageName: string): Promise<any | null> {
|
|
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<void> {
|
|
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<boolean> {
|
|
const path = this.getPypiMetadataPath(packageName);
|
|
return this.objectExists(path);
|
|
}
|
|
|
|
/**
|
|
* Delete PyPI package metadata
|
|
*/
|
|
public async deletePypiPackageMetadata(packageName: string): Promise<void> {
|
|
const path = this.getPypiMetadataPath(packageName);
|
|
return this.deleteObject(path);
|
|
}
|
|
|
|
/**
|
|
* Get PyPI Simple API index (HTML)
|
|
*/
|
|
public async getPypiSimpleIndex(packageName: string): Promise<string | null> {
|
|
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<void> {
|
|
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<string | null> {
|
|
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<void> {
|
|
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<Buffer | null> {
|
|
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<void> {
|
|
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<boolean> {
|
|
const path = this.getPypiPackageFilePath(packageName, filename);
|
|
return this.objectExists(path);
|
|
}
|
|
|
|
/**
|
|
* Delete PyPI package file
|
|
*/
|
|
public async deletePypiPackageFile(packageName: string, filename: string): Promise<void> {
|
|
const path = this.getPypiPackageFilePath(packageName, filename);
|
|
return this.deleteObject(path);
|
|
}
|
|
|
|
/**
|
|
* List all PyPI packages
|
|
*/
|
|
public async listPypiPackages(): Promise<string[]> {
|
|
const prefix = 'pypi/metadata/';
|
|
const objects = await this.listObjects(prefix);
|
|
const packages = new Set<string>();
|
|
|
|
// 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<string[]> {
|
|
const prefix = `pypi/packages/${packageName}/`;
|
|
const objects = await this.listObjects(prefix);
|
|
const versions = new Set<string>();
|
|
|
|
// 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<void> {
|
|
// 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<void> {
|
|
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}`;
|
|
}
|
|
|
|
// ========================================================================
|
|
// RUBYGEMS STORAGE METHODS
|
|
// ========================================================================
|
|
|
|
/**
|
|
* Get RubyGems versions file (compact index)
|
|
*/
|
|
public async getRubyGemsVersions(): Promise<string | null> {
|
|
const path = this.getRubyGemsVersionsPath();
|
|
const data = await this.getObject(path);
|
|
return data ? data.toString('utf-8') : null;
|
|
}
|
|
|
|
/**
|
|
* Store RubyGems versions file (compact index)
|
|
*/
|
|
public async putRubyGemsVersions(content: string): Promise<void> {
|
|
const path = this.getRubyGemsVersionsPath();
|
|
const data = Buffer.from(content, 'utf-8');
|
|
return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
}
|
|
|
|
/**
|
|
* Get RubyGems info file for a gem (compact index)
|
|
*/
|
|
public async getRubyGemsInfo(gemName: string): Promise<string | null> {
|
|
const path = this.getRubyGemsInfoPath(gemName);
|
|
const data = await this.getObject(path);
|
|
return data ? data.toString('utf-8') : null;
|
|
}
|
|
|
|
/**
|
|
* Store RubyGems info file for a gem (compact index)
|
|
*/
|
|
public async putRubyGemsInfo(gemName: string, content: string): Promise<void> {
|
|
const path = this.getRubyGemsInfoPath(gemName);
|
|
const data = Buffer.from(content, 'utf-8');
|
|
return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
}
|
|
|
|
/**
|
|
* Get RubyGems names file
|
|
*/
|
|
public async getRubyGemsNames(): Promise<string | null> {
|
|
const path = this.getRubyGemsNamesPath();
|
|
const data = await this.getObject(path);
|
|
return data ? data.toString('utf-8') : null;
|
|
}
|
|
|
|
/**
|
|
* Store RubyGems names file
|
|
*/
|
|
public async putRubyGemsNames(content: string): Promise<void> {
|
|
const path = this.getRubyGemsNamesPath();
|
|
const data = Buffer.from(content, 'utf-8');
|
|
return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
}
|
|
|
|
/**
|
|
* Get RubyGems .gem file
|
|
*/
|
|
public async getRubyGemsGem(gemName: string, version: string, platform?: string): Promise<Buffer | null> {
|
|
const path = this.getRubyGemsGemPath(gemName, version, platform);
|
|
return this.getObject(path);
|
|
}
|
|
|
|
/**
|
|
* Store RubyGems .gem file
|
|
*/
|
|
public async putRubyGemsGem(
|
|
gemName: string,
|
|
version: string,
|
|
data: Buffer,
|
|
platform?: string
|
|
): Promise<void> {
|
|
const path = this.getRubyGemsGemPath(gemName, version, platform);
|
|
return this.putObject(path, data, { 'Content-Type': 'application/octet-stream' });
|
|
}
|
|
|
|
/**
|
|
* Check if RubyGems .gem file exists
|
|
*/
|
|
public async rubyGemsGemExists(gemName: string, version: string, platform?: string): Promise<boolean> {
|
|
const path = this.getRubyGemsGemPath(gemName, version, platform);
|
|
return this.objectExists(path);
|
|
}
|
|
|
|
/**
|
|
* Delete RubyGems .gem file
|
|
*/
|
|
public async deleteRubyGemsGem(gemName: string, version: string, platform?: string): Promise<void> {
|
|
const path = this.getRubyGemsGemPath(gemName, version, platform);
|
|
return this.deleteObject(path);
|
|
}
|
|
|
|
/**
|
|
* Get RubyGems metadata
|
|
*/
|
|
public async getRubyGemsMetadata(gemName: string): Promise<any | null> {
|
|
const path = this.getRubyGemsMetadataPath(gemName);
|
|
const data = await this.getObject(path);
|
|
return data ? JSON.parse(data.toString('utf-8')) : null;
|
|
}
|
|
|
|
/**
|
|
* Store RubyGems metadata
|
|
*/
|
|
public async putRubyGemsMetadata(gemName: string, metadata: any): Promise<void> {
|
|
const path = this.getRubyGemsMetadataPath(gemName);
|
|
const data = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
|
|
return this.putObject(path, data, { 'Content-Type': 'application/json' });
|
|
}
|
|
|
|
/**
|
|
* Check if RubyGems metadata exists
|
|
*/
|
|
public async rubyGemsMetadataExists(gemName: string): Promise<boolean> {
|
|
const path = this.getRubyGemsMetadataPath(gemName);
|
|
return this.objectExists(path);
|
|
}
|
|
|
|
/**
|
|
* Delete RubyGems metadata
|
|
*/
|
|
public async deleteRubyGemsMetadata(gemName: string): Promise<void> {
|
|
const path = this.getRubyGemsMetadataPath(gemName);
|
|
return this.deleteObject(path);
|
|
}
|
|
|
|
/**
|
|
* List all RubyGems
|
|
*/
|
|
public async listRubyGems(): Promise<string[]> {
|
|
const prefix = 'rubygems/metadata/';
|
|
const objects = await this.listObjects(prefix);
|
|
const gems = new Set<string>();
|
|
|
|
// Extract gem names from paths like: rubygems/metadata/gem-name/metadata.json
|
|
for (const obj of objects) {
|
|
const match = obj.match(/^rubygems\/metadata\/([^\/]+)\/metadata\.json$/);
|
|
if (match) {
|
|
gems.add(match[1]);
|
|
}
|
|
}
|
|
|
|
return Array.from(gems).sort();
|
|
}
|
|
|
|
/**
|
|
* List all versions of a RubyGem
|
|
*/
|
|
public async listRubyGemsVersions(gemName: string): Promise<string[]> {
|
|
const prefix = `rubygems/gems/`;
|
|
const objects = await this.listObjects(prefix);
|
|
const versions = new Set<string>();
|
|
|
|
// Extract versions from filenames: gem-name-version[-platform].gem
|
|
const gemPrefix = `${gemName}-`;
|
|
for (const obj of objects) {
|
|
const filename = obj.split('/').pop();
|
|
if (!filename || !filename.startsWith(gemPrefix) || !filename.endsWith('.gem')) continue;
|
|
|
|
// Remove gem name prefix and .gem suffix
|
|
const versionPart = filename.substring(gemPrefix.length, filename.length - 4);
|
|
|
|
// Split on last hyphen to separate version from platform
|
|
const lastHyphen = versionPart.lastIndexOf('-');
|
|
const version = lastHyphen > 0 ? versionPart.substring(0, lastHyphen) : versionPart;
|
|
|
|
versions.add(version);
|
|
}
|
|
|
|
return Array.from(versions).sort();
|
|
}
|
|
|
|
/**
|
|
* Delete entire RubyGem (all versions and files)
|
|
*/
|
|
public async deleteRubyGem(gemName: string): Promise<void> {
|
|
// Delete metadata
|
|
await this.deleteRubyGemsMetadata(gemName);
|
|
|
|
// Delete all gem files
|
|
const prefix = `rubygems/gems/`;
|
|
const objects = await this.listObjects(prefix);
|
|
const gemPrefix = `${gemName}-`;
|
|
|
|
for (const obj of objects) {
|
|
const filename = obj.split('/').pop();
|
|
if (filename && filename.startsWith(gemPrefix) && filename.endsWith('.gem')) {
|
|
await this.deleteObject(obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete specific version of a RubyGem
|
|
*/
|
|
public async deleteRubyGemsVersion(gemName: string, version: string, platform?: string): Promise<void> {
|
|
// Delete gem file
|
|
await this.deleteRubyGemsGem(gemName, version, platform);
|
|
|
|
// Update metadata to remove this version
|
|
const metadata = await this.getRubyGemsMetadata(gemName);
|
|
if (metadata && metadata.versions) {
|
|
const versionKey = platform ? `${version}-${platform}` : version;
|
|
delete metadata.versions[versionKey];
|
|
await this.putRubyGemsMetadata(gemName, metadata);
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// RUBYGEMS PATH HELPERS
|
|
// ========================================================================
|
|
|
|
private getRubyGemsVersionsPath(): string {
|
|
return 'rubygems/versions';
|
|
}
|
|
|
|
private getRubyGemsInfoPath(gemName: string): string {
|
|
return `rubygems/info/${gemName}`;
|
|
}
|
|
|
|
private getRubyGemsNamesPath(): string {
|
|
return 'rubygems/names';
|
|
}
|
|
|
|
private getRubyGemsGemPath(gemName: string, version: string, platform?: string): string {
|
|
const filename = platform ? `${gemName}-${version}-${platform}.gem` : `${gemName}-${version}.gem`;
|
|
return `rubygems/gems/${filename}`;
|
|
}
|
|
|
|
private getRubyGemsMetadataPath(gemName: string): string {
|
|
return `rubygems/metadata/${gemName}/metadata.json`;
|
|
}
|
|
}
|