160 lines
4.0 KiB
TypeScript
160 lines
4.0 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
|
import type {
|
|
IProtocolUpstreamConfig,
|
|
IUpstreamFetchContext,
|
|
IUpstreamRegistryConfig,
|
|
} from '../upstream/interfaces.upstream.js';
|
|
|
|
/**
|
|
* Cargo-specific upstream implementation.
|
|
*
|
|
* Handles:
|
|
* - Crate metadata (index) fetching
|
|
* - Crate file (.crate) downloading
|
|
* - Sparse index protocol support
|
|
* - Content-addressable caching for .crate files
|
|
*/
|
|
export class CargoUpstream extends BaseUpstream {
|
|
protected readonly protocolName = 'cargo';
|
|
|
|
/** Base URL for crate downloads (may differ from index URL) */
|
|
private readonly downloadUrl: string;
|
|
|
|
constructor(
|
|
config: IProtocolUpstreamConfig,
|
|
downloadUrl?: string,
|
|
logger?: plugins.smartlog.Smartlog,
|
|
) {
|
|
super(config, logger);
|
|
// Default to crates.io download URL if not specified
|
|
this.downloadUrl = downloadUrl || 'https://static.crates.io/crates';
|
|
}
|
|
|
|
/**
|
|
* Fetch crate metadata from the sparse index.
|
|
*/
|
|
public async fetchCrateIndex(crateName: string): Promise<string | null> {
|
|
const path = this.buildIndexPath(crateName);
|
|
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'cargo',
|
|
resource: crateName,
|
|
resourceType: 'index',
|
|
path,
|
|
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 crate file from upstream.
|
|
*/
|
|
public async fetchCrate(crateName: string, version: string): Promise<Buffer | null> {
|
|
// Crate downloads typically go to a different URL than the index
|
|
const path = `/${crateName}/${crateName}-${version}.crate`;
|
|
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'cargo',
|
|
resource: crateName,
|
|
resourceType: 'crate',
|
|
path,
|
|
method: 'GET',
|
|
headers: {
|
|
'accept': 'application/octet-stream',
|
|
},
|
|
query: {},
|
|
};
|
|
|
|
// Use special handling for crate downloads
|
|
const result = await this.fetchCrateFile(crateName, version);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Fetch crate file directly from the download URL.
|
|
*/
|
|
private async fetchCrateFile(crateName: string, version: string): Promise<Buffer | null> {
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'cargo',
|
|
resource: crateName,
|
|
resourceType: 'crate',
|
|
path: `/${crateName}/${crateName}-${version}.crate`,
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Build the sparse index path for a crate.
|
|
*
|
|
* Path structure:
|
|
* - 1 char: /1/{name}
|
|
* - 2 chars: /2/{name}
|
|
* - 3 chars: /3/{first char}/{name}
|
|
* - 4+ chars: /{first 2}/{next 2}/{name}
|
|
*/
|
|
private buildIndexPath(crateName: string): string {
|
|
const lowerName = crateName.toLowerCase();
|
|
const len = lowerName.length;
|
|
|
|
if (len === 1) {
|
|
return `/1/${lowerName}`;
|
|
} else if (len === 2) {
|
|
return `/2/${lowerName}`;
|
|
} else if (len === 3) {
|
|
return `/3/${lowerName[0]}/${lowerName}`;
|
|
} else {
|
|
return `/${lowerName.slice(0, 2)}/${lowerName.slice(2, 4)}/${lowerName}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override URL building for Cargo-specific handling.
|
|
*/
|
|
protected buildUpstreamUrl(
|
|
upstream: IUpstreamRegistryConfig,
|
|
context: IUpstreamFetchContext,
|
|
): string {
|
|
let baseUrl = upstream.url;
|
|
|
|
// For crate downloads, use the download URL
|
|
if (context.resourceType === 'crate') {
|
|
baseUrl = this.downloadUrl;
|
|
}
|
|
|
|
// Remove trailing slash
|
|
if (baseUrl.endsWith('/')) {
|
|
baseUrl = baseUrl.slice(0, -1);
|
|
}
|
|
|
|
return `${baseUrl}${context.path}`;
|
|
}
|
|
}
|