feat(upstream): Add upstream proxy/cache subsystem and integrate per-protocol upstreams
This commit is contained in:
159
ts/cargo/classes.cargoupstream.ts
Normal file
159
ts/cargo/classes.cargoupstream.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user