@push.rocks/smartregistry 2.2.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
- package/dist_ts/cargo/classes.cargoregistry.js +42 -4
- package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
- package/dist_ts/cargo/classes.cargoupstream.js +129 -0
- package/dist_ts/cargo/index.d.ts +1 -0
- package/dist_ts/cargo/index.js +2 -1
- package/dist_ts/classes.smartregistry.js +8 -8
- package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
- package/dist_ts/composer/classes.composerregistry.js +34 -3
- package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
- package/dist_ts/composer/classes.composerupstream.js +159 -0
- package/dist_ts/composer/index.d.ts +1 -0
- package/dist_ts/composer/index.js +2 -1
- package/dist_ts/core/interfaces.core.d.ts +3 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
- package/dist_ts/maven/classes.mavenregistry.js +69 -4
- package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
- package/dist_ts/maven/classes.mavenupstream.js +153 -0
- package/dist_ts/maven/index.d.ts +1 -0
- package/dist_ts/maven/index.js +2 -1
- package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
- package/dist_ts/npm/classes.npmregistry.js +55 -6
- package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
- package/dist_ts/npm/classes.npmupstream.js +206 -0
- package/dist_ts/npm/index.d.ts +1 -0
- package/dist_ts/npm/index.js +2 -1
- package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
- package/dist_ts/oci/classes.ociregistry.js +78 -17
- package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
- package/dist_ts/oci/classes.ociupstream.js +206 -0
- package/dist_ts/oci/index.d.ts +1 -0
- package/dist_ts/oci/index.js +2 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
- package/dist_ts/pypi/classes.pypiregistry.js +60 -4
- package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
- package/dist_ts/pypi/classes.pypiupstream.js +165 -0
- package/dist_ts/pypi/index.d.ts +1 -0
- package/dist_ts/pypi/index.js +2 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
- package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
- package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
- package/dist_ts/rubygems/index.d.ts +1 -0
- package/dist_ts/rubygems/index.js +2 -1
- package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
- package/dist_ts/upstream/classes.baseupstream.js +409 -0
- package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
- package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
- package/dist_ts/upstream/classes.upstreamcache.d.ts +123 -0
- package/dist_ts/upstream/classes.upstreamcache.js +328 -0
- package/dist_ts/upstream/index.d.ts +6 -0
- package/dist_ts/upstream/index.js +7 -0
- package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
- package/dist_ts/upstream/interfaces.upstream.js +23 -0
- package/package.json +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +48 -3
- package/ts/cargo/classes.cargoupstream.ts +159 -0
- package/ts/cargo/index.ts +1 -0
- package/ts/classes.smartregistry.ts +49 -7
- package/ts/composer/classes.composerregistry.ts +39 -2
- package/ts/composer/classes.composerupstream.ts +200 -0
- package/ts/composer/index.ts +1 -0
- package/ts/core/interfaces.core.ts +3 -0
- package/ts/index.ts +3 -0
- package/ts/maven/classes.mavenregistry.ts +84 -3
- package/ts/maven/classes.mavenupstream.ts +220 -0
- package/ts/maven/index.ts +1 -0
- package/ts/npm/classes.npmregistry.ts +61 -5
- package/ts/npm/classes.npmupstream.ts +260 -0
- package/ts/npm/index.ts +1 -0
- package/ts/oci/classes.ociregistry.ts +89 -17
- package/ts/oci/classes.ociupstream.ts +263 -0
- package/ts/oci/index.ts +1 -0
- package/ts/plugins.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +68 -3
- package/ts/pypi/classes.pypiupstream.ts +211 -0
- package/ts/pypi/index.ts +1 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
- package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
- package/ts/rubygems/index.ts +1 -0
- package/ts/upstream/classes.baseupstream.ts +521 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +423 -0
- package/ts/upstream/index.ts +11 -0
- package/ts/upstream/interfaces.upstream.ts +195 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
|
3
|
+
import type {
|
|
4
|
+
IProtocolUpstreamConfig,
|
|
5
|
+
IUpstreamFetchContext,
|
|
6
|
+
IUpstreamResult,
|
|
7
|
+
IUpstreamRegistryConfig,
|
|
8
|
+
} from '../upstream/interfaces.upstream.js';
|
|
9
|
+
import type { IOciManifest, IOciImageIndex, ITagList } from './interfaces.oci.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OCI-specific upstream implementation.
|
|
13
|
+
*
|
|
14
|
+
* Handles:
|
|
15
|
+
* - Manifest fetching (image manifests and index manifests)
|
|
16
|
+
* - Blob proxying (layers, configs)
|
|
17
|
+
* - Tag list fetching
|
|
18
|
+
* - Content-addressable caching (blobs are immutable)
|
|
19
|
+
* - Docker Hub authentication flow
|
|
20
|
+
*/
|
|
21
|
+
export class OciUpstream extends BaseUpstream {
|
|
22
|
+
protected readonly protocolName = 'oci';
|
|
23
|
+
|
|
24
|
+
/** Local registry base path for URL building */
|
|
25
|
+
private readonly localBasePath: string;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
config: IProtocolUpstreamConfig,
|
|
29
|
+
localBasePath: string = '/oci',
|
|
30
|
+
logger?: plugins.smartlog.Smartlog,
|
|
31
|
+
) {
|
|
32
|
+
super(config, logger);
|
|
33
|
+
this.localBasePath = localBasePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch a manifest from upstream registries.
|
|
38
|
+
*/
|
|
39
|
+
public async fetchManifest(
|
|
40
|
+
repository: string,
|
|
41
|
+
reference: string,
|
|
42
|
+
): Promise<{ manifest: IOciManifest | IOciImageIndex; contentType: string; digest: string } | null> {
|
|
43
|
+
const context: IUpstreamFetchContext = {
|
|
44
|
+
protocol: 'oci',
|
|
45
|
+
resource: repository,
|
|
46
|
+
resourceType: 'manifest',
|
|
47
|
+
path: `/v2/${repository}/manifests/${reference}`,
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
'accept': [
|
|
51
|
+
'application/vnd.oci.image.manifest.v1+json',
|
|
52
|
+
'application/vnd.oci.image.index.v1+json',
|
|
53
|
+
'application/vnd.docker.distribution.manifest.v2+json',
|
|
54
|
+
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
55
|
+
'application/vnd.docker.distribution.manifest.v1+json',
|
|
56
|
+
].join(', '),
|
|
57
|
+
},
|
|
58
|
+
query: {},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const result = await this.fetch(context);
|
|
62
|
+
|
|
63
|
+
if (!result || !result.success) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let manifest: IOciManifest | IOciImageIndex;
|
|
68
|
+
if (Buffer.isBuffer(result.body)) {
|
|
69
|
+
manifest = JSON.parse(result.body.toString('utf8'));
|
|
70
|
+
} else {
|
|
71
|
+
manifest = result.body;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contentType = result.headers['content-type'] || 'application/vnd.oci.image.manifest.v1+json';
|
|
75
|
+
const digest = result.headers['docker-content-digest'] || '';
|
|
76
|
+
|
|
77
|
+
return { manifest, contentType, digest };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a manifest exists in upstream (HEAD request).
|
|
82
|
+
*/
|
|
83
|
+
public async headManifest(
|
|
84
|
+
repository: string,
|
|
85
|
+
reference: string,
|
|
86
|
+
): Promise<{ exists: boolean; contentType?: string; digest?: string; size?: number } | null> {
|
|
87
|
+
const context: IUpstreamFetchContext = {
|
|
88
|
+
protocol: 'oci',
|
|
89
|
+
resource: repository,
|
|
90
|
+
resourceType: 'manifest',
|
|
91
|
+
path: `/v2/${repository}/manifests/${reference}`,
|
|
92
|
+
method: 'HEAD',
|
|
93
|
+
headers: {
|
|
94
|
+
'accept': [
|
|
95
|
+
'application/vnd.oci.image.manifest.v1+json',
|
|
96
|
+
'application/vnd.oci.image.index.v1+json',
|
|
97
|
+
'application/vnd.docker.distribution.manifest.v2+json',
|
|
98
|
+
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
99
|
+
].join(', '),
|
|
100
|
+
},
|
|
101
|
+
query: {},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await this.fetch(context);
|
|
105
|
+
|
|
106
|
+
if (!result) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
return { exists: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
exists: true,
|
|
116
|
+
contentType: result.headers['content-type'],
|
|
117
|
+
digest: result.headers['docker-content-digest'],
|
|
118
|
+
size: result.headers['content-length'] ? parseInt(result.headers['content-length'], 10) : undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fetch a blob from upstream registries.
|
|
124
|
+
*/
|
|
125
|
+
public async fetchBlob(repository: string, digest: string): Promise<Buffer | null> {
|
|
126
|
+
const context: IUpstreamFetchContext = {
|
|
127
|
+
protocol: 'oci',
|
|
128
|
+
resource: repository,
|
|
129
|
+
resourceType: 'blob',
|
|
130
|
+
path: `/v2/${repository}/blobs/${digest}`,
|
|
131
|
+
method: 'GET',
|
|
132
|
+
headers: {
|
|
133
|
+
'accept': 'application/octet-stream',
|
|
134
|
+
},
|
|
135
|
+
query: {},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = await this.fetch(context);
|
|
139
|
+
|
|
140
|
+
if (!result || !result.success) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a blob exists in upstream (HEAD request).
|
|
149
|
+
*/
|
|
150
|
+
public async headBlob(
|
|
151
|
+
repository: string,
|
|
152
|
+
digest: string,
|
|
153
|
+
): Promise<{ exists: boolean; size?: number } | null> {
|
|
154
|
+
const context: IUpstreamFetchContext = {
|
|
155
|
+
protocol: 'oci',
|
|
156
|
+
resource: repository,
|
|
157
|
+
resourceType: 'blob',
|
|
158
|
+
path: `/v2/${repository}/blobs/${digest}`,
|
|
159
|
+
method: 'HEAD',
|
|
160
|
+
headers: {},
|
|
161
|
+
query: {},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await this.fetch(context);
|
|
165
|
+
|
|
166
|
+
if (!result) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!result.success) {
|
|
171
|
+
return { exists: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
exists: true,
|
|
176
|
+
size: result.headers['content-length'] ? parseInt(result.headers['content-length'], 10) : undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Fetch the tag list for a repository.
|
|
182
|
+
*/
|
|
183
|
+
public async fetchTags(repository: string, n?: number, last?: string): Promise<ITagList | null> {
|
|
184
|
+
const query: Record<string, string> = {};
|
|
185
|
+
if (n) query.n = n.toString();
|
|
186
|
+
if (last) query.last = last;
|
|
187
|
+
|
|
188
|
+
const context: IUpstreamFetchContext = {
|
|
189
|
+
protocol: 'oci',
|
|
190
|
+
resource: repository,
|
|
191
|
+
resourceType: 'tags',
|
|
192
|
+
path: `/v2/${repository}/tags/list`,
|
|
193
|
+
method: 'GET',
|
|
194
|
+
headers: {
|
|
195
|
+
'accept': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
query,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const result = await this.fetch(context);
|
|
201
|
+
|
|
202
|
+
if (!result || !result.success) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let tagList: ITagList;
|
|
207
|
+
if (Buffer.isBuffer(result.body)) {
|
|
208
|
+
tagList = JSON.parse(result.body.toString('utf8'));
|
|
209
|
+
} else {
|
|
210
|
+
tagList = result.body;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return tagList;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Override URL building for OCI-specific handling.
|
|
218
|
+
* OCI registries use /v2/ prefix and may require special handling for Docker Hub.
|
|
219
|
+
*/
|
|
220
|
+
protected buildUpstreamUrl(
|
|
221
|
+
upstream: IUpstreamRegistryConfig,
|
|
222
|
+
context: IUpstreamFetchContext,
|
|
223
|
+
): string {
|
|
224
|
+
let baseUrl = upstream.url;
|
|
225
|
+
|
|
226
|
+
// Remove trailing slash
|
|
227
|
+
if (baseUrl.endsWith('/')) {
|
|
228
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle Docker Hub special case
|
|
232
|
+
// Docker Hub uses registry-1.docker.io but library images need special handling
|
|
233
|
+
if (baseUrl.includes('docker.io') || baseUrl.includes('registry-1.docker.io')) {
|
|
234
|
+
// For library images (e.g., "nginx" -> "library/nginx")
|
|
235
|
+
const pathParts = context.path.match(/^\/v2\/([^\/]+)\/(.+)$/);
|
|
236
|
+
if (pathParts) {
|
|
237
|
+
const [, repository, rest] = pathParts;
|
|
238
|
+
// If repository doesn't contain a slash, it's a library image
|
|
239
|
+
if (!repository.includes('/')) {
|
|
240
|
+
return `${baseUrl}/v2/library/${repository}/${rest}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return `${baseUrl}${context.path}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Override header building for OCI-specific authentication.
|
|
250
|
+
* OCI registries may require token-based auth obtained from a separate endpoint.
|
|
251
|
+
*/
|
|
252
|
+
protected buildHeaders(
|
|
253
|
+
upstream: IUpstreamRegistryConfig,
|
|
254
|
+
context: IUpstreamFetchContext,
|
|
255
|
+
): Record<string, string> {
|
|
256
|
+
const headers = super.buildHeaders(upstream, context);
|
|
257
|
+
|
|
258
|
+
// OCI registries typically use Docker-Distribution-API-Version header
|
|
259
|
+
headers['docker-distribution-api-version'] = 'registry/2.0';
|
|
260
|
+
|
|
261
|
+
return headers;
|
|
262
|
+
}
|
|
263
|
+
}
|
package/ts/oci/index.ts
CHANGED
package/ts/plugins.ts
CHANGED
|
@@ -8,10 +8,16 @@ import * as smartarchive from '@push.rocks/smartarchive';
|
|
|
8
8
|
import * as smartbucket from '@push.rocks/smartbucket';
|
|
9
9
|
import * as smartlog from '@push.rocks/smartlog';
|
|
10
10
|
import * as smartpath from '@push.rocks/smartpath';
|
|
11
|
+
import * as smartrequest from '@push.rocks/smartrequest';
|
|
11
12
|
|
|
12
|
-
export { smartarchive, smartbucket, smartlog, smartpath };
|
|
13
|
+
export { smartarchive, smartbucket, smartlog, smartpath, smartrequest };
|
|
13
14
|
|
|
14
15
|
// @tsclass scope
|
|
15
16
|
import * as tsclass from '@tsclass/tsclass';
|
|
16
17
|
|
|
17
18
|
export { tsclass };
|
|
19
|
+
|
|
20
|
+
// third party
|
|
21
|
+
import { minimatch } from 'minimatch';
|
|
22
|
+
|
|
23
|
+
export { minimatch };
|
|
@@ -3,6 +3,7 @@ import { BaseRegistry } from '../core/classes.baseregistry.js';
|
|
|
3
3
|
import { RegistryStorage } from '../core/classes.registrystorage.js';
|
|
4
4
|
import { AuthManager } from '../core/classes.authmanager.js';
|
|
5
5
|
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
|
6
7
|
import { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
|
7
8
|
import type {
|
|
8
9
|
IPypiPackageMetadata,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
IPypiUploadResponse,
|
|
12
13
|
} from './interfaces.pypi.js';
|
|
13
14
|
import * as helpers from './helpers.pypi.js';
|
|
15
|
+
import { PypiUpstream } from './classes.pypiupstream.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* PyPI registry implementation
|
|
@@ -22,12 +24,14 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
22
24
|
private basePath: string = '/pypi';
|
|
23
25
|
private registryUrl: string;
|
|
24
26
|
private logger: Smartlog;
|
|
27
|
+
private upstream: PypiUpstream | null = null;
|
|
25
28
|
|
|
26
29
|
constructor(
|
|
27
30
|
storage: RegistryStorage,
|
|
28
31
|
authManager: AuthManager,
|
|
29
32
|
basePath: string = '/pypi',
|
|
30
|
-
registryUrl: string = 'http://localhost:5000'
|
|
33
|
+
registryUrl: string = 'http://localhost:5000',
|
|
34
|
+
upstreamConfig?: IProtocolUpstreamConfig
|
|
31
35
|
) {
|
|
32
36
|
super();
|
|
33
37
|
this.storage = storage;
|
|
@@ -47,6 +51,20 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
47
51
|
}
|
|
48
52
|
});
|
|
49
53
|
this.logger.enableConsole();
|
|
54
|
+
|
|
55
|
+
// Initialize upstream if configured
|
|
56
|
+
if (upstreamConfig?.enabled) {
|
|
57
|
+
this.upstream = new PypiUpstream(upstreamConfig, registryUrl, this.logger);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clean up resources (timers, connections, etc.)
|
|
63
|
+
*/
|
|
64
|
+
public destroy(): void {
|
|
65
|
+
if (this.upstream) {
|
|
66
|
+
this.upstream.stop();
|
|
67
|
+
}
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
public async init(): Promise<void> {
|
|
@@ -214,7 +232,45 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
214
232
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
215
233
|
|
|
216
234
|
// Get package metadata
|
|
217
|
-
|
|
235
|
+
let metadata = await this.storage.getPypiPackageMetadata(normalized);
|
|
236
|
+
|
|
237
|
+
// Try upstream if not found locally
|
|
238
|
+
if (!metadata && this.upstream) {
|
|
239
|
+
const upstreamHtml = await this.upstream.fetchSimplePackage(normalized);
|
|
240
|
+
if (upstreamHtml) {
|
|
241
|
+
// Parse the HTML to extract file information and cache it
|
|
242
|
+
// For now, just return the upstream HTML directly (caching can be improved later)
|
|
243
|
+
const acceptHeader = context.headers['accept'] || context.headers['Accept'] || '';
|
|
244
|
+
const preferJson = acceptHeader.includes('application/vnd.pypi.simple') &&
|
|
245
|
+
acceptHeader.includes('json');
|
|
246
|
+
|
|
247
|
+
if (preferJson) {
|
|
248
|
+
// Try to get JSON format from upstream
|
|
249
|
+
const upstreamJson = await this.upstream.fetchPackageJson(normalized);
|
|
250
|
+
if (upstreamJson) {
|
|
251
|
+
return {
|
|
252
|
+
status: 200,
|
|
253
|
+
headers: {
|
|
254
|
+
'Content-Type': 'application/vnd.pypi.simple.v1+json',
|
|
255
|
+
'Cache-Control': 'public, max-age=300'
|
|
256
|
+
},
|
|
257
|
+
body: upstreamJson,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Return HTML format
|
|
263
|
+
return {
|
|
264
|
+
status: 200,
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
267
|
+
'Cache-Control': 'public, max-age=300'
|
|
268
|
+
},
|
|
269
|
+
body: upstreamHtml,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
if (!metadata) {
|
|
219
275
|
return this.errorResponse(404, 'Package not found');
|
|
220
276
|
}
|
|
@@ -449,7 +505,16 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
449
505
|
*/
|
|
450
506
|
private async handleDownload(packageName: string, filename: string): Promise<IResponse> {
|
|
451
507
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
452
|
-
|
|
508
|
+
let fileData = await this.storage.getPypiPackageFile(normalized, filename);
|
|
509
|
+
|
|
510
|
+
// Try upstream if not found locally
|
|
511
|
+
if (!fileData && this.upstream) {
|
|
512
|
+
fileData = await this.upstream.fetchPackageFile(normalized, filename);
|
|
513
|
+
if (fileData) {
|
|
514
|
+
// Cache locally
|
|
515
|
+
await this.storage.putPypiPackageFile(normalized, filename, fileData);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
453
518
|
|
|
454
519
|
if (!fileData) {
|
|
455
520
|
return {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
|
3
|
+
import type {
|
|
4
|
+
IProtocolUpstreamConfig,
|
|
5
|
+
IUpstreamFetchContext,
|
|
6
|
+
IUpstreamRegistryConfig,
|
|
7
|
+
} from '../upstream/interfaces.upstream.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PyPI-specific upstream implementation.
|
|
11
|
+
*
|
|
12
|
+
* Handles:
|
|
13
|
+
* - Simple API (HTML) - PEP 503
|
|
14
|
+
* - JSON API - PEP 691
|
|
15
|
+
* - Package file downloads (wheels, sdists)
|
|
16
|
+
* - Package name normalization
|
|
17
|
+
*/
|
|
18
|
+
export class PypiUpstream extends BaseUpstream {
|
|
19
|
+
protected readonly protocolName = 'pypi';
|
|
20
|
+
|
|
21
|
+
/** Local registry URL for rewriting download URLs */
|
|
22
|
+
private readonly localRegistryUrl: string;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
config: IProtocolUpstreamConfig,
|
|
26
|
+
localRegistryUrl: string,
|
|
27
|
+
logger?: plugins.smartlog.Smartlog,
|
|
28
|
+
) {
|
|
29
|
+
super(config, logger);
|
|
30
|
+
this.localRegistryUrl = localRegistryUrl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch Simple API index (list of all packages) in HTML format.
|
|
35
|
+
*/
|
|
36
|
+
public async fetchSimpleIndex(): Promise<string | null> {
|
|
37
|
+
const context: IUpstreamFetchContext = {
|
|
38
|
+
protocol: 'pypi',
|
|
39
|
+
resource: '*',
|
|
40
|
+
resourceType: 'index',
|
|
41
|
+
path: '/simple/',
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
'accept': 'text/html',
|
|
45
|
+
},
|
|
46
|
+
query: {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = await this.fetch(context);
|
|
50
|
+
|
|
51
|
+
if (!result || !result.success) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Buffer.isBuffer(result.body)) {
|
|
56
|
+
return result.body.toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return typeof result.body === 'string' ? result.body : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetch Simple API package page (list of files) in HTML format.
|
|
64
|
+
*/
|
|
65
|
+
public async fetchSimplePackage(packageName: string): Promise<string | null> {
|
|
66
|
+
const normalizedName = this.normalizePackageName(packageName);
|
|
67
|
+
const path = `/simple/${normalizedName}/`;
|
|
68
|
+
|
|
69
|
+
const context: IUpstreamFetchContext = {
|
|
70
|
+
protocol: 'pypi',
|
|
71
|
+
resource: packageName,
|
|
72
|
+
resourceType: 'simple',
|
|
73
|
+
path,
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
'accept': 'text/html',
|
|
77
|
+
},
|
|
78
|
+
query: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = await this.fetch(context);
|
|
82
|
+
|
|
83
|
+
if (!result || !result.success) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Buffer.isBuffer(result.body)) {
|
|
88
|
+
return result.body.toString('utf8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return typeof result.body === 'string' ? result.body : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch package metadata using JSON API (PEP 691).
|
|
96
|
+
*/
|
|
97
|
+
public async fetchPackageJson(packageName: string): Promise<any | null> {
|
|
98
|
+
const normalizedName = this.normalizePackageName(packageName);
|
|
99
|
+
const path = `/simple/${normalizedName}/`;
|
|
100
|
+
|
|
101
|
+
const context: IUpstreamFetchContext = {
|
|
102
|
+
protocol: 'pypi',
|
|
103
|
+
resource: packageName,
|
|
104
|
+
resourceType: 'metadata',
|
|
105
|
+
path,
|
|
106
|
+
method: 'GET',
|
|
107
|
+
headers: {
|
|
108
|
+
'accept': 'application/vnd.pypi.simple.v1+json',
|
|
109
|
+
},
|
|
110
|
+
query: {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = await this.fetch(context);
|
|
114
|
+
|
|
115
|
+
if (!result || !result.success) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Buffer.isBuffer(result.body)) {
|
|
120
|
+
return JSON.parse(result.body.toString('utf8'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result.body;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetch full package info from PyPI JSON API (/pypi/{package}/json).
|
|
128
|
+
*/
|
|
129
|
+
public async fetchPypiJson(packageName: string): Promise<any | null> {
|
|
130
|
+
const normalizedName = this.normalizePackageName(packageName);
|
|
131
|
+
const path = `/pypi/${normalizedName}/json`;
|
|
132
|
+
|
|
133
|
+
const context: IUpstreamFetchContext = {
|
|
134
|
+
protocol: 'pypi',
|
|
135
|
+
resource: packageName,
|
|
136
|
+
resourceType: 'pypi-json',
|
|
137
|
+
path,
|
|
138
|
+
method: 'GET',
|
|
139
|
+
headers: {
|
|
140
|
+
'accept': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
query: {},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await this.fetch(context);
|
|
146
|
+
|
|
147
|
+
if (!result || !result.success) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Buffer.isBuffer(result.body)) {
|
|
152
|
+
return JSON.parse(result.body.toString('utf8'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result.body;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fetch a package file (wheel or sdist) from upstream.
|
|
160
|
+
*/
|
|
161
|
+
public async fetchPackageFile(packageName: string, filename: string): Promise<Buffer | null> {
|
|
162
|
+
const normalizedName = this.normalizePackageName(packageName);
|
|
163
|
+
const path = `/packages/${normalizedName}/${filename}`;
|
|
164
|
+
|
|
165
|
+
const context: IUpstreamFetchContext = {
|
|
166
|
+
protocol: 'pypi',
|
|
167
|
+
resource: packageName,
|
|
168
|
+
resourceType: 'package',
|
|
169
|
+
path,
|
|
170
|
+
method: 'GET',
|
|
171
|
+
headers: {
|
|
172
|
+
'accept': 'application/octet-stream',
|
|
173
|
+
},
|
|
174
|
+
query: {},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result = await this.fetch(context);
|
|
178
|
+
|
|
179
|
+
if (!result || !result.success) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Normalize a PyPI package name according to PEP 503.
|
|
188
|
+
* - Lowercase all characters
|
|
189
|
+
* - Replace runs of ., -, _ with single -
|
|
190
|
+
*/
|
|
191
|
+
private normalizePackageName(name: string): string {
|
|
192
|
+
return name.toLowerCase().replace(/[-_.]+/g, '-');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Override URL building for PyPI-specific handling.
|
|
197
|
+
*/
|
|
198
|
+
protected buildUpstreamUrl(
|
|
199
|
+
upstream: IUpstreamRegistryConfig,
|
|
200
|
+
context: IUpstreamFetchContext,
|
|
201
|
+
): string {
|
|
202
|
+
let baseUrl = upstream.url;
|
|
203
|
+
|
|
204
|
+
// Remove trailing slash
|
|
205
|
+
if (baseUrl.endsWith('/')) {
|
|
206
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return `${baseUrl}${context.path}`;
|
|
210
|
+
}
|
|
211
|
+
}
|