镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 08:52:45 +08:00
feat: pmu same-owner version updates + exact-pin per-version json + cache/fetch/offline
pmu (publish.php):
- Track package owner (uploader email) and allow the SAME owner to publish NEW
versions of an existing package; reject an already-published version, and
reject edits by a different creator (409 with the owner).
install.c:
- exact-version pin (pmm install <pkg>==<ver>) now prefers {base}/<pkg>/<version>.json
(the per-version index generated for the mirror), falling back to variants.
- install_file respects --offline (use cache only) and pmm_fetch_only (stop after
downloading, don't install); new globals pmm_offline / pmm_fetch_only.
- deprecated the silent parallel-range path, so large downloads also show a bar.
main.c:
- new 'pmm cache list|clean' and 'pmm fetch <pkg...>' commands; '--offline'|-o flag.
- i18n keys for the new messages (zh/en builtin + language packs).
这个提交包含在:
+3
-3
文件差异因一行或多行过长而隐藏
+53
@@ -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_no_cache = 0; /* --no-cache: drop cached file before download */
|
||||||
int pmm_force_reinstall = 0; /* --force: reinstall even if already present */
|
int pmm_force_reinstall = 0; /* --force: reinstall even if already present */
|
||||||
int pmm_yes = 0; /* -y/--yes: skip confirmation prompts */
|
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`.
|
/* Extract the data.tar.* member of a .deb (an ar container) to `outdata`.
|
||||||
* Pure C (no ar/dpkg required). Returns compression code:
|
* 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);
|
snprintf(path, sizeof(path), "%s/%s", cache, name);
|
||||||
if (pmm_no_cache) remove(path); /* force a fresh download */
|
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 */
|
/* apt-style fallback: mirrors (by priority) first, then the origin URL */
|
||||||
MirrorList *ml = mirrors_load();
|
MirrorList *ml = mirrors_load();
|
||||||
int ncand = 0;
|
int ncand = 0;
|
||||||
@@ -448,6 +462,9 @@ int install_file(const char *url, const char *name) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* pmm fetch: we only wanted to populate the cache — stop here. */
|
||||||
|
if (pmm_fetch_only) return 0;
|
||||||
|
|
||||||
return install_path(path, name);
|
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;
|
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. */
|
/* Install a comma-separated Depends list. Exposed so local .pdm installs resolve deps. */
|
||||||
int pmm_install_dep_list(const char *list) {
|
int pmm_install_dep_list(const char *list) {
|
||||||
if (!list || !*list) return 0;
|
if (!list || !*list) return 0;
|
||||||
@@ -851,6 +886,24 @@ int install_from_registry(const char *name, const char *spec, const char *mirror
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* For an exact version, prefer the per-version index
|
||||||
|
* {base}/<pkg>/<version>.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 */
|
/* resolve declared dependencies before installing this package */
|
||||||
JsonValue *deps = json_get(meta, "depends");
|
JsonValue *deps = json_get(meta, "depends");
|
||||||
if (deps && deps->type == JSON_ARRAY) {
|
if (deps && deps->type == JSON_ARRAY) {
|
||||||
|
|||||||
@@ -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). */
|
/* Set by `pmm install --no-cache` to force a fresh download (drop cache file). */
|
||||||
extern int pmm_no_cache;
|
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
|
/* Set by `pmm install --force` to reinstall even if a package is already
|
||||||
* present, and by `-y/--yes` to skip any confirmation prompt. */
|
* present, and by `-y/--yes` to skip any confirmation prompt. */
|
||||||
extern int pmm_force_reinstall;
|
extern int pmm_force_reinstall;
|
||||||
|
|||||||
+67
@@ -875,6 +875,64 @@ static int cmd_clean(void) {
|
|||||||
return 0;
|
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 <pkg>... : predownload packages into the cache (no install) ---- */
|
||||||
|
static int cmd_fetch(int argc, char **argv) {
|
||||||
|
if (argc < 1) { pmm_error("用法: pmm fetch <pkg>...\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
|
/* Return the registry "latest" version of `pkg`. We use the top-level
|
||||||
* `version` field of `<pkg>.json` (the mirror bumps it on every release) rather
|
* `version` field of `<pkg>.json` (the mirror bumps it on every release) rather
|
||||||
* than walking the variants array. Returns a malloc'd string, or NULL. */
|
* 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 setting mirror ... ", pmm_tr("desc.mirror"));
|
||||||
printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update"));
|
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 clean ", pmm_tr("desc.clean"));
|
||||||
|
printf(" %-32s%s\n", "pmm cache [list|clean] ", pmm_tr("desc.cache"));
|
||||||
|
printf(" %-32s%s\n", "pmm fetch <pkg> ", pmm_tr("desc.fetch"));
|
||||||
printf(" %-32s%s\n", "pmm doctor ", pmm_tr("desc.doctor"));
|
printf(" %-32s%s\n", "pmm doctor ", pmm_tr("desc.doctor"));
|
||||||
printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help"));
|
printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help"));
|
||||||
printf("\n%s\n", pmm_tr("help.options"));
|
printf("\n%s\n", pmm_tr("help.options"));
|
||||||
@@ -1390,6 +1450,12 @@ int main(int argc, char **argv) {
|
|||||||
if (strcmp(argv[1], "clean") == 0)
|
if (strcmp(argv[1], "clean") == 0)
|
||||||
return cmd_clean();
|
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)
|
if (strcmp(argv[1], "doctor") == 0 || strcmp(argv[1], "diagnose") == 0)
|
||||||
return cmd_doctor();
|
return cmd_doctor();
|
||||||
|
|
||||||
@@ -1419,6 +1485,7 @@ int main(int argc, char **argv) {
|
|||||||
for (int i = 2; i < argc; i++) {
|
for (int i = 2; i < argc; i++) {
|
||||||
const char *a = argv[i];
|
const char *a = argv[i];
|
||||||
if (strcmp(a, "--no-cache") == 0) { pmm_no_cache = 1; continue; }
|
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, "--force") == 0) { pmm_force_reinstall = 1; continue; }
|
||||||
if (strcmp(a, "-y") == 0 || strcmp(a, "--yes") == 0) { pmm_yes = 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; }
|
if (strcmp(a, "-dpkg") == 0) { forced = 1; if (i + 1 < argc) items[ni++] = argv[++i]; continue; }
|
||||||
|
|||||||
@@ -130,5 +130,9 @@
|
|||||||
"msg.cache-cleared": "cache cleaned: %d files, freed %.1f KB",
|
"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",
|
"msg.self-up-to-date": "already up to date (v%s, latest v%s), skipping upgrade",
|
||||||
"desc.doctor": "diagnose",
|
"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)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,5 +130,9 @@
|
|||||||
"msg.cache-cleared": "已清理缓存: %d 个文件,释放 %.1f KB",
|
"msg.cache-cleared": "已清理缓存: %d 个文件,释放 %.1f KB",
|
||||||
"msg.self-up-to-date": "当前 v%s 已是最新(v%s),跳过升级",
|
"msg.self-up-to-date": "当前 v%s 已是最新(v%s),跳过升级",
|
||||||
"desc.doctor": "诊断",
|
"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)"
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-27
@@ -1,8 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
/* web/pmu/publish.php — upload a completed .pdm (raw body) and publish it to the
|
/* web/pmu/publish.php — upload a completed .pdm (raw body) and publish it to the
|
||||||
* mirror registry: writes/updates <pkg>.json, stores the .pdm under
|
* mirror registry: writes/updates dists/<pkg>.json, stores the .pdm under
|
||||||
* web/mirror/packages/<pkg>/, and appends the name to packages.json.
|
* web/mirror/files/<first-letter>/<pkg>/, and appends the name to packages.json.
|
||||||
* Duplicate package names are rejected (409). Requires a valid bearer token. */
|
*
|
||||||
|
* 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);
|
define('PMM_SITE', 1);
|
||||||
require __DIR__ . '/lib.php';
|
require __DIR__ . '/lib.php';
|
||||||
|
|
||||||
@@ -28,40 +34,46 @@ $filesRoot = __DIR__ . '/../mirror/files';
|
|||||||
$pkgJson = $regDir . '/' . $name . '.json';
|
$pkgJson = $regDir . '/' . $name . '.json';
|
||||||
$letter = strtolower(substr($name, 0, 1));
|
$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';
|
$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;
|
$pkgDir = $filesRoot . '/' . $letter . '/' . $name;
|
||||||
if (!is_dir($pkgDir)) @mkdir($pkgDir, 0777, true);
|
if (!is_dir($pkgDir)) @mkdir($pkgDir, 0777, true);
|
||||||
file_put_contents($pkgDir . '/' . $file, $body);
|
file_put_contents($pkgDir . '/' . $file, $body);
|
||||||
|
|
||||||
$url = 'https://pmm.parlz.com/mirror/files/' . $letter . '/' . $name . '/' . $file;
|
/* write the registry + per-version metadata */
|
||||||
$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,
|
|
||||||
]],
|
|
||||||
];
|
|
||||||
file_put_contents($pkgJson, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
file_put_contents($pkgJson, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
/* per-version metadata: dists/<name>/<version>.json */
|
|
||||||
$vdir = $regDir . '/' . $name;
|
$vdir = $regDir . '/' . $name;
|
||||||
if (!is_dir($vdir)) @mkdir($vdir, 0777, true);
|
if (!is_dir($vdir)) @mkdir($vdir, 0777, true);
|
||||||
file_put_contents($vdir . '/' . $ver . '.json',
|
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));
|
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
/* append the name to the aggregate index if new */
|
/* append the name to the aggregate index if new */
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户