diff --git a/src/i18n.c b/src/i18n.c index 2e4ef81..a27f5bc 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -54,7 +54,7 @@ static const char *kBuiltinZh = "\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\"," "\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\"," "\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\"," -"\"msg.resolving-dep\":\"正在解析依赖 %s%s%s\",\"msg.dep-satisfied\":\"依赖 %s%s%s 已装/被提供,跳过\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\",\"msg.cache-cleared\":\"已清理缓存: %d 个文件,释放 %.1f KB\",\"msg.doctor-issues\":\"doctor: 发现 %d 个问题\",\"msg.self-up-to-date\":\"当前 v%s 已是最新(v%s),跳过升级\"," +"\"msg.resolving-dep\":\"正在解析依赖 %s%s%s\",\"msg.dep-satisfied\":\"依赖 %s%s%s 已装/被提供,跳过\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\",\"msg.cache-cleared\":\"已清理缓存: %d 个文件,释放 %.1f KB\",\"msg.offline-using-cache\":\"离线: 使用缓存 %s\",\"msg.err.not-cached\":\"%s 未在缓存中(先运行 pmm fetch)\",\"msg.doctor-issues\":\"doctor: 发现 %d 个问题\",\"msg.self-up-to-date\":\"当前 v%s 已是最新(v%s),跳过升级\"," "\"msg.path-exists\":\"PATH 已包含 PMM 目录\",\"msg.err.registry-not-found\":\"找不到软件包 '%s'\"," "\"msg.pmm-self-replaced\":\"pmm 已更新到 %s\",\"msg.pmm-self-hint\":\"无法覆盖 %s(需 root/管理员);新版本在 %s。请将其加入 PATH 并在当前 shell 运行 hash -r\"," "\"msg.err.usage\":\"用法\",\"msg.err.unknown-cmd\":\"未知命令 '%s' (试试 'pmm help')\"," @@ -63,13 +63,13 @@ static const char *kBuiltinZh = "\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\"," "\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\"," "\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\"," -"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.doctor\":\"诊断\",\"desc.help\":\"帮助\"," +"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.cache\":\"管理缓存\",\"desc.fetch\":\"预下载到缓存\",\"desc.doctor\":\"诊断\",\"desc.help\":\"帮助\"," "\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\"," "\"opt.verbose\":\"详情\"}"; static const char *kBuiltinEn = -"{\"help.banner\":\"PMM %s (%s)\",\"help.usage\":\"usage:\",\"help.commands\":\"commands:\",\"help.options\":\"options:\",\"help.common\":\"common commands:\",\"cmd.list\":\"list\",\"cmd.info\":\"info\",\"cmd.search\":\"search\",\"cmd.install\":\"install\",\"cmd.remove\":\"remove\",\"cmd.update\":\"update\",\"cmd.upgrade\":\"upgrade\",\"cmd.mirror\":\"mirror\",\"cmd.self-update\":\"self-update\",\"cmd.version\":\"version\",\"cmd.help\":\"help\",\"desc.list\":\"list packages by name\",\"desc.info\":\"show detailed info for a package\",\"desc.search\":\"search packages by keyword\",\"desc.install\":\"install a package by name\",\"desc.remove\":\"uninstall a package by name\",\"desc.update\":\"refresh the registry index (apt-style)\",\"desc.upgrade\":\"upgrade all installed packages\",\"desc.mirror\":\"list / manage mirrors\",\"desc.self-update\":\"update the pmm tool itself\",\"desc.version\":\"show version\",\"desc.help\":\"show help\",\"opt.p-drive\":\"install under :\\\\\\\\.pmm\",\"opt.no-color\":\"omit ANSI colours on a terminal (PMM_NO_COLOR=1 too)\",\"opt.quiet\":\"only print errors/warnings (suppress info/success)\",\"opt.verbose\":\"print extra detail\",\"msg.looking-up\":\"looking up %s in mirror %s\",\"msg.selected\":\"selected %s@%s (os=%s arch=%s)\",\"msg.downloading\":\"downloading %s%s\",\"msg.downloaded\":\"downloaded %s\",\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\",\"msg.sha256\":\"sha256: %s %s\",\"msg.sha1\":\"sha1: %s %s\",\"msg.installing\":\"installing %s ...\",\"msg.installing-file\":\"installing local file %s\",\"msg.installed\":\"installed %s\",\"msg.removed\":\"removed %s (%d files)\",\"msg.packed\":\"packed %s (%s %s) -> %s\",\"msg.checksum-ok\":\"%s checksum OK: %s\",\"msg.checksum-mismatch\":\"CHECKSUM MISMATCH (%s)!\\n expected: %s\\n actual: %s\",\"msg.registry-ok\":\"registry sha256 OK: %s\",\"msg.registry-not-found\":\"package '%s' not found in any registry mirror\",\"msg.no-version\":\"no version of '%s' for os=%s arch=%s satisfying '%s'\",\"msg.up-to-date\":\"%s %s is up to date (latest %s)\",\"msg.upgrading\":\"%s: %s -> %s\",\"msg.upgraded\":\"upgraded %d package(s).\",\"msg.no-installed\":\"no installed packages.\",\"msg.registry-empty\":\"registry index is empty.\",\"msg.registry-updated\":\"updated registry index: %d/%d packages cached.\",\"msg.skipping-no-registry\":\"skipping %s: no registry entry\",\"msg.no-match\":\"no package matches '%s'\",\"msg.description\":\"description: %s\",\"msg.cleaning-cache\":\"cleaning cache %s\",\"msg.drive-set\":\"install drive set to %c:\",\"msg.path-set\":\"install path set to %s\",\"msg.path-adding\":\"adding to PATH: %s\",\"msg.path-exists\":\"PATH already contains PMM dirs\",\"msg.path-updated\":\"PATH updated for new shells (reopen a terminal or run: setx) (user-level)\",\"msg.path-reload\":\"updated %s (reload your shell)\",\"msg.registered\":\"registered '%s' %s in Installed Apps (uninstall via pmm remove %s)\",\"msg.unregistered\":\"removed '%s' from Installed Apps\",\"msg.mirror-added\":\"mirror '%s' added\",\"msg.mirror-active\":\"active mirror set to '%s'\",\"msg.mirror-removed\":\"mirror '%s' removed\",\"msg.lang-set\":\"language set to '%s'\",\"msg.setting-set\":\"set %s = %s\",\"msg.empty-path\":\"empty file path\",\"msg.verifying-native\":\"verifying %s is a native %s binary...\",\"msg.resolving-dep\":\"resolving dependency %s%s%s\",\"msg.err.usage\":\"usage:\",\"msg.err.not-found\":\"package '%s' not found\",\"msg.err.not-installed\":\"package '%s' is not installed\",\"msg.err.empty-path\":\"empty file path\",\"msg.err.registry-not-found\":\"package '%s' not found in any registry mirror\",\"msg.err.no-suitable-asset\":\"no suitable %s asset found in latest release of %s\",\"msg.err.unknown-cmd\":\"unknown command '%s' (try 'pmm help')\",\"msg.err.failed-install\":\"failed to install %s\",\"msg.err.cannot-write\":\"cannot write %s\",\"msg.err.cannot-read\":\"cannot read %s\",\"msg.err.cannot-open\":\"cannot open file: %s\",\"msg.err.invalid-locale\":\"invalid language pack: %s\",\"msg.err.download-failed\":\"failed to download %s\",\"msg.err.no-registry-mirror\":\"no registry mirror configured. Add to ~/.pmm/mirror.ini\",\"msg.err.no-mirror\":\"no mirrors configured (use: pmm mirror add)\",\"msg.err.no-mirror-file\":\"no mirror file\",\"msg.err.no-registry-index\":\"no registry index (packages.json) available\",\"msg.err.bad-registry-index\":\"bad registry index\",\"msg.err.bad-entry\":\"bad registry entry '%s'\",\"msg.err.bad-registry-entry\":\"bad registry entry for '%s' (%s)\",\"msg.err.checksum-mismatch-file\":\"checksum mismatch for %s in %s\",\"msg.err.no-control\":\"%s has no pdm-control file\",\"msg.err.no-package\":\"pdm-control missing 'Package:' field\",\"msg.err.tar-control\":\"tar (control) failed\",\"msg.err.tar-data\":\"tar (data) failed\",\"msg.err.not-pdm\":\"'%s' is not a .pdm file\",\"msg.err.no-tar\":\"cannot run tar\",\"msg.err.bad-archive\":\"not a valid .pdm archive: %s\",\"msg.err.no-control-tar\":\"missing control.tar.gz in %s\",\"msg.err.extract\":\"data extraction failed\",\"msg.err.oom\":\"out of memory\",\"msg.err.json-parse\":\"JSON parse error: %s\",\"msg.err.unknown-host\":\"unknown host '%s'\",\"msg.err.checksum-refuse\":\"refusing to install: checksum verification failed\",\"msg.err.registry-entry-no-url\":\"registry entry for '%s' has no url\",\"msg.err.rpm-stage\":\"cannot create rpm stage dir: %s\",\"msg.err.rpm-decompress\":\"rpm payload decompress/cpio failed (need %s)\",\"msg.err.rpm-copy\":\"failed to copy rpm payload into /\",\"msg.warn.parse-checksum\":\"warning: could not parse %s checksum file\",\"msg.warn.download-retry\":\"download failed, trying next source...\",\"msg.warn.not-usable\":\"%s is not a usable %s binary; will fall back\",\"msg.warn.no-checksum\":\"warning: no sha256sums member, skipping integrity check\",\"msg.warn.no-filelist\":\"warning: could not record file list\",\"msg.warn.no-register\":\"warning: could not register '%s' in Installed Apps\",\"msg.api-unreachable\":\"could not reach a known release API for %s\",\"msg.retrying-asset\":\"retrying with %s (%s)\",\"mirror.sources\":\"mirrors:\",\"mirror.checking\":\"checking registry mirrors...\",\"mirror.result\":\"result: %d/%d sources reachable\",\"lang.packs-in\":\"language packs in %s:\",\"desc.clean\":\"clean cache\",\"msg.dep-satisfied\":\"dependency %s%s%s already installed/provided, skipped\",\"msg.err.conflict\":\"cannot install %s: conflicts with installed %s\",\"msg.cache-cleared\":\"cache cleaned: %d files, freed %.1f KB\",\"msg.self-up-to-date\":\"already up to date (v%s, latest v%s), skipping upgrade\",\"desc.doctor\":\"diagnose\",\"msg.doctor-issues\":\"doctor: found %d issue(s)\"}"; +"{\"help.banner\":\"PMM %s (%s)\",\"help.usage\":\"usage:\",\"help.commands\":\"commands:\",\"help.options\":\"options:\",\"help.common\":\"common commands:\",\"cmd.list\":\"list\",\"cmd.info\":\"info\",\"cmd.search\":\"search\",\"cmd.install\":\"install\",\"cmd.remove\":\"remove\",\"cmd.update\":\"update\",\"cmd.upgrade\":\"upgrade\",\"cmd.mirror\":\"mirror\",\"cmd.self-update\":\"self-update\",\"cmd.version\":\"version\",\"cmd.help\":\"help\",\"desc.list\":\"list packages by name\",\"desc.info\":\"show detailed info for a package\",\"desc.search\":\"search packages by keyword\",\"desc.install\":\"install a package by name\",\"desc.remove\":\"uninstall a package by name\",\"desc.update\":\"refresh the registry index (apt-style)\",\"desc.upgrade\":\"upgrade all installed packages\",\"desc.mirror\":\"list / manage mirrors\",\"desc.self-update\":\"update the pmm tool itself\",\"desc.version\":\"show version\",\"desc.help\":\"show help\",\"opt.p-drive\":\"install under :\\\\\\\\.pmm\",\"opt.no-color\":\"omit ANSI colours on a terminal (PMM_NO_COLOR=1 too)\",\"opt.quiet\":\"only print errors/warnings (suppress info/success)\",\"opt.verbose\":\"print extra detail\",\"msg.looking-up\":\"looking up %s in mirror %s\",\"msg.selected\":\"selected %s@%s (os=%s arch=%s)\",\"msg.downloading\":\"downloading %s%s\",\"msg.downloaded\":\"downloaded %s\",\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\",\"msg.sha256\":\"sha256: %s %s\",\"msg.sha1\":\"sha1: %s %s\",\"msg.installing\":\"installing %s ...\",\"msg.installing-file\":\"installing local file %s\",\"msg.installed\":\"installed %s\",\"msg.removed\":\"removed %s (%d files)\",\"msg.packed\":\"packed %s (%s %s) -> %s\",\"msg.checksum-ok\":\"%s checksum OK: %s\",\"msg.checksum-mismatch\":\"CHECKSUM MISMATCH (%s)!\\n expected: %s\\n actual: %s\",\"msg.registry-ok\":\"registry sha256 OK: %s\",\"msg.registry-not-found\":\"package '%s' not found in any registry mirror\",\"msg.no-version\":\"no version of '%s' for os=%s arch=%s satisfying '%s'\",\"msg.up-to-date\":\"%s %s is up to date (latest %s)\",\"msg.upgrading\":\"%s: %s -> %s\",\"msg.upgraded\":\"upgraded %d package(s).\",\"msg.no-installed\":\"no installed packages.\",\"msg.registry-empty\":\"registry index is empty.\",\"msg.registry-updated\":\"updated registry index: %d/%d packages cached.\",\"msg.skipping-no-registry\":\"skipping %s: no registry entry\",\"msg.no-match\":\"no package matches '%s'\",\"msg.description\":\"description: %s\",\"msg.cleaning-cache\":\"cleaning cache %s\",\"msg.drive-set\":\"install drive set to %c:\",\"msg.path-set\":\"install path set to %s\",\"msg.path-adding\":\"adding to PATH: %s\",\"msg.path-exists\":\"PATH already contains PMM dirs\",\"msg.path-updated\":\"PATH updated for new shells (reopen a terminal or run: setx) (user-level)\",\"msg.path-reload\":\"updated %s (reload your shell)\",\"msg.registered\":\"registered '%s' %s in Installed Apps (uninstall via pmm remove %s)\",\"msg.unregistered\":\"removed '%s' from Installed Apps\",\"msg.mirror-added\":\"mirror '%s' added\",\"msg.mirror-active\":\"active mirror set to '%s'\",\"msg.mirror-removed\":\"mirror '%s' removed\",\"msg.lang-set\":\"language set to '%s'\",\"msg.setting-set\":\"set %s = %s\",\"msg.empty-path\":\"empty file path\",\"msg.verifying-native\":\"verifying %s is a native %s binary...\",\"msg.resolving-dep\":\"resolving dependency %s%s%s\",\"msg.err.usage\":\"usage:\",\"msg.err.not-found\":\"package '%s' not found\",\"msg.err.not-installed\":\"package '%s' is not installed\",\"msg.err.empty-path\":\"empty file path\",\"msg.err.registry-not-found\":\"package '%s' not found in any registry mirror\",\"msg.err.no-suitable-asset\":\"no suitable %s asset found in latest release of %s\",\"msg.err.unknown-cmd\":\"unknown command '%s' (try 'pmm help')\",\"msg.err.failed-install\":\"failed to install %s\",\"msg.err.cannot-write\":\"cannot write %s\",\"msg.err.cannot-read\":\"cannot read %s\",\"msg.err.cannot-open\":\"cannot open file: %s\",\"msg.err.invalid-locale\":\"invalid language pack: %s\",\"msg.err.download-failed\":\"failed to download %s\",\"msg.err.no-registry-mirror\":\"no registry mirror configured. Add to ~/.pmm/mirror.ini\",\"msg.err.no-mirror\":\"no mirrors configured (use: pmm mirror add)\",\"msg.err.no-mirror-file\":\"no mirror file\",\"msg.err.no-registry-index\":\"no registry index (packages.json) available\",\"msg.err.bad-registry-index\":\"bad registry index\",\"msg.err.bad-entry\":\"bad registry entry '%s'\",\"msg.err.bad-registry-entry\":\"bad registry entry for '%s' (%s)\",\"msg.err.checksum-mismatch-file\":\"checksum mismatch for %s in %s\",\"msg.err.no-control\":\"%s has no pdm-control file\",\"msg.err.no-package\":\"pdm-control missing 'Package:' field\",\"msg.err.tar-control\":\"tar (control) failed\",\"msg.err.tar-data\":\"tar (data) failed\",\"msg.err.not-pdm\":\"'%s' is not a .pdm file\",\"msg.err.no-tar\":\"cannot run tar\",\"msg.err.bad-archive\":\"not a valid .pdm archive: %s\",\"msg.err.no-control-tar\":\"missing control.tar.gz in %s\",\"msg.err.extract\":\"data extraction failed\",\"msg.err.oom\":\"out of memory\",\"msg.err.json-parse\":\"JSON parse error: %s\",\"msg.err.unknown-host\":\"unknown host '%s'\",\"msg.err.checksum-refuse\":\"refusing to install: checksum verification failed\",\"msg.err.registry-entry-no-url\":\"registry entry for '%s' has no url\",\"msg.err.rpm-stage\":\"cannot create rpm stage dir: %s\",\"msg.err.rpm-decompress\":\"rpm payload decompress/cpio failed (need %s)\",\"msg.err.rpm-copy\":\"failed to copy rpm payload into /\",\"msg.warn.parse-checksum\":\"warning: could not parse %s checksum file\",\"msg.warn.download-retry\":\"download failed, trying next source...\",\"msg.warn.not-usable\":\"%s is not a usable %s binary; will fall back\",\"msg.warn.no-checksum\":\"warning: no sha256sums member, skipping integrity check\",\"msg.warn.no-filelist\":\"warning: could not record file list\",\"msg.warn.no-register\":\"warning: could not register '%s' in Installed Apps\",\"msg.api-unreachable\":\"could not reach a known release API for %s\",\"msg.retrying-asset\":\"retrying with %s (%s)\",\"mirror.sources\":\"mirrors:\",\"mirror.checking\":\"checking registry mirrors...\",\"mirror.result\":\"result: %d/%d sources reachable\",\"lang.packs-in\":\"language packs in %s:\",\"desc.clean\":\"clean cache\",\"desc.cache\":\"manage download cache\",\"desc.fetch\":\"predownload to cache\",\"msg.dep-satisfied\":\"dependency %s%s%s already installed/provided, skipped\",\"msg.err.conflict\":\"cannot install %s: conflicts with installed %s\",\"msg.cache-cleared\":\"cache cleaned: %d files, freed %.1f KB\",\"msg.offline-using-cache\":\"offline: using cached %s\",\"msg.err.not-cached\":\"%s not in cache (run pmm fetch)\",\"msg.self-up-to-date\":\"already up to date (v%s, latest v%s), skipping upgrade\",\"desc.doctor\":\"diagnose\",\"msg.doctor-issues\":\"doctor: found %d issue(s)\"}"; /* Which built-in fallback matches the current locale? en -> en-US, else zh-CN. */ static const char *builtin_source(void) { diff --git a/src/install.c b/src/install.c index bca0575..eb5a7ae 100644 --- a/src/install.c +++ b/src/install.c @@ -158,6 +158,8 @@ static int install_path(const char *path, const char *name); int pmm_no_cache = 0; /* --no-cache: drop cached file before download */ int pmm_force_reinstall = 0; /* --force: reinstall even if already present */ int pmm_yes = 0; /* -y/--yes: skip confirmation prompts */ +int pmm_offline = 0; /* --offline: use cache only */ +int pmm_fetch_only = 0; /* pmm fetch: stop after download, don't install */ /* Extract the data.tar.* member of a .deb (an ar container) to `outdata`. * Pure C (no ar/dpkg required). Returns compression code: @@ -415,6 +417,18 @@ int install_file(const char *url, const char *name) { snprintf(path, sizeof(path), "%s/%s", cache, name); if (pmm_no_cache) remove(path); /* force a fresh download */ + /* --offline: install from the cache only, never touch the network */ + if (pmm_offline) { + FILE *cf = fopen(path, "rb"); + if (!cf) { + pmm_error("%s", pmm_tr_fmt("msg.err.not-cached", name)); + return -1; + } + fclose(cf); + pmm_info("%s", pmm_tr_fmt("msg.offline-using-cache", path)); + return (pmm_fetch_only) ? 0 : install_path(path, name); + } + /* apt-style fallback: mirrors (by priority) first, then the origin URL */ MirrorList *ml = mirrors_load(); int ncand = 0; @@ -448,6 +462,9 @@ int install_file(const char *url, const char *name) { return -1; } + /* pmm fetch: we only wanted to populate the cache — stop here. */ + if (pmm_fetch_only) return 0; + return install_path(path, name); } @@ -759,6 +776,24 @@ static int parse_dep(const char *d, char *name, size_t ns, char *spec, size_t ss return name[0] ? 0 : -1; } +/* If `spec` is a single exact version ("1.2.3" or "==1.2.3"), copy it to `out` + * and return 1; otherwise return 0 (range / multiple conditions). */ +static int exact_version(const char *spec, char *out, size_t osz) { + if (!spec || !*spec) return 0; + const char *p = spec; + while (*p == ' ' || *p == '\t') p++; + if (p[0] == '=' && p[1] == '=') p += 2; + else if (p[0] == '=') p += 1; + while (*p == ' ' || *p == '\t') p++; + if (!*p) return 0; + for (const char *q = p; *q; q++) + if (*q == '<' || *q == '>' || *q == ',' || *q == '!' || *q == '=' || *q == ' ') return 0; + size_t n = strlen(p); + if (n >= osz) n = osz - 1; + memcpy(out, p, n); out[n] = 0; + return 1; +} + /* Install a comma-separated Depends list. Exposed so local .pdm installs resolve deps. */ int pmm_install_dep_list(const char *list) { if (!list || !*list) return 0; @@ -851,6 +886,24 @@ int install_from_registry(const char *name, const char *spec, const char *mirror return -1; } + /* For an exact version, prefer the per-version index + * {base}//.json (which carries precisely that version's + * variants). Falls back to the package's variants if it 404s. */ + char exactver[128]; + if (exact_version(spec, exactver, sizeof(exactver))) { + char purl[2100]; + snprintf(purl, sizeof(purl), "%s/%s/%s.json", used_base, name, exactver); + int pst = 0; + char *pbody = http_get(purl, &pst); + if (pbody && pst != 404 && pst != 403 && pst != 503) { + JsonValue *pv = json_parse(pbody); + free(pbody); + JsonValue *pvv = pv ? json_get(pv, "variants") : NULL; + if (pvv && pvv->count > 0) { json_free(meta); meta = pv; } + else json_free(pv); + } + } + /* resolve declared dependencies before installing this package */ JsonValue *deps = json_get(meta, "depends"); if (deps && deps->type == JSON_ARRAY) { diff --git a/src/install.h b/src/install.h index adcf381..f615133 100644 --- a/src/install.h +++ b/src/install.h @@ -13,6 +13,12 @@ int install_local_file(const char *path); /* Set by `pmm install --no-cache` to force a fresh download (drop cache file). */ extern int pmm_no_cache; +/* Set by `--offline` to install from the cache only (no network). */ +extern int pmm_offline; + +/* Set by `pmm fetch` to stop after downloading (caching) a package, not install. */ +extern int pmm_fetch_only; + /* Set by `pmm install --force` to reinstall even if a package is already * present, and by `-y/--yes` to skip any confirmation prompt. */ extern int pmm_force_reinstall; diff --git a/src/main.c b/src/main.c index 794c488..6e50f18 100644 --- a/src/main.c +++ b/src/main.c @@ -875,6 +875,64 @@ static int cmd_clean(void) { return 0; } +/* ---- pmm cache [list|clean] ---- */ +static unsigned long long g_list_size; +static int g_list_count; +static void cache_list_walk(const char *path, int top) { +#ifdef _WIN32 + char patt[1200]; snprintf(patt, sizeof(patt), "%s\\*", path); + WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(patt, &fd); + if (h != INVALID_HANDLE_VALUE) { + do { + if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; + char full[1400]; snprintf(full, sizeof(full), "%s\\%s", path, fd.cFileName); + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) cache_list_walk(full, 0); + else { ULONGLONG sz = ((ULONGLONG)fd.nFileSizeHigh << 32) | fd.nFileSizeLow; + printf(" %s\n", full); g_list_count++; g_list_size += (unsigned long long)sz; } + } while (FindNextFileA(h, &fd)); + FindClose(h); + } +#else + DIR *d = opendir(path); + if (d) { + struct dirent *e; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; + char full[1400]; snprintf(full, sizeof(full), "%s/%s", path, e->d_name); + struct stat st; + if (stat(full, &st) == 0) { + if (S_ISDIR(st.st_mode)) cache_list_walk(full, 0); + else { printf(" %s\n", full); g_list_count++; g_list_size += (unsigned long long)st.st_size; } + } + } + closedir(d); + } +#endif + (void)top; +} +static int cmd_cache(int argc, char **argv) { + if (argc > 0 && strcmp(argv[0], "clean") == 0) return cmd_clean(); + char cache[1024]; + pmm_cache_dir(cache, sizeof(cache)); + g_list_size = 0; g_list_count = 0; + printf("cache: %s\n", cache); + cache_list_walk(cache, 1); + printf("(%d 个文件, %.1f KB)\n", g_list_count, (double)g_list_size / 1024.0); + return 0; +} + +/* ---- pmm fetch ... : predownload packages into the cache (no install) ---- */ +static int cmd_fetch(int argc, char **argv) { + if (argc < 1) { pmm_error("用法: pmm fetch ...\n"); return 1; } + pmm_fetch_only = 1; + int ok = 0; + for (int i = 0; i < argc; i++) + if (install_from_registry(argv[i], NULL, NULL) == 0) ok++; + pmm_fetch_only = 0; + pmm_success("已抓取 %d/%d 个包到缓存\n", ok, argc); + return ok == argc ? 0 : 1; +} + /* Return the registry "latest" version of `pkg`. We use the top-level * `version` field of `.json` (the mirror bumps it on every release) rather * than walking the variants array. Returns a malloc'd string, or NULL. */ @@ -1069,6 +1127,8 @@ static void print_help(void) { printf(" %-32s%s\n", "pmm setting mirror ... ", pmm_tr("desc.mirror")); printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update")); printf(" %-32s%s\n", "pmm clean ", pmm_tr("desc.clean")); + printf(" %-32s%s\n", "pmm cache [list|clean] ", pmm_tr("desc.cache")); + printf(" %-32s%s\n", "pmm fetch ", pmm_tr("desc.fetch")); printf(" %-32s%s\n", "pmm doctor ", pmm_tr("desc.doctor")); printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help")); printf("\n%s\n", pmm_tr("help.options")); @@ -1390,6 +1450,12 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "clean") == 0) return cmd_clean(); + if (strcmp(argv[1], "cache") == 0) + return cmd_cache(argc - 2, argv + 2); + + if (strcmp(argv[1], "fetch") == 0) + return cmd_fetch(argc - 2, argv + 2); + if (strcmp(argv[1], "doctor") == 0 || strcmp(argv[1], "diagnose") == 0) return cmd_doctor(); @@ -1419,6 +1485,7 @@ int main(int argc, char **argv) { for (int i = 2; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "--no-cache") == 0) { pmm_no_cache = 1; continue; } + if (strcmp(a, "--offline") == 0 || strcmp(a, "-o") == 0) { pmm_offline = 1; continue; } if (strcmp(a, "--force") == 0) { pmm_force_reinstall = 1; continue; } if (strcmp(a, "-y") == 0 || strcmp(a, "--yes") == 0) { pmm_yes = 1; continue; } if (strcmp(a, "-dpkg") == 0) { forced = 1; if (i + 1 < argc) items[ni++] = argv[++i]; continue; } diff --git a/web/mirror/lang/en-US.pjson b/web/mirror/lang/en-US.pjson index 4fd1b97..812d90b 100644 --- a/web/mirror/lang/en-US.pjson +++ b/web/mirror/lang/en-US.pjson @@ -130,5 +130,9 @@ "msg.cache-cleared": "cache cleaned: %d files, freed %.1f KB", "msg.self-up-to-date": "already up to date (v%s, latest v%s), skipping upgrade", "desc.doctor": "diagnose", - "msg.doctor-issues": "doctor: found %d issue(s)" + "msg.doctor-issues": "doctor: found %d issue(s)", + "desc.cache": "manage download cache", + "desc.fetch": "predownload to cache", + "msg.offline-using-cache": "offline: using cached %s", + "msg.err.not-cached": "%s not in cache (run pmm fetch)" } diff --git a/web/mirror/lang/zh-CN.pjson b/web/mirror/lang/zh-CN.pjson index a479fb2..d00a1e8 100644 --- a/web/mirror/lang/zh-CN.pjson +++ b/web/mirror/lang/zh-CN.pjson @@ -130,5 +130,9 @@ "msg.cache-cleared": "已清理缓存: %d 个文件,释放 %.1f KB", "msg.self-up-to-date": "当前 v%s 已是最新(v%s),跳过升级", "desc.doctor": "诊断", - "msg.doctor-issues": "doctor: 发现 %d 个问题" + "msg.doctor-issues": "doctor: 发现 %d 个问题", + "desc.cache": "管理缓存", + "desc.fetch": "预下载到缓存", + "msg.offline-using-cache": "离线: 使用缓存 %s", + "msg.err.not-cached": "%s 未在缓存中(先运行 pmm fetch)" } diff --git a/web/pmu/publish.php b/web/pmu/publish.php index 7c91864..70ddae2 100644 --- a/web/pmu/publish.php +++ b/web/pmu/publish.php @@ -1,8 +1,14 @@ .json, stores the .pdm under - * web/mirror/packages//, and appends the name to packages.json. - * Duplicate package names are rejected (409). Requires a valid bearer token. */ + * mirror registry: writes/updates dists/.json, stores the .pdm under + * web/mirror/files///, and appends the name to packages.json. + * + * Ownership rules: + * - a package that doesn't exist yet is created, and the uploader becomes owner; + * - the same owner may publish a NEW version of the same package (the version + * must not already exist); + * - a different creator is rejected (409). + * Requires a valid bearer token. */ define('PMM_SITE', 1); require __DIR__ . '/lib.php'; @@ -23,45 +29,51 @@ $body = file_get_contents('php://input'); if (strlen($body) === 0) pmu_fail('empty upload body'); if (strlen($body) > 100 * 1024 * 1024) pmu_fail('package too large (>100 MB)'); -$regDir = __DIR__ . '/../mirror/dists'; +$regDir = __DIR__ . '/../mirror/dists'; $filesRoot = __DIR__ . '/../mirror/files'; -$pkgJson = $regDir . '/' . $name . '.json'; -$letter = strtolower(substr($name, 0, 1)); +$pkgJson = $regDir . '/' . $name . '.json'; +$letter = strtolower(substr($name, 0, 1)); -/* duplicate-name reject: the package name is already published */ -if (is_file($pkgJson)) pmu_fail("package name '$name' already exists", 409); - -$sha = hash('sha256', $body); +$sha = hash('sha256', $body); $file = $ver . '-' . $os . '-' . $arch . '.pdm'; +$url = 'https://pmm.parlz.com/mirror/files/' . $letter . '/' . $name . '/' . $file; +$variant = [ + 'name' => $name, 'version' => $ver, 'os' => $os, 'arch' => $arch, + 'file' => $file, 'url' => $url, 'sha256' => $sha, 'description' => $desc, +]; + +/* existing package? */ +$meta = null; +if (is_file($pkgJson)) $meta = json_decode(file_get_contents($pkgJson), true) ?: null; + +if ($meta) { + /* ownership: only the original creator may add versions */ + $own = $meta['owner'] ?? ''; + if ($own !== $email) pmu_fail("package '$name' already exists (owner: $own)", 409); + /* the version must be new */ + foreach (($meta['variants'] ?? []) as $v) + if (($v['version'] ?? '') === $ver) pmu_fail("version '$ver' already published for $name", 409); + /* append the new variant (mirror keeps all versions) */ + $meta['version'] = $ver; + $meta['variants'][] = $variant; +} else { + $meta = [ + 'name' => $name, 'version' => $ver, 'os' => $os, 'arch' => $arch, + 'description' => $desc, 'owner' => $email, 'variants' => [$variant], + ]; +} + +/* write the payload */ $pkgDir = $filesRoot . '/' . $letter . '/' . $name; if (!is_dir($pkgDir)) @mkdir($pkgDir, 0777, true); file_put_contents($pkgDir . '/' . $file, $body); -$url = 'https://pmm.parlz.com/mirror/files/' . $letter . '/' . $name . '/' . $file; -$meta = [ - 'name' => $name, - 'version' => $ver, - 'os' => $os, - 'arch' => $arch, - 'description' => $desc, - 'variants' => [[ - 'name' => $name, - 'version' => $ver, - 'os' => $os, - 'arch' => $arch, - 'file' => $file, - 'url' => $url, - 'sha256' => $sha, - 'description' => $desc, - ]], -]; +/* write the registry + per-version metadata */ file_put_contents($pkgJson, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - -/* per-version metadata: dists//.json */ $vdir = $regDir . '/' . $name; if (!is_dir($vdir)) @mkdir($vdir, 0777, true); file_put_contents($vdir . '/' . $ver . '.json', - json_encode(['name' => $name, 'version' => $ver, 'variants' => $meta['variants']], + json_encode(['name' => $name, 'version' => $ver, 'owner' => $email, 'variants' => [$variant]], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); /* append the name to the aggregate index if new */