feat(core): Add PyPI and RubyGems protocol support, Cargo token management, and storage helpers

This commit is contained in:
2025-11-21 14:23:18 +00:00
parent fb8d6897e3
commit 9ca1e670ef
12 changed files with 2414 additions and 6 deletions

View File

@@ -601,4 +601,231 @@ export class RegistryStorage implements IStorageBackend {
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}`;
}
}