243 lines
10 KiB
C
243 lines
10 KiB
C
/* 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;
|
||
}
|