feat(maven): Add Maven registry protocol support (storage, auth, routing, interfaces, and exports)
This commit is contained in:
596
ts/maven/classes.mavenregistry.ts
Normal file
596
ts/maven/classes.mavenregistry.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
/**
|
||||
* Maven Registry Implementation
|
||||
* Implements Maven repository protocol for Java artifacts
|
||||
*/
|
||||
|
||||
import { BaseRegistry } from '../core/classes.baseregistry.js';
|
||||
import type { RegistryStorage } from '../core/classes.registrystorage.js';
|
||||
import type { AuthManager } from '../core/classes.authmanager.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
||||
import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
|
||||
import {
|
||||
pathToGAV,
|
||||
buildFilename,
|
||||
calculateChecksums,
|
||||
generateMetadataXml,
|
||||
parseMetadataXml,
|
||||
formatMavenTimestamp,
|
||||
isSnapshot,
|
||||
validatePom,
|
||||
extractGAVFromPom,
|
||||
gavToPath,
|
||||
} from './helpers.maven.js';
|
||||
|
||||
/**
|
||||
* Maven Registry class
|
||||
* Handles Maven repository HTTP protocol
|
||||
*/
|
||||
export class MavenRegistry extends BaseRegistry {
|
||||
private storage: RegistryStorage;
|
||||
private authManager: AuthManager;
|
||||
private basePath: string = '/maven';
|
||||
private registryUrl: string;
|
||||
|
||||
constructor(
|
||||
storage: RegistryStorage,
|
||||
authManager: AuthManager,
|
||||
basePath: string,
|
||||
registryUrl: string
|
||||
) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.authManager = authManager;
|
||||
this.basePath = basePath;
|
||||
this.registryUrl = registryUrl;
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
// No special initialization needed for Maven
|
||||
}
|
||||
|
||||
public getBasePath(): string {
|
||||
return this.basePath;
|
||||
}
|
||||
|
||||
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
||||
// Remove base path from URL
|
||||
const path = context.path.replace(this.basePath, '');
|
||||
|
||||
// Extract token from Authorization header
|
||||
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
|
||||
let token: IAuthToken | null = null;
|
||||
|
||||
if (authHeader) {
|
||||
const tokenString = authHeader.replace(/^(Bearer|Basic)\s+/i, '');
|
||||
// For now, try to validate as Maven token (reuse npm token type)
|
||||
token = await this.authManager.validateToken(tokenString, 'maven');
|
||||
}
|
||||
|
||||
// Parse path to determine request type
|
||||
const coordinate = pathToGAV(path);
|
||||
|
||||
if (!coordinate) {
|
||||
// Not a valid artifact path, could be metadata or root
|
||||
if (path.endsWith('/maven-metadata.xml')) {
|
||||
return this.handleMetadataRequest(context.method, path, token);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { error: 'NOT_FOUND', message: 'Invalid Maven path' },
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a checksum file
|
||||
if (coordinate.extension === 'md5' || coordinate.extension === 'sha1' ||
|
||||
coordinate.extension === 'sha256' || coordinate.extension === 'sha512') {
|
||||
return this.handleChecksumRequest(context.method, coordinate, token);
|
||||
}
|
||||
|
||||
// Handle artifact requests (JAR, POM, WAR, etc.)
|
||||
return this.handleArtifactRequest(context.method, coordinate, token, context.body);
|
||||
}
|
||||
|
||||
protected async checkPermission(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
if (!token) return false;
|
||||
return this.authManager.authorize(token, `maven:artifact:${resource}`, action);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// REQUEST HANDLERS
|
||||
// ========================================================================
|
||||
|
||||
private async handleArtifactRequest(
|
||||
method: string,
|
||||
coordinate: IMavenCoordinate,
|
||||
token: IAuthToken | null,
|
||||
body?: Buffer | any
|
||||
): Promise<IResponse> {
|
||||
const { groupId, artifactId, version } = coordinate;
|
||||
const filename = buildFilename(coordinate);
|
||||
const resource = `${groupId}:${artifactId}`;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
// Read permission required
|
||||
if (!await this.checkPermission(token, resource, 'read')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
|
||||
},
|
||||
body: { error: 'UNAUTHORIZED', message: 'Authentication required' },
|
||||
};
|
||||
}
|
||||
|
||||
return method === 'GET'
|
||||
? this.getArtifact(groupId, artifactId, version, filename)
|
||||
: this.headArtifact(groupId, artifactId, version, filename);
|
||||
|
||||
case 'PUT':
|
||||
// Write permission required
|
||||
if (!await this.checkPermission(token, resource, 'write')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
|
||||
},
|
||||
body: { error: 'UNAUTHORIZED', message: 'Write permission required' },
|
||||
};
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: { error: 'BAD_REQUEST', message: 'Request body required' },
|
||||
};
|
||||
}
|
||||
|
||||
return this.putArtifact(groupId, artifactId, version, filename, coordinate, body);
|
||||
|
||||
case 'DELETE':
|
||||
// Delete permission required
|
||||
if (!await this.checkPermission(token, resource, 'delete')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
|
||||
},
|
||||
body: { error: 'UNAUTHORIZED', message: 'Delete permission required' },
|
||||
};
|
||||
}
|
||||
|
||||
return this.deleteArtifact(groupId, artifactId, version, filename);
|
||||
|
||||
default:
|
||||
return {
|
||||
status: 405,
|
||||
headers: { 'Allow': 'GET, HEAD, PUT, DELETE' },
|
||||
body: { error: 'METHOD_NOT_ALLOWED', message: 'Method not allowed' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async handleChecksumRequest(
|
||||
method: string,
|
||||
coordinate: IMavenCoordinate,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
const { groupId, artifactId, version, extension } = coordinate;
|
||||
const resource = `${groupId}:${artifactId}`;
|
||||
|
||||
// Checksums follow the same permissions as their artifacts
|
||||
if (method === 'GET' || method === 'HEAD') {
|
||||
if (!await this.checkPermission(token, resource, 'read')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
|
||||
},
|
||||
body: { error: 'UNAUTHORIZED', message: 'Authentication required' },
|
||||
};
|
||||
}
|
||||
|
||||
return this.getChecksum(groupId, artifactId, version, coordinate);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 405,
|
||||
headers: { 'Allow': 'GET, HEAD' },
|
||||
body: { error: 'METHOD_NOT_ALLOWED', message: 'Checksums are auto-generated' },
|
||||
};
|
||||
}
|
||||
|
||||
private async handleMetadataRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
// Parse path to extract groupId and artifactId
|
||||
// Path format: /com/example/my-lib/maven-metadata.xml
|
||||
const parts = path.split('/').filter(p => p && p !== 'maven-metadata.xml');
|
||||
|
||||
if (parts.length < 2) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: { error: 'BAD_REQUEST', message: 'Invalid metadata path' },
|
||||
};
|
||||
}
|
||||
|
||||
const artifactId = parts[parts.length - 1];
|
||||
const groupId = parts.slice(0, -1).join('.');
|
||||
const resource = `${groupId}:${artifactId}`;
|
||||
|
||||
if (method === 'GET') {
|
||||
// Metadata is usually public (read permission optional)
|
||||
// Some registries allow anonymous metadata access
|
||||
return this.getMetadata(groupId, artifactId);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 405,
|
||||
headers: { 'Allow': 'GET' },
|
||||
body: { error: 'METHOD_NOT_ALLOWED', message: 'Metadata is auto-generated' },
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ARTIFACT OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
private async getArtifact(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string
|
||||
): Promise<IResponse> {
|
||||
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: { error: 'NOT_FOUND', message: 'Artifact not found' },
|
||||
};
|
||||
}
|
||||
|
||||
// Determine content type based on extension
|
||||
const extension = filename.split('.').pop() || '';
|
||||
const contentType = this.getContentType(extension);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': data.length.toString(),
|
||||
},
|
||||
body: data,
|
||||
};
|
||||
}
|
||||
|
||||
private async headArtifact(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string
|
||||
): Promise<IResponse> {
|
||||
const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
|
||||
|
||||
if (!exists) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Get file size for Content-Length header
|
||||
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
|
||||
const extension = filename.split('.').pop() || '';
|
||||
const contentType = this.getContentType(extension);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': data ? data.length.toString() : '0',
|
||||
},
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async putArtifact(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string,
|
||||
coordinate: IMavenCoordinate,
|
||||
body: Buffer | any
|
||||
): Promise<IResponse> {
|
||||
const data = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
|
||||
|
||||
// Validate POM if uploading .pom file
|
||||
if (coordinate.extension === 'pom') {
|
||||
const pomValid = validatePom(data.toString('utf-8'));
|
||||
if (!pomValid) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: { error: 'INVALID_POM', message: 'Invalid POM file' },
|
||||
};
|
||||
}
|
||||
|
||||
// Verify GAV matches path
|
||||
const pomGAV = extractGAVFromPom(data.toString('utf-8'));
|
||||
if (pomGAV && (pomGAV.groupId !== groupId || pomGAV.artifactId !== artifactId || pomGAV.version !== version)) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: { error: 'GAV_MISMATCH', message: 'POM coordinates do not match upload path' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Store the artifact
|
||||
await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
|
||||
|
||||
// Generate and store checksums
|
||||
const checksums = await calculateChecksums(data);
|
||||
await this.storeChecksums(groupId, artifactId, version, filename, checksums);
|
||||
|
||||
// Update maven-metadata.xml if this is a primary artifact (jar, pom, war)
|
||||
if (['jar', 'pom', 'war', 'ear', 'aar'].includes(coordinate.extension)) {
|
||||
await this.updateMetadata(groupId, artifactId, version);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
headers: {
|
||||
'Location': `${this.registryUrl}/${gavToPath(groupId, artifactId, version)}/${filename}`,
|
||||
},
|
||||
body: { success: true, message: 'Artifact uploaded successfully' },
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteArtifact(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string
|
||||
): Promise<IResponse> {
|
||||
const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
|
||||
|
||||
if (!exists) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: { error: 'NOT_FOUND', message: 'Artifact not found' },
|
||||
};
|
||||
}
|
||||
|
||||
await this.storage.deleteMavenArtifact(groupId, artifactId, version, filename);
|
||||
|
||||
// Also delete checksums
|
||||
for (const ext of ['md5', 'sha1', 'sha256', 'sha512']) {
|
||||
const checksumFile = `${filename}.${ext}`;
|
||||
const checksumExists = await this.storage.mavenArtifactExists(groupId, artifactId, version, checksumFile);
|
||||
if (checksumExists) {
|
||||
await this.storage.deleteMavenArtifact(groupId, artifactId, version, checksumFile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 204,
|
||||
headers: {},
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CHECKSUM OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
private async getChecksum(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
coordinate: IMavenCoordinate
|
||||
): Promise<IResponse> {
|
||||
const checksumFilename = buildFilename(coordinate);
|
||||
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, checksumFilename);
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: { error: 'NOT_FOUND', message: 'Checksum not found' },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': data.length.toString(),
|
||||
},
|
||||
body: data,
|
||||
};
|
||||
}
|
||||
|
||||
private async storeChecksums(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string,
|
||||
checksums: IChecksums
|
||||
): Promise<void> {
|
||||
// Store each checksum as a separate file
|
||||
await this.storage.putMavenArtifact(
|
||||
groupId,
|
||||
artifactId,
|
||||
version,
|
||||
`${filename}.md5`,
|
||||
Buffer.from(checksums.md5, 'utf-8')
|
||||
);
|
||||
|
||||
await this.storage.putMavenArtifact(
|
||||
groupId,
|
||||
artifactId,
|
||||
version,
|
||||
`${filename}.sha1`,
|
||||
Buffer.from(checksums.sha1, 'utf-8')
|
||||
);
|
||||
|
||||
if (checksums.sha256) {
|
||||
await this.storage.putMavenArtifact(
|
||||
groupId,
|
||||
artifactId,
|
||||
version,
|
||||
`${filename}.sha256`,
|
||||
Buffer.from(checksums.sha256, 'utf-8')
|
||||
);
|
||||
}
|
||||
|
||||
if (checksums.sha512) {
|
||||
await this.storage.putMavenArtifact(
|
||||
groupId,
|
||||
artifactId,
|
||||
version,
|
||||
`${filename}.sha512`,
|
||||
Buffer.from(checksums.sha512, 'utf-8')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// METADATA OPERATIONS
|
||||
// ========================================================================
|
||||
|
||||
private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
|
||||
const metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
|
||||
|
||||
if (!metadataBuffer) {
|
||||
// Generate empty metadata if none exists
|
||||
const emptyMetadata: IMavenMetadata = {
|
||||
groupId,
|
||||
artifactId,
|
||||
versioning: {
|
||||
versions: [],
|
||||
lastUpdated: formatMavenTimestamp(new Date()),
|
||||
},
|
||||
};
|
||||
|
||||
const xml = generateMetadataXml(emptyMetadata);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
'Content-Length': xml.length.toString(),
|
||||
},
|
||||
body: Buffer.from(xml, 'utf-8'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
'Content-Length': metadataBuffer.length.toString(),
|
||||
},
|
||||
body: metadataBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateMetadata(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
newVersion: string
|
||||
): Promise<void> {
|
||||
// Get existing metadata or create new
|
||||
const existingBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
|
||||
let metadata: IMavenMetadata;
|
||||
|
||||
if (existingBuffer) {
|
||||
const parsed = parseMetadataXml(existingBuffer.toString('utf-8'));
|
||||
if (parsed) {
|
||||
metadata = parsed;
|
||||
} else {
|
||||
// Create new if parsing failed
|
||||
metadata = {
|
||||
groupId,
|
||||
artifactId,
|
||||
versioning: {
|
||||
versions: [],
|
||||
lastUpdated: formatMavenTimestamp(new Date()),
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
metadata = {
|
||||
groupId,
|
||||
artifactId,
|
||||
versioning: {
|
||||
versions: [],
|
||||
lastUpdated: formatMavenTimestamp(new Date()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Add new version if not already present
|
||||
if (!metadata.versioning.versions.includes(newVersion)) {
|
||||
metadata.versioning.versions.push(newVersion);
|
||||
metadata.versioning.versions.sort(); // Sort versions
|
||||
}
|
||||
|
||||
// Update latest and release
|
||||
const versions = metadata.versioning.versions;
|
||||
metadata.versioning.latest = versions[versions.length - 1];
|
||||
|
||||
// Release is the latest non-SNAPSHOT version
|
||||
const releaseVersions = versions.filter(v => !isSnapshot(v));
|
||||
if (releaseVersions.length > 0) {
|
||||
metadata.versioning.release = releaseVersions[releaseVersions.length - 1];
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
metadata.versioning.lastUpdated = formatMavenTimestamp(new Date());
|
||||
|
||||
// Generate and store XML
|
||||
const xml = generateMetadataXml(metadata);
|
||||
await this.storage.putMavenMetadata(groupId, artifactId, Buffer.from(xml, 'utf-8'));
|
||||
|
||||
// Also store checksums for metadata
|
||||
const checksums = await calculateChecksums(Buffer.from(xml, 'utf-8'));
|
||||
const metadataFilename = 'maven-metadata.xml';
|
||||
await this.storeChecksums(groupId, artifactId, '', metadataFilename, checksums);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// UTILITY METHODS
|
||||
// ========================================================================
|
||||
|
||||
private getContentType(extension: string): string {
|
||||
const contentTypes: Record<string, string> = {
|
||||
'jar': 'application/java-archive',
|
||||
'war': 'application/java-archive',
|
||||
'ear': 'application/java-archive',
|
||||
'aar': 'application/java-archive',
|
||||
'pom': 'application/xml',
|
||||
'xml': 'application/xml',
|
||||
'md5': 'text/plain',
|
||||
'sha1': 'text/plain',
|
||||
'sha256': 'text/plain',
|
||||
'sha512': 'text/plain',
|
||||
};
|
||||
|
||||
return contentTypes[extension] || 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
333
ts/maven/helpers.maven.ts
Normal file
333
ts/maven/helpers.maven.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* Maven helper utilities
|
||||
* Path conversion, XML generation, checksum calculation
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
import type {
|
||||
IMavenCoordinate,
|
||||
IMavenMetadata,
|
||||
IChecksums,
|
||||
IMavenPom,
|
||||
} from './interfaces.maven.js';
|
||||
|
||||
/**
|
||||
* Convert Maven GAV coordinates to storage path
|
||||
* Example: com.example:my-lib:1.0.0 → com/example/my-lib/1.0.0
|
||||
*/
|
||||
export function gavToPath(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version?: string
|
||||
): string {
|
||||
const groupPath = groupId.replace(/\./g, '/');
|
||||
if (version) {
|
||||
return `${groupPath}/${artifactId}/${version}`;
|
||||
}
|
||||
return `${groupPath}/${artifactId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Maven path to GAV coordinates
|
||||
* Example: com/example/my-lib/1.0.0/my-lib-1.0.0.jar → {groupId, artifactId, version, ...}
|
||||
*/
|
||||
export function pathToGAV(path: string): IMavenCoordinate | null {
|
||||
// Remove leading slash if present
|
||||
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
||||
|
||||
// Split path into parts
|
||||
const parts = cleanPath.split('/');
|
||||
if (parts.length < 4) {
|
||||
return null; // Not a valid artifact path
|
||||
}
|
||||
|
||||
// Last part is filename
|
||||
const filename = parts[parts.length - 1];
|
||||
const version = parts[parts.length - 2];
|
||||
const artifactId = parts[parts.length - 3];
|
||||
const groupId = parts.slice(0, -3).join('.');
|
||||
|
||||
// Parse filename to extract classifier and extension
|
||||
const parsed = parseFilename(filename, artifactId, version);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
groupId,
|
||||
artifactId,
|
||||
version,
|
||||
classifier: parsed.classifier,
|
||||
extension: parsed.extension,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Maven artifact filename
|
||||
* Example: my-lib-1.0.0-sources.jar → {classifier: 'sources', extension: 'jar'}
|
||||
*/
|
||||
export function parseFilename(
|
||||
filename: string,
|
||||
artifactId: string,
|
||||
version: string
|
||||
): { classifier?: string; extension: string } | null {
|
||||
// Expected format: {artifactId}-{version}[-{classifier}].{extension}
|
||||
const prefix = `${artifactId}-${version}`;
|
||||
|
||||
if (!filename.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const remainder = filename.substring(prefix.length);
|
||||
|
||||
// Check for classifier
|
||||
const dotIndex = remainder.lastIndexOf('.');
|
||||
if (dotIndex === -1) {
|
||||
return null; // No extension
|
||||
}
|
||||
|
||||
const extension = remainder.substring(dotIndex + 1);
|
||||
const classifierPart = remainder.substring(0, dotIndex);
|
||||
|
||||
if (classifierPart.length === 0) {
|
||||
// No classifier
|
||||
return { extension };
|
||||
}
|
||||
|
||||
if (classifierPart.startsWith('-')) {
|
||||
// Has classifier
|
||||
const classifier = classifierPart.substring(1);
|
||||
return { classifier, extension };
|
||||
}
|
||||
|
||||
return null; // Invalid format
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Maven artifact filename
|
||||
* Example: {artifactId: 'my-lib', version: '1.0.0', classifier: 'sources', extension: 'jar'}
|
||||
* → 'my-lib-1.0.0-sources.jar'
|
||||
*/
|
||||
export function buildFilename(coordinate: IMavenCoordinate): string {
|
||||
const { artifactId, version, classifier, extension } = coordinate;
|
||||
|
||||
let filename = `${artifactId}-${version}`;
|
||||
if (classifier) {
|
||||
filename += `-${classifier}`;
|
||||
}
|
||||
filename += `.${extension}`;
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate checksums for Maven artifact
|
||||
* Returns MD5, SHA-1, SHA-256, SHA-512
|
||||
*/
|
||||
export async function calculateChecksums(data: Buffer): Promise<IChecksums> {
|
||||
const crypto = await import('crypto');
|
||||
|
||||
return {
|
||||
md5: crypto.createHash('md5').update(data).digest('hex'),
|
||||
sha1: crypto.createHash('sha1').update(data).digest('hex'),
|
||||
sha256: crypto.createHash('sha256').update(data).digest('hex'),
|
||||
sha512: crypto.createHash('sha512').update(data).digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate maven-metadata.xml from metadata object
|
||||
*/
|
||||
export function generateMetadataXml(metadata: IMavenMetadata): string {
|
||||
const { groupId, artifactId, versioning } = metadata;
|
||||
const { latest, release, versions, lastUpdated, snapshot, snapshotVersions } = versioning;
|
||||
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += '<metadata>\n';
|
||||
xml += ` <groupId>${escapeXml(groupId)}</groupId>\n`;
|
||||
xml += ` <artifactId>${escapeXml(artifactId)}</artifactId>\n`;
|
||||
|
||||
// Add version if SNAPSHOT
|
||||
if (snapshot) {
|
||||
const snapshotVersion = versions[versions.length - 1]; // Assume last version is the SNAPSHOT
|
||||
xml += ` <version>${escapeXml(snapshotVersion)}</version>\n`;
|
||||
}
|
||||
|
||||
xml += ' <versioning>\n';
|
||||
|
||||
if (latest) {
|
||||
xml += ` <latest>${escapeXml(latest)}</latest>\n`;
|
||||
}
|
||||
|
||||
if (release) {
|
||||
xml += ` <release>${escapeXml(release)}</release>\n`;
|
||||
}
|
||||
|
||||
xml += ' <versions>\n';
|
||||
for (const version of versions) {
|
||||
xml += ` <version>${escapeXml(version)}</version>\n`;
|
||||
}
|
||||
xml += ' </versions>\n';
|
||||
|
||||
xml += ` <lastUpdated>${lastUpdated}</lastUpdated>\n`;
|
||||
|
||||
// Add SNAPSHOT info if present
|
||||
if (snapshot) {
|
||||
xml += ' <snapshot>\n';
|
||||
xml += ` <timestamp>${escapeXml(snapshot.timestamp)}</timestamp>\n`;
|
||||
xml += ` <buildNumber>${snapshot.buildNumber}</buildNumber>\n`;
|
||||
xml += ' </snapshot>\n';
|
||||
}
|
||||
|
||||
// Add SNAPSHOT versions if present
|
||||
if (snapshotVersions && snapshotVersions.length > 0) {
|
||||
xml += ' <snapshotVersions>\n';
|
||||
for (const sv of snapshotVersions) {
|
||||
xml += ' <snapshotVersion>\n';
|
||||
if (sv.classifier) {
|
||||
xml += ` <classifier>${escapeXml(sv.classifier)}</classifier>\n`;
|
||||
}
|
||||
xml += ` <extension>${escapeXml(sv.extension)}</extension>\n`;
|
||||
xml += ` <value>${escapeXml(sv.value)}</value>\n`;
|
||||
xml += ` <updated>${sv.updated}</updated>\n`;
|
||||
xml += ' </snapshotVersion>\n';
|
||||
}
|
||||
xml += ' </snapshotVersions>\n';
|
||||
}
|
||||
|
||||
xml += ' </versioning>\n';
|
||||
xml += '</metadata>\n';
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse maven-metadata.xml to metadata object
|
||||
* Basic XML parsing for Maven metadata
|
||||
*/
|
||||
export function parseMetadataXml(xml: string): IMavenMetadata | null {
|
||||
try {
|
||||
// Simple regex-based parsing (for basic metadata)
|
||||
// In production, use a proper XML parser
|
||||
|
||||
const groupIdMatch = xml.match(/<groupId>([^<]+)<\/groupId>/);
|
||||
const artifactIdMatch = xml.match(/<artifactId>([^<]+)<\/artifactId>/);
|
||||
const latestMatch = xml.match(/<latest>([^<]+)<\/latest>/);
|
||||
const releaseMatch = xml.match(/<release>([^<]+)<\/release>/);
|
||||
const lastUpdatedMatch = xml.match(/<lastUpdated>([^<]+)<\/lastUpdated>/);
|
||||
|
||||
if (!groupIdMatch || !artifactIdMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse versions
|
||||
const versionsMatch = xml.match(/<versions>([\s\S]*?)<\/versions>/);
|
||||
const versions: string[] = [];
|
||||
if (versionsMatch) {
|
||||
const versionMatches = versionsMatch[1].matchAll(/<version>([^<]+)<\/version>/g);
|
||||
for (const match of versionMatches) {
|
||||
versions.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: groupIdMatch[1],
|
||||
artifactId: artifactIdMatch[1],
|
||||
versioning: {
|
||||
latest: latestMatch ? latestMatch[1] : undefined,
|
||||
release: releaseMatch ? releaseMatch[1] : undefined,
|
||||
versions,
|
||||
lastUpdated: lastUpdatedMatch ? lastUpdatedMatch[1] : formatMavenTimestamp(new Date()),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape XML special characters
|
||||
*/
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp in Maven format: yyyyMMddHHmmss
|
||||
*/
|
||||
export function formatMavenTimestamp(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format SNAPSHOT timestamp: yyyyMMdd.HHmmss
|
||||
*/
|
||||
export function formatSnapshotTimestamp(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}.${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if version is a SNAPSHOT
|
||||
*/
|
||||
export function isSnapshot(version: string): boolean {
|
||||
return version.endsWith('-SNAPSHOT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate POM basic structure
|
||||
*/
|
||||
export function validatePom(pomXml: string): boolean {
|
||||
try {
|
||||
// Basic validation - check for required fields
|
||||
return (
|
||||
pomXml.includes('<groupId>') &&
|
||||
pomXml.includes('<artifactId>') &&
|
||||
pomXml.includes('<version>') &&
|
||||
pomXml.includes('<modelVersion>')
|
||||
);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract GAV from POM XML
|
||||
*/
|
||||
export function extractGAVFromPom(pomXml: string): { groupId: string; artifactId: string; version: string } | null {
|
||||
try {
|
||||
const groupIdMatch = pomXml.match(/<groupId>([^<]+)<\/groupId>/);
|
||||
const artifactIdMatch = pomXml.match(/<artifactId>([^<]+)<\/artifactId>/);
|
||||
const versionMatch = pomXml.match(/<version>([^<]+)<\/version>/);
|
||||
|
||||
if (groupIdMatch && artifactIdMatch && versionMatch) {
|
||||
return {
|
||||
groupId: groupIdMatch[1],
|
||||
artifactId: artifactIdMatch[1],
|
||||
version: versionMatch[1],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
7
ts/maven/index.ts
Normal file
7
ts/maven/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Maven Registry module exports
|
||||
*/
|
||||
|
||||
export { MavenRegistry } from './classes.mavenregistry.js';
|
||||
export * from './interfaces.maven.js';
|
||||
export * from './helpers.maven.js';
|
||||
127
ts/maven/interfaces.maven.ts
Normal file
127
ts/maven/interfaces.maven.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Maven registry type definitions
|
||||
* Supports Maven repository protocol for Java artifacts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maven coordinate system (GAV + optional classifier)
|
||||
* Example: com.example:my-library:1.0.0:sources:jar
|
||||
*/
|
||||
export interface IMavenCoordinate {
|
||||
groupId: string; // e.g., "com.example.myapp"
|
||||
artifactId: string; // e.g., "my-library"
|
||||
version: string; // e.g., "1.0.0" or "1.0-SNAPSHOT"
|
||||
classifier?: string; // e.g., "sources", "javadoc"
|
||||
extension: string; // e.g., "jar", "war", "pom"
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven metadata (maven-metadata.xml) structure
|
||||
* Contains version list and latest/release information
|
||||
*/
|
||||
export interface IMavenMetadata {
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
versioning: IMavenVersioning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven versioning information
|
||||
*/
|
||||
export interface IMavenVersioning {
|
||||
latest?: string; // Latest version (including SNAPSHOTs)
|
||||
release?: string; // Latest release version (excluding SNAPSHOTs)
|
||||
versions: string[]; // List of all versions
|
||||
lastUpdated: string; // Format: yyyyMMddHHmmss
|
||||
snapshot?: IMavenSnapshot; // For SNAPSHOT versions
|
||||
snapshotVersions?: IMavenSnapshotVersion[]; // For SNAPSHOT builds
|
||||
}
|
||||
|
||||
/**
|
||||
* SNAPSHOT build information
|
||||
*/
|
||||
export interface IMavenSnapshot {
|
||||
timestamp: string; // Format: yyyyMMdd.HHmmss
|
||||
buildNumber: number; // Incremental build number
|
||||
}
|
||||
|
||||
/**
|
||||
* SNAPSHOT version entry
|
||||
*/
|
||||
export interface IMavenSnapshotVersion {
|
||||
classifier?: string;
|
||||
extension: string;
|
||||
value: string; // Timestamped version
|
||||
updated: string; // Format: yyyyMMddHHmmss
|
||||
}
|
||||
|
||||
/**
|
||||
* Checksums for Maven artifacts
|
||||
* Maven requires separate checksum files for each artifact
|
||||
*/
|
||||
export interface IChecksums {
|
||||
md5: string; // MD5 hash
|
||||
sha1: string; // SHA-1 hash (required)
|
||||
sha256?: string; // SHA-256 hash (optional)
|
||||
sha512?: string; // SHA-512 hash (optional)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven artifact file information
|
||||
*/
|
||||
export interface IMavenArtifactFile {
|
||||
filename: string; // Full filename with extension
|
||||
data: Buffer; // File content
|
||||
coordinate: IMavenCoordinate; // Parsed GAV coordinates
|
||||
checksums?: IChecksums; // Calculated checksums
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven upload request
|
||||
* Contains all files for a single version (JAR, POM, sources, etc.)
|
||||
*/
|
||||
export interface IMavenUploadRequest {
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
version: string;
|
||||
files: IMavenArtifactFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven protocol configuration
|
||||
*/
|
||||
export interface IMavenProtocolConfig {
|
||||
enabled: boolean;
|
||||
basePath: string; // Default: '/maven'
|
||||
features?: {
|
||||
snapshots?: boolean; // Support SNAPSHOT versions (default: true)
|
||||
checksums?: boolean; // Auto-generate checksums (default: true)
|
||||
metadata?: boolean; // Auto-generate maven-metadata.xml (default: true)
|
||||
allowedExtensions?: string[]; // Allowed file extensions (default: jar, war, pom, etc.)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven POM (Project Object Model) minimal structure
|
||||
* Only essential fields for validation
|
||||
*/
|
||||
export interface IMavenPom {
|
||||
modelVersion: string; // Always "4.0.0"
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
version: string;
|
||||
packaging?: string; // jar, war, pom, etc.
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven repository search result
|
||||
*/
|
||||
export interface IMavenSearchResult {
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
latestVersion: string;
|
||||
versions: string[];
|
||||
lastUpdated: string;
|
||||
}
|
||||
Reference in New Issue
Block a user