feat(core/registrystorage): Persist OCI manifest content-type in sidecar and normalize manifest body handling
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user