feat(core/registrystorage): Persist OCI manifest content-type in sidecar and normalize manifest body handling

This commit is contained in:
2025-11-25 22:10:06 +00:00
parent 67188a4e9f
commit 41405eb40a
5 changed files with 511 additions and 22 deletions

View File

@@ -198,7 +198,7 @@ export class OciRegistry extends BaseRegistry {
const digest = query.digest;
if (digest && body) {
// Monolithic upload: complete upload in single POST
const blobData = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
const blobData = this.toBuffer(body);
// Verify digest
const calculatedDigest = await this.calculateDigest(blobData);
@@ -320,10 +320,17 @@ export class OciRegistry extends BaseRegistry {
};
}
// Get stored content type, falling back to detecting from manifest content
let contentType = await this.storage.getOciManifestContentType(repository, digest);
if (!contentType) {
// Fallback: detect content type from manifest content
contentType = this.detectManifestContentType(manifestData);
}
return {
status: 200,
headers: {
'Content-Type': 'application/vnd.oci.image.manifest.v1+json',
'Content-Type': contentType,
'Docker-Content-Digest': digest,
},
body: manifestData,
@@ -356,10 +363,18 @@ export class OciRegistry extends BaseRegistry {
const manifestData = await this.storage.getOciManifest(repository, digest);
// Get stored content type, falling back to detecting from manifest content
let contentType = await this.storage.getOciManifestContentType(repository, digest);
if (!contentType && manifestData) {
// Fallback: detect content type from manifest content
contentType = this.detectManifestContentType(manifestData);
}
contentType = contentType || 'application/vnd.oci.image.manifest.v1+json';
return {
status: 200,
headers: {
'Content-Type': 'application/vnd.oci.image.manifest.v1+json',
'Content-Type': contentType,
'Docker-Content-Digest': digest,
'Content-Length': manifestData ? manifestData.length.toString() : '0',
},
@@ -388,16 +403,7 @@ export class OciRegistry extends BaseRegistry {
// Preserve raw bytes for accurate digest calculation
// Per OCI spec, digest must match the exact bytes sent by client
let manifestData: Buffer;
if (Buffer.isBuffer(body)) {
manifestData = body;
} else if (typeof body === 'string') {
// String body - convert directly without JSON transformation
manifestData = Buffer.from(body, 'utf-8');
} else {
// Body was already parsed as JSON object - re-serialize as fallback
manifestData = Buffer.from(JSON.stringify(body));
}
const manifestData = this.toBuffer(body);
const contentType = headers?.['content-type'] || headers?.['Content-Type'] || 'application/vnd.oci.image.manifest.v1+json';
// Calculate manifest digest
@@ -525,7 +531,7 @@ export class OciRegistry extends BaseRegistry {
private async uploadChunk(
uploadId: string,
data: Buffer,
data: Buffer | Uint8Array | unknown,
contentRange: string
): Promise<IResponse> {
const session = this.uploadSessions.get(uploadId);
@@ -537,8 +543,9 @@ export class OciRegistry extends BaseRegistry {
};
}
session.chunks.push(data);
session.totalSize += data.length;
const chunkData = this.toBuffer(data);
session.chunks.push(chunkData);
session.totalSize += chunkData.length;
session.lastActivity = new Date();
return {
@@ -555,7 +562,7 @@ export class OciRegistry extends BaseRegistry {
private async completeUpload(
uploadId: string,
digest: string,
finalData?: Buffer
finalData?: Buffer | Uint8Array | unknown
): Promise<IResponse> {
const session = this.uploadSessions.get(uploadId);
if (!session) {
@@ -567,7 +574,7 @@ export class OciRegistry extends BaseRegistry {
}
const chunks = [...session.chunks];
if (finalData) chunks.push(finalData);
if (finalData) chunks.push(this.toBuffer(finalData));
const blobData = Buffer.concat(chunks);
// Verify digest
@@ -665,6 +672,59 @@ export class OciRegistry extends BaseRegistry {
// HELPER METHODS
// ========================================================================
/**
* Detect manifest content type from manifest content.
* OCI Image Index has "manifests" array, OCI Image Manifest has "config" object.
* Also checks the mediaType field if present.
*/
private detectManifestContentType(manifestData: Buffer): string {
try {
const manifest = JSON.parse(manifestData.toString('utf-8'));
// First check if manifest has explicit mediaType field
if (manifest.mediaType) {
return manifest.mediaType;
}
// Otherwise detect from structure
if (Array.isArray(manifest.manifests)) {
// OCI Image Index (multi-arch manifest list)
return 'application/vnd.oci.image.index.v1+json';
} else if (manifest.config) {
// OCI Image Manifest
return 'application/vnd.oci.image.manifest.v1+json';
}
// Fallback to standard manifest type
return 'application/vnd.oci.image.manifest.v1+json';
} catch (e) {
// If parsing fails, return default
return 'application/vnd.oci.image.manifest.v1+json';
}
}
/**
* Convert any binary-like data to Buffer.
* Handles Buffer, Uint8Array (modern cross-platform), string, and objects.
*
* Note: Buffer.isBuffer(Uint8Array) returns false even though Buffer extends Uint8Array.
* This is because Uint8Array is the modern, cross-platform standard while Buffer is Node.js-specific.
* Many HTTP frameworks pass request bodies as Uint8Array for better compatibility.
*/
private toBuffer(data: unknown): Buffer {
if (Buffer.isBuffer(data)) {
return data;
}
if (data instanceof Uint8Array) {
return Buffer.from(data);
}
if (typeof data === 'string') {
return Buffer.from(data, 'utf-8');
}
// Fallback: serialize object to JSON (may cause digest mismatch for manifests)
return Buffer.from(JSON.stringify(data));
}
private async getTagsData(repository: string): Promise<Record<string, string>> {
const path = `oci/tags/${repository}/tags.json`;
const data = await this.storage.getObject(path);