maven-proxy 1.3.1 → 1.3.2

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English](README.md) | [中文](README-zh.md)
4
4
 
5
+ [English ChangeLog](ChangeLog.md) | [中文更新日志](ChangeLog-zh.md)
6
+
5
7
  [![npm version](https://img.shields.io/npm/v/maven-proxy.svg)](https://www.npmjs.com/package/maven-proxy)
6
8
 
7
9
  ## 1. Project Positioning
@@ -275,6 +277,8 @@ Environment variables:
275
277
  - REPO_FALLBACK_REPOS: repository fallback list.
276
278
  - NPM_REGISTRY_DOMAINS: npm domains for ecosystem routing (wildcards supported).
277
279
  - MAVEN_REPO_DOMAINS: maven domains for ecosystem routing (wildcards supported).
280
+ - MAVEN_CACHE_USE_DOMAIN_DIR: whether Maven cache uses hostname as the first-level directory. Default false.
281
+ - MAVEN_CACHE_IGNORE_PATH_PREFIXES: Maven cache path-prefix ignore rules (comma-separated, supports host/path and host:port/path). Default: repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven.
278
282
  - HTTPS_MITM_DOMAINS: MITM domain list (includes registry.npmjs.org by default, wildcards supported).
279
283
  - DOWNLOAD_LOG_DIR: log directory.
280
284
  - LOG_RETENTION: log retention duration (supports s/m/h/d), for example 7d.
@@ -325,6 +329,8 @@ Priority:
325
329
  - `HTTPS_PASSTHROUGH_FOR_UNMATCHED`: Whether unmatched HTTPS domains are tunneled directly. Default `false`.
326
330
  - `NPM_REGISTRY_DOMAINS`: Domains treated as npm ecosystem for cache routing (wildcards supported).
327
331
  - `MAVEN_REPO_DOMAINS`: Domains treated as Maven ecosystem for cache routing (wildcards supported).
332
+ - `MAVEN_CACHE_USE_DOMAIN_DIR`: Whether Maven cache uses hostname as the first-level directory. Default `false`.
333
+ - `MAVEN_CACHE_IGNORE_PATH_PREFIXES`: Maven cache path-prefix ignore rules (comma-separated, supports `host/path` and `host:port/path`). Default `repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven`.
328
334
  - `MULTI_THREAD_DOMAINS`: Domains allowed to use multi-thread download (wildcards supported).
329
335
  - `MULTI_THREAD_COUNT`: Number of download threads for ranged downloads.
330
336
  - `MULTI_THREAD_MIN_SIZE_MB`: Minimum size threshold to trigger multi-thread download (MB).
@@ -179,6 +179,8 @@ function getDefaultConfigTemplate() {
179
179
  appendEntry("HTTPS_PASSTHROUGH_FOR_UNMATCHED", "false", "未命中 MITM 域名时是否允许隧道透传,默认 false。", "Allow tunnel passthrough for unmatched MITM domains. Default: false.");
180
180
  appendEntry("NPM_REGISTRY_DOMAINS", "registry.npmjs.org,registry.npmmirror.com,npm.pkg.github.com", "识别为 npm 生态并分流缓存的域名列表(支持通配符)。", "Domains recognized as npm ecosystem for cache routing (wildcard supported).");
181
181
  appendEntry("MAVEN_REPO_DOMAINS", "repo1.maven.org,repo.maven.apache.org,jitpack.io,plugins.gradle.org,dl.google.com", "识别为 Maven 生态并分流缓存的域名列表(支持通配符)。", "Domains recognized as Maven ecosystem for cache routing (wildcard supported).");
182
+ appendEntry("MAVEN_CACHE_USE_DOMAIN_DIR", "false", "Maven 缓存是否按域名作为一级目录,默认 false。", "Whether Maven cache uses hostname as the first-level directory. Default: false.");
183
+ appendEntry("MAVEN_CACHE_IGNORE_PATH_PREFIXES", "repo1.maven.org/maven2,repo.maven.apache.org/maven2,jitpack.io/,plugins.gradle.org/m2,dl.google.com/dl/android/maven2,dl.google.com/dl/google/maven", "Maven 缓存应忽略的路径前缀规则(逗号分隔,支持 host/path 与 host:port/path)。", "Maven cache path-prefix ignore rules (comma-separated, supports host/path and host:port/path).");
182
184
  appendEntry("MULTI_THREAD_DOMAINS", "repo1.maven.org", "启用多线程下载的域名列表(支持通配符)。", "Domains that enable multi-thread download (wildcard supported).");
183
185
  appendEntry("MULTI_THREAD_COUNT", "8", "多线程下载线程数,默认 8。", "Thread count for multi-thread download. Default: 8.");
184
186
  appendEntry("MULTI_THREAD_MIN_SIZE_MB", "1", "触发多线程下载的最小文件大小阈值(MB),默认 1。", "Minimum file size threshold in MB to trigger multi-thread download. Default: 1.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-proxy",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Maven proxy with cache, HTTPS MITM for selected domains, and local repo publishing",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -16,6 +16,162 @@ function looksLikeMavenVersionSegment(segment) {
16
16
  return /^\d[0-9A-Za-z._-]*$/.test(String(segment || ""));
17
17
  }
18
18
 
19
+ function normalizeSlashPath(rawPath) {
20
+ const decoded = safeDecode(rawPath || "/").replace(/\\/g, "/").replace(/\/+/g, "/");
21
+ if (!decoded) {
22
+ return "/";
23
+ }
24
+ return decoded.startsWith("/") ? decoded : `/${decoded}`;
25
+ }
26
+
27
+ function trimTrailingSlash(normalizedPath) {
28
+ if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
29
+ return normalizedPath.slice(0, -1);
30
+ }
31
+ return normalizedPath;
32
+ }
33
+
34
+ function normalizeRulePrefixPath(rawPrefix) {
35
+ return trimTrailingSlash(normalizeSlashPath(rawPrefix || "/"));
36
+ }
37
+
38
+ function getEffectivePort(urlObj) {
39
+ if (urlObj?.port) {
40
+ return String(urlObj.port);
41
+ }
42
+ return urlObj?.protocol === "https:" ? "443" : "80";
43
+ }
44
+
45
+ function parseMavenCacheIgnorePathRule(rawRule) {
46
+ const text = String(rawRule || "").trim();
47
+ if (!text) {
48
+ return null;
49
+ }
50
+
51
+ const firstSlash = text.indexOf("/");
52
+ if (firstSlash <= 0) {
53
+ return null;
54
+ }
55
+
56
+ const hostPortText = text.slice(0, firstSlash).trim().toLowerCase();
57
+ const prefixText = text.slice(firstSlash);
58
+ const hostMatch = hostPortText.match(/^([^:\/]+)(?::(\d+))?$/);
59
+ if (!hostMatch) {
60
+ return null;
61
+ }
62
+
63
+ const hostname = hostMatch[1];
64
+ const port = hostMatch[2] || "";
65
+ const pathPrefix = normalizeRulePrefixPath(prefixText);
66
+
67
+ return {
68
+ hostname,
69
+ port,
70
+ pathPrefix,
71
+ raw: text,
72
+ };
73
+ }
74
+
75
+ function ruleMatchesUrl(rule, urlObj) {
76
+ if (!rule || !urlObj) {
77
+ return false;
78
+ }
79
+
80
+ const hostname = String(urlObj.hostname || "").toLowerCase();
81
+ if (!hostname || hostname !== rule.hostname) {
82
+ return false;
83
+ }
84
+
85
+ if (!rule.port) {
86
+ return true;
87
+ }
88
+
89
+ return getEffectivePort(urlObj) === rule.port;
90
+ }
91
+
92
+ export function parseMavenCacheIgnorePathPrefixes(rawRules) {
93
+ const input = Array.isArray(rawRules) ? rawRules.join(",") : String(rawRules || "");
94
+ const tokens = input
95
+ .split(",")
96
+ .map((item) => item.trim())
97
+ .filter(Boolean);
98
+
99
+ const parsed = [];
100
+ const dedupe = new Set();
101
+
102
+ for (const token of tokens) {
103
+ const rule = parseMavenCacheIgnorePathRule(token);
104
+ if (!rule) {
105
+ continue;
106
+ }
107
+
108
+ const key = `${rule.hostname}:${rule.port}|${rule.pathPrefix}`;
109
+ if (dedupe.has(key)) {
110
+ continue;
111
+ }
112
+
113
+ dedupe.add(key);
114
+ parsed.push(rule);
115
+ }
116
+
117
+ return parsed;
118
+ }
119
+
120
+ export function stripMavenIgnoredPathPrefix(pathname, urlObj, rules = []) {
121
+ const normalizedPath = normalizeSlashPath(pathname || "/");
122
+ const validRules = Array.isArray(rules) ? rules : [];
123
+ if (validRules.length === 0 || !urlObj) {
124
+ return normalizedPath;
125
+ }
126
+
127
+ const matchedRules = validRules
128
+ .filter((rule) => ruleMatchesUrl(rule, urlObj))
129
+ .sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
130
+
131
+ for (const rule of matchedRules) {
132
+ const prefix = rule.pathPrefix;
133
+ if (prefix === "/") {
134
+ return normalizedPath;
135
+ }
136
+
137
+ if (normalizedPath === prefix) {
138
+ return "/";
139
+ }
140
+
141
+ if (normalizedPath.startsWith(`${prefix}/`)) {
142
+ const stripped = normalizedPath.slice(prefix.length);
143
+ return stripped || "/";
144
+ }
145
+ }
146
+
147
+ return normalizedPath;
148
+ }
149
+
150
+ export function buildMavenHostlessPathCandidates(pathname, rules = []) {
151
+ const normalizedPath = normalizeSlashPath(pathname || "/");
152
+ const candidates = new Set([normalizedPath]);
153
+ const validRules = Array.isArray(rules) ? rules : [];
154
+
155
+ for (const rule of validRules) {
156
+ const prefix = rule.pathPrefix;
157
+ if (!prefix || prefix === "/") {
158
+ continue;
159
+ }
160
+
161
+ if (normalizedPath === prefix) {
162
+ candidates.add("/");
163
+ continue;
164
+ }
165
+
166
+ if (normalizedPath.startsWith(`${prefix}/`)) {
167
+ const stripped = normalizedPath.slice(prefix.length);
168
+ candidates.add(stripped || "/");
169
+ }
170
+ }
171
+
172
+ return [...candidates];
173
+ }
174
+
19
175
  function isLikelyMavenFilePath(parts, normalizedPath) {
20
176
  if (normalizedPath.endsWith("/") || parts.length === 0) {
21
177
  return false;
@@ -63,9 +219,14 @@ function isLikelyMavenFilePath(parts, normalizedPath) {
63
219
  export function getCacheFilePath(cacheDir, urlObj, options = {}) {
64
220
  const ecosystem = sanitizeSegment(String(options.ecosystem || "generic").toLowerCase());
65
221
  const includeHost = options.includeHost ?? ecosystem !== "maven";
222
+ const mavenIgnoreRules = options.mavenCacheIgnorePathPrefixRules || [];
66
223
 
67
224
  const rawPathname = safeDecode(urlObj.pathname || "/");
68
- const normalized = rawPathname.replace(/\\/g, "/");
225
+ let normalized = rawPathname.replace(/\\/g, "/");
226
+ if (ecosystem === "maven") {
227
+ normalized = stripMavenIgnoredPathPrefix(normalized, urlObj, mavenIgnoreRules);
228
+ }
229
+
69
230
  const lowerNormalized = normalized.toLowerCase();
70
231
  const parts = normalized.split("/").filter(Boolean);
71
232
 
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import dotenv from "dotenv";
5
5
  import { detectJavaHome } from "../common/java-home.js";
6
+ import { parseMavenCacheIgnorePathPrefixes } from "../cache/cache-path.js";
6
7
 
7
8
  const cwd = process.cwd();
8
9
  const userConfigDir = path.resolve(os.homedir(), "maven-proxy");
@@ -196,11 +197,24 @@ const defaultRepoFallbackRepos = [
196
197
  "https://dl.google.com",
197
198
  ];
198
199
 
200
+ const defaultMavenCacheIgnorePathPrefixes = [
201
+ "repo1.maven.org/maven2",
202
+ "repo.maven.apache.org/maven2",
203
+ "jitpack.io/",
204
+ "plugins.gradle.org/m2",
205
+ "dl.google.com/dl/android/maven2",
206
+ "dl.google.com/dl/google/maven",
207
+ ];
208
+
199
209
  const repoFallbackRepos = normalizeRepoList(
200
210
  process.env.REPO_FALLBACK_REPOS,
201
211
  defaultRepoFallbackRepos,
202
212
  );
203
213
 
214
+ const mavenCacheIgnorePathPrefixes = String(process.env.MAVEN_CACHE_IGNORE_PATH_PREFIXES || "").trim()
215
+ || defaultMavenCacheIgnorePathPrefixes.join(",");
216
+ const mavenCacheIgnorePathPrefixRules = parseMavenCacheIgnorePathPrefixes(mavenCacheIgnorePathPrefixes);
217
+
204
218
  const defaultMavenRepoDomains = [
205
219
  "repo1.maven.org",
206
220
  "repo.maven.apache.org",
@@ -241,6 +255,9 @@ export const config = {
241
255
  httpsMitmDomains: toList(process.env.HTTPS_MITM_DOMAINS, ["repo1.maven.org", "repo.maven.apache.org", "registry.npmjs.org"]),
242
256
  npmRegistryDomains: toList(process.env.NPM_REGISTRY_DOMAINS, ["registry.npmjs.org", "registry.npmmirror.com", "npm.pkg.github.com"]),
243
257
  mavenRepoDomains: toList(process.env.MAVEN_REPO_DOMAINS, [...new Set(defaultMavenRepoDomains)]),
258
+ mavenCacheUseDomainDir: toBool(process.env.MAVEN_CACHE_USE_DOMAIN_DIR, false),
259
+ mavenCacheIgnorePathPrefixes,
260
+ mavenCacheIgnorePathPrefixRules,
244
261
  multiThreadDomains: toList(process.env.MULTI_THREAD_DOMAINS, ["repo1.maven.org"]),
245
262
  multiThreadCount: Math.max(1, toInt(process.env.MULTI_THREAD_COUNT, 4)),
246
263
  multiThreadMinSizeBytes,
package/src/index.js CHANGED
@@ -120,6 +120,8 @@ async function main() {
120
120
  startupInfo(`[maven-proxy] repo port: ${config.repoPort}`);
121
121
  startupInfo(`[maven-proxy] cache dir : ${config.cacheDir}`);
122
122
  startupInfo(`[maven-proxy] cache maven: ${config.mavenCacheDir}`);
123
+ startupInfo(`[maven-proxy] maven cache use domain dir: ${config.mavenCacheUseDomainDir}`);
124
+ startupInfo(`[maven-proxy] maven cache ignore path prefixes: ${config.mavenCacheIgnorePathPrefixes || "(none)"}`);
123
125
  startupInfo(`[maven-proxy] cache npm : ${config.npmCacheDir}`);
124
126
  startupInfo(`[maven-proxy] cache other: ${config.genericCacheDir}`);
125
127
  startupInfo(`[maven-proxy] log dir: ${config.downloadLogDir}`);
@@ -200,7 +200,8 @@ export function createHttpRequestHandler({
200
200
  ecosystem = detectPackageEcosystem(urlObj, config, matchesDomain);
201
201
  cachePath = getCacheFilePath(config.cacheDir, urlObj, {
202
202
  ecosystem,
203
- includeHost: ecosystem !== "maven",
203
+ includeHost: ecosystem !== "maven" || config.mavenCacheUseDomainDir,
204
+ mavenCacheIgnorePathPrefixRules: config.mavenCacheIgnorePathPrefixRules,
204
205
  });
205
206
 
206
207
  if (ecosystem === "maven" && mavenAffinityIndex?.enabled) {
@@ -220,7 +221,9 @@ export function createHttpRequestHandler({
220
221
  }
221
222
 
222
223
  if (canonical && mavenAffinityIndex) {
223
- if (isPositiveAffinityEligible(canonical.fileName)) {
224
+ const canUsePositiveAffinity = !config.mavenCacheUseDomainDir && isPositiveAffinityEligible(canonical.fileName);
225
+
226
+ if (canUsePositiveAffinity) {
224
227
  const preferredPath = await mavenAffinityIndex.resolvePreferredCachePath(canonical.canonicalKey);
225
228
  if (preferredPath) {
226
229
  console.log(`[proxy] affinity hit canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
@@ -244,7 +247,12 @@ export function createHttpRequestHandler({
244
247
  await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
245
248
  await downloader.ensureCached(urlObj, cachePath, req.headers);
246
249
 
247
- if (canonical && mavenAffinityIndex && isPositiveAffinityEligible(canonical.fileName)) {
250
+ if (
251
+ canonical &&
252
+ mavenAffinityIndex &&
253
+ !config.mavenCacheUseDomainDir &&
254
+ isPositiveAffinityEligible(canonical.fileName)
255
+ ) {
248
256
  mavenAffinityIndex.recordSuccess({
249
257
  canonicalKey: canonical.canonicalKey,
250
258
  host: urlObj.hostname,
@@ -1,6 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
+ import {
5
+ buildMavenHostlessPathCandidates,
6
+ stripMavenIgnoredPathPrefix,
7
+ } from "../cache/cache-path.js";
4
8
 
5
9
  function safeJoin(baseDir, requestPath) {
6
10
  const pathname = decodeURIComponent(requestPath || "/");
@@ -13,6 +17,58 @@ function safeJoin(baseDir, requestPath) {
13
17
  return path.join(baseDir, normalized);
14
18
  }
15
19
 
20
+ function sanitizeHostSegment(hostname) {
21
+ return String(hostname || "unknown").toLowerCase().replace(/[<>:\\"|?*]/g, "_");
22
+ }
23
+
24
+ function collectRepoHosts(repoBases = []) {
25
+ const hosts = new Set();
26
+
27
+ for (const repoBase of repoBases) {
28
+ try {
29
+ const parsed = new URL(repoBase);
30
+ hosts.add(sanitizeHostSegment(parsed.hostname));
31
+ } catch {
32
+ // ignore invalid URL
33
+ }
34
+ }
35
+
36
+ return [...hosts];
37
+ }
38
+
39
+ function buildDomainScopedPath(mavenCacheDir, hostname, relativePath) {
40
+ const hostDir = sanitizeHostSegment(hostname);
41
+ return safeJoin(path.join(mavenCacheDir, hostDir), relativePath);
42
+ }
43
+
44
+ function normalizeRelativePathForHost(relativePath, hostname, rules = []) {
45
+ const inputPath = `/${String(relativePath || "").replace(/^\/+/, "")}`;
46
+ const normalized = stripMavenIgnoredPathPrefix(
47
+ inputPath,
48
+ { protocol: "https:", hostname, port: "" },
49
+ rules,
50
+ );
51
+ return normalized.replace(/^\/+/, "");
52
+ }
53
+
54
+ function normalizeRelativePathCandidates(relativePath, rules = []) {
55
+ const inputPath = `/${String(relativePath || "").replace(/^\/+/, "")}`;
56
+ const candidates = buildMavenHostlessPathCandidates(inputPath, rules);
57
+ return candidates.map((item) => item.replace(/^\/+/, ""));
58
+ }
59
+
60
+ function buildDefaultRepoFilePath(config, relativePath) {
61
+ if (!config.mavenCacheUseDomainDir) {
62
+ const candidates = normalizeRelativePathCandidates(relativePath, config.mavenCacheIgnorePathPrefixRules);
63
+ return safeJoin(config.mavenCacheDir, candidates[0] || relativePath);
64
+ }
65
+
66
+ const hosts = collectRepoHosts(config.repoFallbackRepos || []);
67
+ const host = hosts[0] || "unknown";
68
+ const normalized = normalizeRelativePathForHost(relativePath, host, config.mavenCacheIgnorePathPrefixRules);
69
+ return buildDomainScopedPath(config.mavenCacheDir, host, normalized || relativePath);
70
+ }
71
+
16
72
  async function statIfExists(filePath) {
17
73
  try {
18
74
  return await fs.promises.stat(filePath);
@@ -24,6 +80,68 @@ async function statIfExists(filePath) {
24
80
  }
25
81
  }
26
82
 
83
+ async function findCachedMavenFile(config, relativePath) {
84
+ const ignoreRules = config.mavenCacheIgnorePathPrefixRules || [];
85
+
86
+ if (!config.mavenCacheUseDomainDir) {
87
+ const candidates = normalizeRelativePathCandidates(relativePath, ignoreRules);
88
+ for (const candidate of candidates) {
89
+ const filePath = safeJoin(config.mavenCacheDir, candidate);
90
+ const stats = await statIfExists(filePath);
91
+ if (stats && stats.isFile()) {
92
+ return { filePath, stats };
93
+ }
94
+ }
95
+
96
+ const fallbackPath = safeJoin(config.mavenCacheDir, candidates[0] || relativePath);
97
+ return { filePath: fallbackPath, stats: null };
98
+ }
99
+
100
+ const checkedHosts = new Set();
101
+ const preferredHosts = collectRepoHosts(config.repoFallbackRepos || []);
102
+
103
+ for (const host of preferredHosts) {
104
+ checkedHosts.add(host);
105
+ const normalized = normalizeRelativePathForHost(relativePath, host, ignoreRules);
106
+ const filePath = buildDomainScopedPath(config.mavenCacheDir, host, normalized || relativePath);
107
+ const stats = await statIfExists(filePath);
108
+ if (stats && stats.isFile()) {
109
+ return { filePath, stats };
110
+ }
111
+ }
112
+
113
+ let entries = [];
114
+ try {
115
+ entries = await fs.promises.readdir(config.mavenCacheDir, { withFileTypes: true });
116
+ } catch (error) {
117
+ if (error.code !== "ENOENT") {
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ for (const entry of entries) {
123
+ if (!entry.isDirectory()) {
124
+ continue;
125
+ }
126
+
127
+ if (checkedHosts.has(entry.name)) {
128
+ continue;
129
+ }
130
+
131
+ const normalized = normalizeRelativePathForHost(relativePath, entry.name, ignoreRules);
132
+ const filePath = safeJoin(path.join(config.mavenCacheDir, entry.name), normalized || relativePath);
133
+ const stats = await statIfExists(filePath);
134
+ if (stats && stats.isFile()) {
135
+ return { filePath, stats };
136
+ }
137
+ }
138
+
139
+ return {
140
+ filePath: buildDefaultRepoFilePath(config, relativePath),
141
+ stats: null,
142
+ };
143
+ }
144
+
27
145
  function buildRemoteUrl(repoBase, relativePath) {
28
146
  const base = repoBase.endsWith("/") ? repoBase : `${repoBase}/`;
29
147
  const relative = relativePath.replace(/^\/+/, "");
@@ -41,31 +159,45 @@ function buildCandidateRelativePaths(relativePath) {
41
159
  return [...new Set(candidates.filter(Boolean))];
42
160
  }
43
161
 
44
- async function ensureFromRemoteRepos(config, downloader, filePath, relativePath, cacheCleanupManager = null) {
162
+ async function ensureFromRemoteRepos(config, downloader, relativePath, cacheCleanupManager = null) {
45
163
  if (!downloader) {
46
- return null;
164
+ return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
47
165
  }
48
166
 
49
167
  const repos = config.repoFallbackRepos || [];
50
168
  if (repos.length === 0) {
51
- return null;
169
+ return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
52
170
  }
53
171
 
54
172
  let hasNon404Error = false;
55
173
  let lastError = null;
56
174
  const candidatePaths = buildCandidateRelativePaths(relativePath);
175
+ const ignoreRules = config.mavenCacheIgnorePathPrefixRules || [];
57
176
 
58
177
  for (const repoBase of repos) {
59
178
  for (const candidatePath of candidatePaths) {
60
179
  const remoteUrl = buildRemoteUrl(repoBase, candidatePath);
61
180
 
62
181
  try {
182
+ const normalizedRelativePath = normalizeRelativePathForHost(
183
+ relativePath,
184
+ remoteUrl.hostname,
185
+ ignoreRules,
186
+ ) || relativePath;
187
+
188
+ const targetPath = config.mavenCacheUseDomainDir
189
+ ? buildDomainScopedPath(config.mavenCacheDir, remoteUrl.hostname, normalizedRelativePath)
190
+ : safeJoin(config.mavenCacheDir, normalizedRelativePath);
191
+
63
192
  if (cacheCleanupManager) {
64
193
  await cacheCleanupManager.checkAndCleanupIfNeeded("repo-cache-miss");
65
194
  }
66
195
  console.log(`[repo] cache miss, try remote ${remoteUrl.href}`);
67
- await downloader.ensureCached(remoteUrl, filePath, {});
68
- return await statIfExists(filePath);
196
+ await downloader.ensureCached(remoteUrl, targetPath, {});
197
+ return {
198
+ filePath: targetPath,
199
+ stats: await statIfExists(targetPath),
200
+ };
69
201
  } catch (error) {
70
202
  lastError = error;
71
203
  if (error.statusCode !== 404) {
@@ -79,7 +211,7 @@ async function ensureFromRemoteRepos(config, downloader, filePath, relativePath,
79
211
  throw lastError;
80
212
  }
81
213
 
82
- return null;
214
+ return { filePath: buildDefaultRepoFilePath(config, relativePath), stats: null };
83
215
  }
84
216
 
85
217
  export function startRepoServer(config, downloader = null, cacheCleanupManager = null) {
@@ -87,11 +219,12 @@ export function startRepoServer(config, downloader = null, cacheCleanupManager =
87
219
  try {
88
220
  const urlObj = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
89
221
  const relativePath = path.posix.normalize(urlObj.pathname || "/").replace(/^\/+/, "");
90
- const filePath = safeJoin(config.mavenCacheDir, relativePath);
91
- let stats = await statIfExists(filePath);
222
+ let { filePath, stats } = await findCachedMavenFile(config, relativePath);
92
223
 
93
224
  if (!stats || !stats.isFile()) {
94
- stats = await ensureFromRemoteRepos(config, downloader, filePath, relativePath, cacheCleanupManager);
225
+ const fetched = await ensureFromRemoteRepos(config, downloader, relativePath, cacheCleanupManager);
226
+ filePath = fetched.filePath;
227
+ stats = fetched.stats;
95
228
  }
96
229
 
97
230
  if (!stats || !stats.isFile()) {