mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 06:00:23 +02:00
feat(7z): dispatch + archive recognition + password UI
With the extraction engine in place (021c9cb), the /api/extract task
finally picks .7z files up.
* src/filemgr_internal.h -- file_task_t gains extract_password (256
bytes, UTF-8, the size a sane user will never exceed but enough for the
encrypted RAR / 7z passwords the engine actually accepts).
* src/extract.c -- extract_dispatch() routes a single .7z to sevenz_extract()
and a byte-split set (name.7z.001, .002, ...) to the same call (the
volume detector classifies by extension). api_extract() reads the
optional "password" form field and copies it into the task; the error
code map gains ZIPX_ERR_PASSWORD -> "extract_password".
* Makefile -- the sevenz_extract / sevenz_chain / sevenz_volstream sources
join COMMON_SRCS, and every third_party/7z/*.c (LZMA SDK 26.03, public
domain, decoder subset) joins THIRD_PARTY_C_SRCS. The C flag set gains
-Ithird_party/7z and -DZ7_PPMD_SUPPORT, the latter so PPMd is built
in (the SDK guards PPMd sources behind a macro that defaults to off).
* assets/main.js -- isSevenZipArchive + isSevenZipSplitVolume; both feed
isExtractableArchive. actionExtract() now prompts for a password on
7z archives (encrypted-stream support is opt-in -- the engine rejects
unencrypted archives with no password just fine, but the prompt saves
a wasted scan when an archive is encrypted). startExtractTask() takes
the password and sends it as a form field.
* assets/lang-{en,zh}.js -- extractPasswordAsk string for the new prompt.
ZIP 108 / RAR 27 / 7z 26 still all green on the host.
No real-device build attempted in this commit: the WSL runner is the
one that knows the PS5 toolchain, and the user has to be the one to
push it through. The Makefile changes are the wiring the cross
compiler needs; a `make linux` on a Linux host with libmicrohttpd-dev
should link end-to-end.
This commit is contained in:
1 parent
021c9cb339
commit
200b38604f
6 files changed
+80
-22
No files matched your search
@@ -24,7 +24,7 @@ HOST_PKG_CONFIG ?= pkg-config
|
||||
|
||||
BIN := web-file-mgr.elf
|
||||
LINUX_BIN := web-file-mgr-linux
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c src/rar_extract.c src/zipx_volume.c src/zipx_volstream.c
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c src/rar_extract.c src/zipx_volume.c src/zipx_volstream.c src/zipx_common.c src/sevenz_extract.c src/sevenz_chain.c src/sevenz_volstream.c
|
||||
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c src/cpu_support_stub.c
|
||||
LINUX_SRCS := $(COMMON_SRCS)
|
||||
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
|
||||
@@ -70,10 +70,10 @@ UNRAR7_SRCS := \
|
||||
third_party/unrar7/timefn.cpp third_party/unrar7/ui.cpp third_party/unrar7/unicode.cpp \
|
||||
third_party/unrar7/unpack.cpp third_party/unrar7/volume.cpp
|
||||
|
||||
THIRD_PARTY_C_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c)
|
||||
THIRD_PARTY_C_FLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include \
|
||||
THIRD_PARTY_C_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c) $(wildcard third_party/7z/*.c)
|
||||
THIRD_PARTY_C_FLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -Ithird_party/7z \
|
||||
-DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE \
|
||||
-DHAVE_FSEEKO
|
||||
-DHAVE_FSEEKO -DZ7_PPMD_SUPPORT
|
||||
UNRAR7_CXX_FLAGS := -O2 -w -std=c++17 -DRARDLL -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
|
||||
# prospero-clang++ defaults to -stdlib=libc++; state it explicitly for clarity.
|
||||
UNRAR7_CXX_FLAGS_PS5 := $(UNRAR7_CXX_FLAGS) -stdlib=libc++
|
||||
@@ -84,12 +84,12 @@ PS5_TP_OBJS := $(patsubst %.c,ps5-obj/%.o,$(THIRD_PARTY_C_SRCS)) \
|
||||
LINUX_TP_OBJS := $(patsubst %.c,linux-obj/%.o,$(THIRD_PARTY_C_SRCS)) \
|
||||
$(patsubst %.cpp,linux-obj/%.o,$(UNRAR7_SRCS))
|
||||
|
||||
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LDFLAGS := -Wl,--gc-sections
|
||||
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
|
||||
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ window.WFM_LANG = {
|
||||
extracting: "Extracting",
|
||||
extractConfirm: "Extract {name} to {path}?",
|
||||
extractOverwriteAsk: "If a file or folder with the same name already exists in the target:\n\nOK = overwrite same-name files (folders still merge)\nCancel = fail if the target already exists",
|
||||
extractPasswordAsk: "This archive may be encrypted (e.g. 7zAES).\n\nEnter the password to extract, or leave it empty to try without one.",
|
||||
extractUploadConfirm: "Upload and extract {name}?\n\nTarget folder: {path}\nThe uploaded archive will be deleted after success.",
|
||||
extractStarted: "Extraction started: {name}",
|
||||
extractDone: "Extraction complete: {name}",
|
||||
|
||||
@@ -101,6 +101,7 @@ window.WFM_LANG = {
|
||||
extracting: "解压",
|
||||
extractConfirm: "解压 {name} 到 {path}?",
|
||||
extractOverwriteAsk: "若目标已存在同名文件或目录:\n\n确定 = 覆盖同名文件(目录仍会合并)\n取消 = 若目标已存在则失败",
|
||||
extractPasswordAsk: "此压缩包可能加密了(如 7zAES)。\n\n输入密码后解压,留空则尝试无密码解压。",
|
||||
extractUploadConfirm: "上传并解压 {name}?\n\n目标目录:{path}\n成功后将删除上传的压缩包。",
|
||||
extractStarted: "已开始解压 {name}",
|
||||
extractDone: "解压完成:{name}",
|
||||
|
||||
+29
-4
@@ -784,11 +784,25 @@ function isZipSplitVolume(item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSevenZipArchive(item) {
|
||||
return item.type === "-" && /\.7z$/i.test(item.name);
|
||||
}
|
||||
|
||||
function isSevenZipSplitVolume(item) {
|
||||
if (item.type !== "-") return false;
|
||||
// Byte-split 7z sets: name.7z.001 / .002 / ... — any volume is enough,
|
||||
// the engine walks the directory to assemble the rest.
|
||||
if (/\.7z\.0*\d+$/i.test(item.name)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExtractableArchive(item) {
|
||||
if (item.type !== "-") return false;
|
||||
if (/\.zip$/i.test(item.name)) return true;
|
||||
if (isZipSplitVolume(item)) return true;
|
||||
if (isRarMainVolume(item)) return true;
|
||||
if (isSevenZipArchive(item)) return true;
|
||||
if (isSevenZipSplitVolume(item)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -835,18 +849,20 @@ function actionInstallSelectedPkgs() {
|
||||
return queuePkgInstall(selectedEntries().filter(isPkgPackage));
|
||||
}
|
||||
|
||||
async function startExtractTask(path, dstDir, conflict, removeSource, name, large) {
|
||||
async function startExtractTask(path, dstDir, conflict, removeSource, name, large, password) {
|
||||
try {
|
||||
taskRefreshPath = cwd;
|
||||
setBusy(true);
|
||||
setStatus(t("extractStarted", { name }));
|
||||
const data = await apiForm("/api/extract", {
|
||||
const form = {
|
||||
path,
|
||||
dst_dir: dstDir,
|
||||
conflict,
|
||||
remove_source: removeSource ? "1" : "0",
|
||||
large: large ? "1" : "0"
|
||||
});
|
||||
};
|
||||
if (password) form.password = password;
|
||||
const data = await apiForm("/api/extract", form);
|
||||
trackTask(data.task_id, "extract", false);
|
||||
clearSelection(false);
|
||||
await pollTasks();
|
||||
@@ -880,7 +896,16 @@ function actionExtract() {
|
||||
if (!confirm(t("extractConfirm", { name: displayName(item), path: displayPath(cwd) }))) return;
|
||||
const conflict = confirm(t("extractOverwriteAsk")) ? "overwrite" : "fail";
|
||||
const large = shouldPromptLargeMode(item.size) ? promptLargeMode(item.size) : false;
|
||||
startExtractTask(item.path, cwd, conflict, false, displayName(item), large);
|
||||
// 7z archives can be encrypted (7zAES); ask up front so an unprotected
|
||||
// archive doesn't pay a wasted scan + folder parse. An empty submission is
|
||||
// fine — the engine returns ZIPX_ERR_PASSWORD and the user retries.
|
||||
let password = "";
|
||||
if (isSevenZipArchive(item) || isSevenZipSplitVolume(item)) {
|
||||
const asked = prompt(t("extractPasswordAsk"), "");
|
||||
if (asked === null) return;
|
||||
password = asked;
|
||||
}
|
||||
startExtractTask(item.path, cwd, conflict, false, displayName(item), large, password);
|
||||
}
|
||||
|
||||
function openImagePreview(item) {
|
||||
|
||||
+38
-12
@@ -15,6 +15,7 @@
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "rar_extract.h"
|
||||
#include "sevenz_extract.h"
|
||||
#include "zip_extract.h"
|
||||
#include "zipx_volume.h"
|
||||
|
||||
@@ -184,10 +185,11 @@ extract_dispatch(file_task_t *task, zipx_conflict_t conflict,
|
||||
"(rename the parts to 'x.part1.rar', 'x.part2.rar', ...)");
|
||||
status = ZIPX_ERR_UNSUPPORTED;
|
||||
} else if(kind == 2) {
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"7z archives are not supported yet (repack as .zip or .rar)");
|
||||
status = ZIPX_ERR_UNSUPPORTED;
|
||||
status = sevenz_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task,
|
||||
task->extract_password[0] ? task->extract_password
|
||||
: NULL,
|
||||
result);
|
||||
} else {
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
@@ -206,9 +208,16 @@ extract_dispatch(file_task_t *task, zipx_conflict_t conflict,
|
||||
return rar_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task, result);
|
||||
}
|
||||
if(ends_with_ci(task->src, ".7z")) {
|
||||
return sevenz_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task,
|
||||
task->extract_password[0] ? task->extract_password
|
||||
: NULL,
|
||||
result);
|
||||
}
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"unsupported archive format (only .zip and .rar are accepted)");
|
||||
"unsupported archive format (only .zip, .rar and .7z are accepted)");
|
||||
return ZIPX_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
@@ -231,6 +240,7 @@ extract_error_code(zipx_status_t status) {
|
||||
case ZIPX_ERR_SPACE: return "no_space";
|
||||
case ZIPX_ERR_IO: return "extract_io";
|
||||
case ZIPX_ERR_CRC: return "extract_crc";
|
||||
case ZIPX_ERR_PASSWORD: return "extract_password";
|
||||
default: return "extract_failed";
|
||||
}
|
||||
}
|
||||
@@ -317,6 +327,7 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
char *conflict_str = body_form_value(body, body_size, "conflict");
|
||||
char *remove_str = body_form_value(body, body_size, "remove_source");
|
||||
char *large_str = body_form_value(body, body_size, "large");
|
||||
char *password_str = body_form_value(body, body_size, "password");
|
||||
extract_conflict_t conflict = EXTRACT_CONFLICT_FAIL;
|
||||
int remove_source = remove_str && !strcmp(remove_str, "1");
|
||||
int large = large_str && !strcmp(large_str, "1");
|
||||
@@ -325,7 +336,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
struct stat st;
|
||||
|
||||
if(!path || !dst_dir || !path[0] || !dst_dir[0]) {
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(conflict_str) {
|
||||
@@ -334,23 +346,27 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
} else if(!strcmp(conflict_str, "merge")) {
|
||||
conflict = EXTRACT_CONFLICT_MERGE;
|
||||
} else if(strcmp(conflict_str, "fail")) {
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid conflict");
|
||||
}
|
||||
}
|
||||
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "file not found");
|
||||
}
|
||||
if(stat(dst_dir, &st) || !S_ISDIR(st.st_mode)) {
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"destination must be a directory");
|
||||
}
|
||||
|
||||
task = calloc(1, sizeof(*task));
|
||||
if(!task) {
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"out of memory");
|
||||
}
|
||||
@@ -360,6 +376,14 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
task->extract_conflict = (int)conflict;
|
||||
task->extract_remove_source = remove_source;
|
||||
task->extract_large = large;
|
||||
/* The size cap (256 bytes, including the NUL) leaves room for a 255-codepoint
|
||||
UTF-8 password without overflowing the field or letting a malicious header
|
||||
run away with it. Anything longer is truncated, which is what a sane user
|
||||
will never hit but matches the storage size of the field. */
|
||||
if(password_str) {
|
||||
snprintf(task->extract_password, sizeof(task->extract_password), "%s",
|
||||
password_str);
|
||||
}
|
||||
snprintf(task->src, sizeof(task->src), "%s", path);
|
||||
snprintf(task->dst, sizeof(task->dst), "%s", dst_dir);
|
||||
task->created_at = time(NULL);
|
||||
@@ -370,7 +394,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
if(has_active_task_locked()) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
free_task(task);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
task->id = g_next_task_id++;
|
||||
@@ -384,7 +409,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
pthread_detach(task->thread);
|
||||
}
|
||||
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str);
|
||||
free(large_str); free(password_str);
|
||||
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
@@ -59,6 +59,11 @@ typedef struct file_task {
|
||||
int extract_conflict;
|
||||
int extract_remove_source;
|
||||
int extract_large;
|
||||
/* UTF-8 password for archives that encrypt their streams (7zAES, RAR5 AES).
|
||||
Empty means "try without one"; the engine returns ZIPX_ERR_PASSWORD for
|
||||
an archive that needs one, and the web UI prompts and retries. The
|
||||
length is bounded so a runaway header field cannot overflow task memory. */
|
||||
char extract_password[256];
|
||||
unsigned long long speed_sample_done;
|
||||
struct timespec speed_sample_time;
|
||||
task_eta_sample_t eta_samples[ETA_SAMPLE_SLOTS];
|
||||
|
||||
Reference in new issue
Block a user