import * as plugins from '../plugins.js'; import { BaseUpstream } from '../upstream/classes.baseupstream.js'; import type { IProtocolUpstreamConfig, IUpstreamFetchContext, IUpstreamRegistryConfig, } from '../upstream/interfaces.upstream.js'; /** * RubyGems-specific upstream implementation. * * Handles: * - Compact Index format (/versions, /info/{gem}, /names) * - Gem file (.gem) downloading * - Gem spec fetching * - HTTP Range requests for incremental updates */ export class RubygemsUpstream extends BaseUpstream { protected readonly protocolName = 'rubygems'; constructor( config: IProtocolUpstreamConfig, logger?: plugins.smartlog.Smartlog, ) { super(config, logger); } /** * Fetch the /versions file (master list of all gems). */ public async fetchVersions(etag?: string): Promise<{ data: string; etag?: string } | null> { const headers: Record = { 'accept': 'text/plain', }; if (etag) { headers['if-none-match'] = etag; } const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: '*', resourceType: 'versions', path: '/versions', method: 'GET', headers, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } let data: string; if (Buffer.isBuffer(result.body)) { data = result.body.toString('utf8'); } else if (typeof result.body === 'string') { data = result.body; } else { return null; } return { data, etag: result.headers['etag'], }; } /** * Fetch gem info file (/info/{gemname}). */ public async fetchInfo(gemName: string): Promise { const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: gemName, resourceType: 'info', path: `/info/${gemName}`, method: 'GET', headers: { 'accept': 'text/plain', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return result.body.toString('utf8'); } return typeof result.body === 'string' ? result.body : null; } /** * Fetch the /names file (list of all gem names). */ public async fetchNames(): Promise { const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: '*', resourceType: 'names', path: '/names', method: 'GET', headers: { 'accept': 'text/plain', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return result.body.toString('utf8'); } return typeof result.body === 'string' ? result.body : null; } /** * Fetch a gem file. */ public async fetchGem(gemName: string, version: string): Promise { const path = `/gems/${gemName}-${version}.gem`; const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: gemName, resourceType: 'gem', path, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Fetch gem spec (quick spec). */ public async fetchQuickSpec(gemName: string, version: string): Promise { const path = `/quick/Marshal.4.8/${gemName}-${version}.gemspec.rz`; const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: gemName, resourceType: 'spec', path, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Fetch gem versions JSON from API. */ public async fetchVersionsJson(gemName: string): Promise { const path = `/api/v1/versions/${gemName}.json`; const context: IUpstreamFetchContext = { protocol: 'rubygems', resource: gemName, resourceType: 'versions-json', path, method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return Array.isArray(result.body) ? result.body : null; } /** * Override URL building for RubyGems-specific handling. */ protected buildUpstreamUrl( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): string { let baseUrl = upstream.url; // Remove trailing slash if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } return `${baseUrl}${context.path}`; } }