Files
MeowOS-For-R3P2/patch_v2/meowpanel/src/mp_files.c
T

620 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* mp_files.c — 文件域:路径安全/白名单 + /api/files* + multipart 上传解析 + 卡根派生路径。 */
#include "mp.h"
char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
char g_tf_card_buf[2048], g_apps_dir_buf[2048], g_logs_dir_buf[2048], g_notif_log_buf[2048];
const char *g_tf_card = NULL, *g_apps_dir = NULL, *g_logs_dir = NULL, *g_notif_log = NULL;
const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
const char *g_sensitive_names[] = {
"settings.conf", "setting.bak", ".env", "passwd", "shadow",
"meow.conf", "user.ini", "config.json",
};
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
void init_tf_paths(void)
{
const char *env = getenv("MEOW_TF_ROOT");
if (env && env[0]) {
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
size_t l = strlen(g_tf_root_buf);
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
}
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
env = getenv("MEOW_NOTIF_LOG");
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
g_tf_card = g_tf_card_buf;
g_apps_dir = g_apps_dir_buf;
g_logs_dir = g_logs_dir_buf;
g_notif_log = g_notif_log_buf;
}
int tf_present(void)
{
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
FILE *f = fopen("/proc/mounts", "r");
if (!f) return 0;
char line[512];
int present = 0;
while (fgets(line, sizeof line, f)) {
if (strstr(line, TF_ROOT)) { present = 1; break; }
}
fclose(f);
return present;
}
int path_in_whitelist(const char *abs)
{
size_t rl = strlen(TF_ROOT);
if (strncmp(abs, TF_ROOT, rl) != 0) return 0;
if (abs[rl] == 0) return 1; /* 根自身 */
if (abs[rl] != '/') return 0;
const char *rest = abs + rl + 1;
for (int i = 0; i < ALLOWED_COUNT; i++) {
size_t dl = strlen(g_allowed_dirs[i]);
if (strncmp(rest, g_allowed_dirs[i], dl) == 0) {
if (rest[dl] == 0 || rest[dl] == '/') return 1;
}
}
return 0;
}
int is_sensitive_name(const char *base)
{
for (int i = 0; i < SENSITIVE_COUNT; i++)
if (!strcmp(base, g_sensitive_names[i])) return 1;
return 0;
}
int normalize_path(const char *in, char *buf, size_t buflen)
{
size_t rl = strlen(TF_ROOT);
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
char joined[2048];
if (in[0] == '/') {
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
snprintf(joined, sizeof joined, "%s", in);
} else {
/* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */
if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1);
else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in);
else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
}
} else {
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
}
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
char out[2048];
size_t n = 0;
out[0] = '/';
out[1] = 0;
n = 1;
char *p = joined;
while (*p) {
while (*p == '/') p++;
if (!*p) break;
char seg[256];
int k = 0;
while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; }
seg[k] = 0;
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
if (!strcmp(seg, "..")) {
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
if (n <= 1) return -1;
while (n > 1 && out[n - 1] != '/') n--;
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
out[n] = 0;
continue;
}
if (k > 0) {
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
if (need >= sizeof out) return -1;
if (n > 1) { out[n++] = '/'; out[n] = 0; }
memcpy(out + n, seg, (size_t)k);
n += (size_t)k;
out[n] = 0;
}
}
/* 空/根(n==1,仅 "/")→ 卡根 */
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
size_t ol = strlen(out);
if (ol >= buflen) return -1;
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
return 0;
}
void make_rel(const char *abs, char *buf, size_t buflen)
{
size_t rl = strlen(TF_ROOT);
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
const char *rest = abs + rl;
if (*rest == 0) snprintf(buf, buflen, "/");
else snprintf(buf, buflen, "%s", rest);
}
int open_safe_for_read(const char *abs, int follow_rel)
{
(void)follow_rel;
if (!path_in_whitelist(abs)) return -1;
const char *base = strrchr(abs, '/');
base = base ? base + 1 : abs;
if (is_sensitive_name(base)) return -1;
struct stat st;
if (stat(abs, &st) < 0) return -1;
if (!S_ISREG(st.st_mode)) return -1;
return open(abs, O_RDONLY);
}
void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
{
sA(s, "{\"name\":");
sA_str(s, name);
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
isdir ? "dir" : "file", size, mtime);
}
int nat_strcmp(const char *a, const char *b)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
while (*pa && *pb) {
unsigned char ca = *pa, cb = *pb;
if (ca >= '0' && ca <= '9' && cb >= '0' && cb <= '9') {
const unsigned char *na = pa, *nb = pb;
while (*na == '0') na++;
while (*nb == '0') nb++;
const unsigned char *da = na, *db = nb;
while (*da >= '0' && *da <= '9') da++;
while (*db >= '0' && *db <= '9') db++;
int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */
if (la != lb) return la - lb; /* 位数小者数小 */
int c = memcmp(na, nb, (size_t)la);
if (c) return c;
int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */
if (ta != tb) return ta - tb;
pa = da; pb = db;
continue; /* 数字段完全相同,继续 */
}
unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca;
unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb;
if (la != lbc) return (int)la - (int)lbc;
if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */
pa++; pb++;
}
return (int)*pa - (int)*pb;
}
int fent_cmp(const void *a, const void *b)
{
const struct fent *fa = a, *fb = b;
if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir;
return nat_strcmp(fa->name, fb->name);
}
int files_list(int sock, const char *relpath)
{
char abs[1024], rel[1024];
if (normalize_path(relpath, abs, sizeof abs) < 0)
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
if (!path_in_whitelist(abs))
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
struct stat st;
if (stat(abs, &st) < 0)
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
if (!S_ISDIR(st.st_mode))
return send_err(sock, 404, "NOT_FOUND", "非目录");
DIR *d = opendir(abs);
if (!d)
return send_err(sock, 500, "INTERNAL", "读目录失败");
make_rel(abs, rel, sizeof rel);
sbuf_t s = {0};
sA(&s, "{\"root\":");
sA_str(&s, TF_ROOT);
sA(&s, ",\"path\":");
sA_str(&s, abs);
sA(&s, ",\"rel\":");
sA_str(&s, rel);
sA(&s, ",\"allowed\":[");
for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); }
sA(&s, "],\"items\":[");
/* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称
* 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */
int cap = 1024, n = 0;
struct fent *ents = malloc((size_t)cap * sizeof *ents);
struct dirent *e;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
char full[1400];
snprintf(full, sizeof full, "%s/%s", abs, e->d_name);
struct stat fs;
if (stat(full, &fs) < 0) continue;
if (n >= cap) break;
struct fent *en = &ents[n++];
snprintf(en->name, sizeof en->name, "%s", e->d_name);
en->isdir = S_ISDIR(fs.st_mode);
en->size = en->isdir ? 0 : (long long)fs.st_size;
en->mtime = (long long)fs.st_mtime;
}
closedir(d);
qsort(ents, (size_t)n, sizeof *ents, fent_cmp);
for (int i = 0; i < n; i++) {
if (i) sA(&s, ",");
fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime);
}
free(ents);
sA(&s, "]}");
return send_json(sock, 200, s.b ? s.b : "");
}
char *read_text_file_limited(const char *abs, long long *size, int *truncated)
{
int fd = open_safe_for_read(abs, 0);
if (fd < 0) return NULL;
struct stat st;
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
long long total = (long long)st.st_size;
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
char *buf = malloc(cap + 1);
if (!buf) { close(fd); return NULL; }
ssize_t n = read(fd, buf, cap);
close(fd);
if (n < 0) { free(buf); return NULL; }
buf[n] = 0;
/* 二进制检测:含 NUL 或大量控制字符 */
int ctrl = 0;
for (ssize_t i = 0; i < n; i++) {
unsigned char c = (unsigned char)buf[i];
if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */
if (c < 0x09) ctrl++;
}
if (ctrl > n / 10) { free(buf); return (char *)-2; }
if (size) *size = total;
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
return buf;
}
int safe_filename(const char *name, char *out, size_t cap)
{
if (!name || !name[0]) return -1;
const char *b = name;
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
const char *slash = strrchr(b, '/');
const char *bsl = strrchr(b, '\\');
if (bsl && (!slash || bsl > slash)) slash = bsl;
if (slash) b = slash + 1;
if (!b[0]) return -1;
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
for (const char *p = b; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
}
snprintf(out, cap, "%s", b);
return 0;
}
int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
{
/* 取 boundary */
const char *ct = r->content_type;
const char *b = strstr(ct, "boundary=");
if (!b) return -1;
b += 9;
if (*b == '"') { b++; }
char boundary[128];
int bi = 0;
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
boundary[bi] = 0;
char dmark[160];
snprintf(dmark, sizeof dmark, "--%s", boundary);
char emark[160];
snprintf(emark, sizeof emark, "--%s--", boundary);
/* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */
const char *p = r->body;
const char *end = r->body + r->body_len;
int nfiles = 0;
char curpath[1024] = "";
while (p < end) {
const char *fstart = mem_find(p, (size_t)(end - p), dmark);
if (!fstart) break;
fstart += strlen(dmark);
if (*fstart == '-') break; /* 结束标记 --boundary-- */
if (*fstart == '\r' || *fstart == '\n') fstart++;
/* 解析该 part 的头部(直至空行) */
const char *headers_end = strstr(fstart, "\r\n\r\n");
if (!headers_end) break;
char hdr[512];
size_t hl = (size_t)(headers_end - fstart);
if (hl >= sizeof hdr) hl = sizeof hdr - 1;
memcpy(hdr, fstart, hl);
hdr[hl] = 0;
const char *data_start = headers_end + 4;
const char *name = NULL, *fname = NULL;
char *save = NULL;
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
char *l = ln;
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
/* name 与 filename 常同行(name="file"; filename="up.txt")。
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
char *fn = strstr(l, "filename=\"");
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
char *nm = strstr(l, "name=\"");
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
}
}
/* 数据结束:下一个 "--boundary" 之前(二进制安全) */
const char *data_end = mem_find(data_start, (size_t)(end - data_start), dmark);
if (!data_end) data_end = end;
/* 去掉数据尾部 \r\n */
while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--;
if (name && !strcmp(name, "path")) {
size_t dl = (size_t)(data_end - data_start);
if (dl >= sizeof curpath) dl = sizeof curpath - 1;
memcpy(curpath, data_start, dl);
curpath[dl] = 0;
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
size_t dl = (size_t)(data_end - data_start);
char safe[260];
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
/* 目标目录规范化 + 白名单校验 */
char absdir[1024];
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
char joined[1400];
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
char abs[1400];
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write_full(fd, data_start, dl);
close(fd);
nfiles++;
}
/* fd<0(不可写)→ 不计数,跳过该文件 */
}
p = data_end;
}
return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */
}
int api_files_list(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024] = "/";
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
return files_list(sock, path);
}
int api_files_read(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024];
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
char abs[1024];
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
long long size; int trunc;
char *txt = read_text_file_limited(abs, &size, &trunc);
if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取");
if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大");
sbuf_t s = {0};
sA(&s, "{\"path\":");
sA_str(&s, abs);
sA(&s, ",\"content\":");
sA_str(&s, txt);
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
free(txt);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_files_download(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024];
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
char abs[1024];
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
const char *base = strrchr(abs, '/');
base = base ? base + 1 : abs;
if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件");
return send_file(sock, 200, "application/octet-stream", abs, base);
}
int api_files_upload(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0)
return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data");
upfile_t files[16];
memset(files, 0, sizeof files);
int n = parse_multipart(r, files, 16);
if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败");
sbuf_t s = {0};
sA(&s, "{\"item\":[");
int first = 1;
for (int i = 0; i < n && i < 16; i++) {
if (!files[i].filename[0]) continue;
if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */
struct stat st;
if (stat(files[i].path, &st) < 0) continue;
const char *base = strrchr(files[i].path, '/');
base = base ? base + 1 : files[i].path;
if (!first) sA(&s, ",");
first = 0;
sA(&s, "{\"name\":");
sA_str(&s, base);
sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}",
(long long)st.st_size, (long long)st.st_mtime);
}
sA(&s, "]}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int rm_recursive(const char *path)
{
struct stat st;
if (lstat(path, &st) < 0) return -1;
if (S_ISDIR(st.st_mode)) {
DIR *d = opendir(path);
if (!d) return -1;
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", path, e->d_name);
rm_recursive(full);
}
closedir(d);
return rmdir(path);
}
return unlink(path);
}
int api_files_mkdir(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
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) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); }
char abs[1024];
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
struct stat st;
if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); }
if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); }
const char *base = strrchr(abs, '/');
const char *b = base ? base + 1 : abs;
sbuf_t s = {0};
sA(&s, "{\"item\":{\"name\":");
sA_str(&s, b);
sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL));
jv_free(root);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_files_rename(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
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");
jv_t *pnn = jv_obj_get(root, "new_name");
if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); }
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
char abs[1024], newabs[1400];
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
char parent[1024];
snprintf(parent, sizeof parent, "%s", abs);
char *sl = strrchr(parent, '/');
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
*sl = 0;
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
sbuf_t s = {0};
sA(&s, "{\"item\":{\"name\":");
sA_str(&s, pnn->str);
sA(&s, ",\"type\":\"");
struct stat st;
if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file");
sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime);
jv_free(root);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_files_delete(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024];
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
char recursive[8] = "0";
qparam(r->query, "recursive", recursive, sizeof recursive);
char abs[1024];
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根");
struct stat st;
if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在");
if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) {
DIR *d = opendir(abs);
int nonempty = 0;
if (d) {
struct dirent *e;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
nonempty = 1; break;
}
closedir(d);
}
if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)");
}
if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败");
return send_json(sock, 200, "{\"removed\":true}");
}
int multipart_extract_file(const req_t *r, const char *outpath)
{
const char *b = strstr(r->content_type, "boundary=");
if (!b) return -1;
b += 9;
if (*b == '"') b++;
char boundary[128]; int bi = 0;
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
boundary[bi] = 0;
char dmark[160];
snprintf(dmark, sizeof dmark, "--%s", boundary);
const char *p = r->body;
while (p < r->body + r->body_len) {
const char *fs = strstr(p, dmark);
if (!fs) break;
fs += strlen(dmark);
if (*fs == '-') break;
if (*fs == '\r' || *fs == '\n') fs++;
const char *he = strstr(fs, "\r\n\r\n");
if (!he) break;
char hdr[512];
size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1;
memcpy(hdr, fs, hl); hdr[hl] = 0;
int isfile = 0;
char *save = NULL;
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++;
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
if (strstr(l, "name=\"file\"")) isfile = 1;
}
}
const char *ds = he + 4;
const char *de = mem_find(ds, (size_t)((r->body + r->body_len) - ds), dmark);
if (!de) de = r->body + r->body_len;
while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--;
if (isfile) {
int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return -1;
write_full(fd, ds, (size_t)(de - ds));
close(fd);
return 0;
}
p = de;
}
return -1;
}