/* mp_apps.c — 应用域:MeowAPPs 扫描 + nyai 安装/导出/移除。 */ #include "mp.h" int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail) { char pj[1024]; snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name); char buf[8192]; if (read_small_file(pj, buf, sizeof buf) <= 0) { /* 解析失败项:invalid */ if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *root = jv_parse(buf, (int)strlen(buf)); if (!root) { if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *jpkg = jv_obj_get(root, "package"); jv_t *jname = jv_obj_get(root, "name"); jv_t *jver = jv_obj_get(root, "version"); jv_t *jexec = jv_obj_get(root, "exec"); jv_t *jfw = jv_obj_get(root, "framework"); jv_t *jim = jv_obj_get(root, "immersive"); jv_t *jfull = jv_obj_get(root, "fullscreen"); const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name; const char *nm = (jname && jname->type == J_STR) ? jname->str : ""; const char *ver = (jver && jver->type == J_STR) ? jver->str : ""; const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : ""; /* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */ long long dsize = 0; snprintf(pj, sizeof pj, "%s/%s", appdir, name); DIR *d = opendir(pj); if (d) { struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name); struct stat st; if (stat(full, &st) == 0) dsize += (long long)st.st_size; } closedir(d); } if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}", jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL)); } else { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":", jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize); sA_str(out, pj); sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}"); } jv_free(root); return 0; } int api_apps_list(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); DIR *d = opendir(MEOWAPPS_DIR); if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在"); sbuf_t s = {0}; sA(&s, "{\"count\":0,\"apps\":["); int first = 1, count = 0; struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue; if (strstr(e->d_name, ".bak")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name); struct stat st; if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue; if (!first) sA(&s, ","); first = 0; apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0); count++; } closedir(d); sA(&s, "]}"); /* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */ /* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */ int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); (void)count; return rc; } int api_apps_info(int sock, const char *pkg) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char appdir[1024]; snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg); struct stat st; if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装"); sbuf_t s = {0}; if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0) return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } const char *nyai_bin(void) { /* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */ if (!access(NYAI_PATH, X_OK)) return NYAI_PATH; return NYAI_PATH_CARD; } int api_apps_install(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)"); char mpkpath[1024]; int ispath = 0; if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) { /* JSON {path} */ if (r->body && r->body[0]) { jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *pj = jv_obj_get(root, "path"); if (pj && pj->type == J_STR) { snprintf(mpkpath, sizeof mpkpath, "%s", pj->str); ispath = 1; } jv_free(root); } if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file"); } else { /* 保存上传 file 到临时文件 */ snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR); mkdir(mpkpath, 0777); char tmp[1024]; snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR); if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败"); snprintf(mpkpath, sizeof mpkpath, "%s", tmp); } if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在"); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", nyai_bin(), mpkpath); char out[4096]; int code = run_cmd(cmd, out, sizeof out); if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败"); /* 包名 = 文件名茎;版本从 app.json 读 */ char stem[256]; const char *base = strrchr(mpkpath, '/'); base = base ? base + 1 : mpkpath; snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base); char *dot = strrchr(stem, '.'); if (dot) *dot = 0; char appdir[1024], pj[1100]; snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem); jv_t *approot = NULL; char version[16] = ""; int upgraded = 0; snprintf(pj, sizeof pj, "%s/app.json", appdir); char jbuf[8192]; if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0) approot = jv_parse(jbuf, (int)strlen(jbuf)); if (approot) { jv_t *v = jv_obj_get(approot, "version"); if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str); jv_free(approot); } if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1; sbuf_t s = {0}; sA(&s, "{\"package\":"); sA_str(&s, stem); sA(&s, ",\"version\":"); sA_str(&s, version[0] ? version : NULL); sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } int api_apps_export(int sock, const char *pkg) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char tmp[1100]; char card_tmp[1200]; snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR); mkdir(card_tmp, 0777); snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg); unlink(tmp); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", nyai_bin(), pkg, tmp); char out[1024]; int code = run_cmd(cmd, out, sizeof out); if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败"); char dn[256]; snprintf(dn, sizeof dn, "%s.mpk", pkg); return send_file(sock, 200, "application/octet-stream", tmp, dn); } int api_apps_remove(int sock, const char *pkg, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char purgebuf[8] = "0"; if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf); int use_purge = atoi(purgebuf); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", nyai_bin(), pkg, use_purge ? " --purge" : ""); char out[1024]; int code = run_cmd(cmd, out, sizeof out); if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败"); sbuf_t s = {0}; sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; }