feat(core,storage,oci,registry-config): add streaming response support and configurable registry URLs across protocols
This commit is contained in:
@@ -34,8 +34,8 @@ import type {
|
||||
* ```
|
||||
*/
|
||||
export class RegistryStorage implements IStorageBackend {
|
||||
private smartBucket: plugins.smartbucket.SmartBucket;
|
||||
private bucket: plugins.smartbucket.Bucket;
|
||||
private smartBucket!: plugins.smartbucket.SmartBucket;
|
||||
private bucket!: plugins.smartbucket.Bucket;
|
||||
private bucketName: string;
|
||||
private hooks?: IStorageHooks;
|
||||
|
||||
@@ -1266,4 +1266,135 @@ export class RegistryStorage implements IStorageBackend {
|
||||
private getRubyGemsMetadataPath(gemName: string): string {
|
||||
return `rubygems/metadata/${gemName}/metadata.json`;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// STREAMING METHODS (Web Streams API)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get an object as a ReadableStream. Returns null if not found.
|
||||
*/
|
||||
public async getObjectStream(key: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
try {
|
||||
const stat = await this.bucket.fastStat({ path: key });
|
||||
const size = stat.ContentLength ?? 0;
|
||||
const stream = await this.bucket.fastGetStream({ path: key }, 'webstream');
|
||||
|
||||
// Call afterGet hook (non-blocking)
|
||||
if (this.hooks?.afterGet) {
|
||||
const context = this.currentContext;
|
||||
if (context) {
|
||||
this.hooks.afterGet({
|
||||
operation: 'get',
|
||||
key,
|
||||
protocol: context.protocol,
|
||||
actor: context.actor,
|
||||
metadata: context.metadata,
|
||||
timestamp: new Date(),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return { stream: stream as ReadableStream<Uint8Array>, size };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an object from a ReadableStream.
|
||||
*/
|
||||
public async putObjectStream(key: string, stream: ReadableStream<Uint8Array>): Promise<void> {
|
||||
if (this.hooks?.beforePut) {
|
||||
const context = this.currentContext;
|
||||
if (context) {
|
||||
const hookContext: IStorageHookContext = {
|
||||
operation: 'put',
|
||||
key,
|
||||
protocol: context.protocol,
|
||||
actor: context.actor,
|
||||
metadata: context.metadata,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const result = await this.hooks.beforePut(hookContext);
|
||||
if (!result.allowed) {
|
||||
throw new Error(result.reason || 'Storage operation denied by hook');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert WebStream to Node Readable at the S3 SDK boundary
|
||||
// AWS SDK v3 PutObjectCommand requires a Node.js Readable (not WebStream)
|
||||
const { Readable } = await import('stream');
|
||||
const nodeStream = Readable.fromWeb(stream as any);
|
||||
await this.bucket.fastPutStream({
|
||||
path: key,
|
||||
readableStream: nodeStream,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
if (this.hooks?.afterPut) {
|
||||
const context = this.currentContext;
|
||||
if (context) {
|
||||
this.hooks.afterPut({
|
||||
operation: 'put',
|
||||
key,
|
||||
protocol: context.protocol,
|
||||
actor: context.actor,
|
||||
metadata: context.metadata,
|
||||
timestamp: new Date(),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object size without reading data (S3 HEAD request).
|
||||
*/
|
||||
public async getObjectSize(key: string): Promise<number | null> {
|
||||
try {
|
||||
const stat = await this.bucket.fastStat({ path: key });
|
||||
return stat.ContentLength ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Protocol-specific streaming wrappers ----
|
||||
|
||||
public async getOciBlobStream(digest: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getOciBlobPath(digest));
|
||||
}
|
||||
|
||||
public async putOciBlobStream(digest: string, stream: ReadableStream<Uint8Array>): Promise<void> {
|
||||
return this.putObjectStream(this.getOciBlobPath(digest), stream);
|
||||
}
|
||||
|
||||
public async getOciBlobSize(digest: string): Promise<number | null> {
|
||||
return this.getObjectSize(this.getOciBlobPath(digest));
|
||||
}
|
||||
|
||||
public async getNpmTarballStream(packageName: string, version: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getNpmTarballPath(packageName, version));
|
||||
}
|
||||
|
||||
public async getMavenArtifactStream(groupId: string, artifactId: string, version: string, filename: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getMavenArtifactPath(groupId, artifactId, version, filename));
|
||||
}
|
||||
|
||||
public async getCargoCrateStream(crateName: string, version: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getCargoCratePath(crateName, version));
|
||||
}
|
||||
|
||||
public async getComposerPackageZipStream(vendorPackage: string, reference: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getComposerZipPath(vendorPackage, reference));
|
||||
}
|
||||
|
||||
public async getPypiPackageFileStream(packageName: string, filename: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getPypiPackageFilePath(packageName, filename));
|
||||
}
|
||||
|
||||
public async getRubyGemsGemStream(gemName: string, version: string, platform?: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
|
||||
return this.getObjectStream(this.getRubyGemsGemPath(gemName, version, platform));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user