Compare commits

...
1 Commits
Author SHA1 Message Date
owendswang a61ac54e1f Added PKG install function 2026-08-06 14:33:02 +08:00
12 changed files with 275 additions and 15 deletions

No files matched your search

+2 -2
View File
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
endif
endif
VERSION_TAG := v1.1
VERSION_TAG := v1.2
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
@@ -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
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
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
LINUX_SRCS := $(COMMON_SRCS)
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
+1
View File
@@ -38,6 +38,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
- Home screen launcher icon and browser favicon use the same embedded `icon0.png` data in the PS5 build to avoid storing the icon twice in the ELF.
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`, `.shn`.
- Preview image files ending with `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
- Install PKG files ends with `.pkg`.
## Build
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -43,6 +43,7 @@
<button id="renameBtn" data-i18n="rename"></button>
<button id="downloadBtn" class="remote-only" data-i18n="download"></button>
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="installPkgBtn" class="install-action" data-i18n="install" hidden></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
+9
View File
@@ -19,7 +19,13 @@ window.WFM_LANG = {
refresh: "Refresh",
mkdir: "New Folder",
newText: "New Text",
install: "Install",
installPackage: "Install package",
pkgInstalling: "Starting package installation: {name}...",
pkgInstallStarted: "Package installation started: {name}",
file: "File",
textFile: "Text",
image: "Image",
dir: "Folder",
parent: "Parent",
name: "Name",
@@ -118,6 +124,9 @@ window.WFM_LANG = {
err_source_destination_same: "Source and destination are the same",
err_destination_inside_source: "Cannot copy or move a folder inside itself",
err_invalid_path: "Invalid path",
err_pkg_type_invalid: "Only .pkg files can be installed",
err_pkg_install_failed: "Package installation failed ({arg})",
err_pkg_install_unsupported: "Package installation is only available on PS5",
err_file_not_found: "File not found",
err_file_already_exists: "A file with the same name already exists",
err_invalid_method: "Invalid request method",
+9
View File
@@ -19,7 +19,13 @@ window.WFM_LANG = {
refresh: "刷新",
mkdir: "新建目录",
newText: "新建文本",
install: "安装",
installPackage: "安装 PKG",
pkgInstalling: "正在提交安装:{name}...",
pkgInstallStarted: "已开始安装:{name}",
file: "文件",
textFile: "文本",
image: "图片",
dir: "目录",
parent: "上级目录",
name: "名称",
@@ -118,6 +124,9 @@ window.WFM_LANG = {
err_source_destination_same: "源和目标相同",
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
err_invalid_path: "路径无效",
err_pkg_type_invalid: "只能安装 .pkg 文件",
err_pkg_install_failed: "PKG 安装失败({arg})",
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
err_file_not_found: "文件不存在",
err_file_already_exists: "同名文件已存在",
err_invalid_method: "请求方式无效",
+13 -2
View File
@@ -329,7 +329,7 @@ button.primary:hover {
}
.paste-action {
max-width: 480px;
max-width: 360px;
}
.paste-action > * {
@@ -341,12 +341,23 @@ button.primary:hover {
display: inline-block;
flex: 1 1 auto;
min-width: 0;
max-width: 180px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-action {
min-width: 88px;
border-color: #8c6425;
background: #352b1c;
color: #f8dfae;
}
button.install-action:hover {
background: #433521;
}
.paste-count {
white-space: nowrap;
}
+94 -9
View File
@@ -18,6 +18,7 @@ let directoryLoadingStartedAt = 0;
let taskOverlayTimer = 0;
let lastCompletionId = null;
let taskPollFailedAlertShown = false;
let localActionBusy = false;
let textEditorPath = null;
let textEditorVersion = null;
let textEditorOriginal = "";
@@ -26,7 +27,7 @@ let downloadFrame = null;
let uploadXhr = null;
let L = {};
const APP_VERSION = "v1.1";
const APP_VERSION = "v1.2";
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
const SORT_KEY = "ps5-web-file-mgr:list-sort";
const LOADING_DISPLAY_DELAY = 250;
@@ -54,6 +55,7 @@ const pasteVerbEl = document.getElementById("pasteVerb");
const pasteNameEl = document.getElementById("pasteName");
const pasteCountEl = document.getElementById("pasteCount");
const pasteTargetTextEl = document.getElementById("pasteTargetText");
const installPkgBtn = document.getElementById("installPkgBtn");
const clearClipboardBtn = document.getElementById("clearClipboardBtn");
const downloadBtn = document.getElementById("downloadBtn");
const uploadMenuEl = document.getElementById("uploadMenu");
@@ -559,6 +561,7 @@ function pathIsSameOrChild(parent, child) {
}
function typeLabel(type) {
if (type === "parent") return t("parent");
return type === "d" ? t("dir") : t("file");
}
@@ -571,6 +574,54 @@ function isPreviewableImage(item) {
return item.type === "-" && /\.(png|jpe?g|gif|bmp|webp)$/i.test(item.name);
}
function isPkgPackage(item) {
return item.type === "-" && /\.pkg$/i.test(item.name);
}
function itemTypeLabel(item) {
if (item.type === "parent" || item.type === "d") return typeLabel(item.type);
if (isPkgPackage(item)) return "PKG";
if (isPreviewableImage(item)) return t("image");
if (isEditableText(item)) return t("textFile");
return t("file");
}
async function installPkg(item) {
if (busy || loadingPath) return;
try {
localActionBusy = true;
setBusy(true);
setStatus(t("pkgInstalling", { name: displayName(item) }));
await api("/api/install-pkg", { path: item.path });
setStatus(t("pkgInstallStarted", { name: displayName(item) }));
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
}
}
async function actionInstallSelectedPkgs() {
if (busy || loadingPath) return;
const items = selectedEntries().filter(isPkgPackage);
if (!items.length) return;
try {
localActionBusy = true;
setBusy(true);
for (let i = 0; i < items.length; i++) {
setStatus(t("pkgInstalling", { name: displayName(items[i]) }));
await api("/api/install-pkg", { path: items[i].path });
}
setStatus(t("pkgInstallStarted", { name: itemTitle(items) }));
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
}
}
function openImagePreview(item) {
if (busy) return;
imagePreviewNameEl.textContent = displayName(item);
@@ -708,6 +759,17 @@ function resetScrollTop() {
if (window.requestAnimationFrame) requestAnimationFrame(apply);
}
function revealPathInList(path) {
const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]");
if (!row) return;
const top = row.offsetTop;
const maxScroll = contentEl.scrollHeight - contentEl.clientHeight;
if (maxScroll <= 0) return;
const target = top - Math.floor((contentEl.clientHeight - row.offsetHeight) / 2);
contentEl.scrollTop = Math.max(0, Math.min(maxScroll, target));
}
function clipboardTitle() {
if (!clipboard || clipboard.items.length === 0) return "";
return itemTitle(clipboard.items);
@@ -740,7 +802,7 @@ function compareEntries(a, b) {
let result = 0;
if (sortKey === "name") result = compareText(displayName(a), displayName(b));
else if (sortKey === "type") result = compareText(typeLabel(a.type), typeLabel(b.type));
else if (sortKey === "type") result = compareText(itemTypeLabel(a), itemTypeLabel(b));
else if (sortKey === "size") result = (a.type === "d" ? 0 : Number(a.size || 0)) -
(b.type === "d" ? 0 : Number(b.size || 0));
else if (sortKey === "mtime") result = Number(a.mtime || 0) - Number(b.mtime || 0);
@@ -804,6 +866,19 @@ function renderClipboard() {
clearClipboardBtn.disabled = locked;
}
function renderInstallPkgButton(items, locked) {
const pkgs = items.filter(isPkgPackage);
installPkgBtn.hidden = pkgs.length === 0;
if (!pkgs.length) {
installPkgBtn.title = "";
installPkgBtn.disabled = true;
return;
}
const title = itemTitle(pkgs);
installPkgBtn.title = t("installPackage") + ": " + title;
installPkgBtn.disabled = locked;
}
function singleSelected() {
const items = selectedEntries();
return items.length === 1 ? items[0] : null;
@@ -826,6 +901,7 @@ function updateButtons() {
selectAllEl.disabled = locked;
selectAllEl.checked = entries.length > 0 && items.length === entries.length;
selectAllEl.indeterminate = items.length > 0 && items.length < entries.length;
renderInstallPkgButton(items, locked);
renderClipboard();
}
@@ -1006,6 +1082,7 @@ function render() {
const iconImg = document.createElement("img");
iconImg.className = "icon";
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" :
isPkgPackage(item) ? "/icon-pkg.png" :
isPreviewableImage(item) ? "/icon-image.png" :
isEditableText(item) ? "/icon-file.png" : "/icon-generic.png";
iconImg.alt = "";
@@ -1017,7 +1094,7 @@ function render() {
tr,
selectTd,
nameTd,
classCell("type-col", isParent ? t("parent") : item.type === "d" ? t("dir") : t("file")),
classCell("type-col", itemTypeLabel(item)),
classCell("size-col", formatSize(item.size, item.type)),
classCell("time-col", formatTime(item.mtime)),
classCell("mode-col", isParent ? "" : "0" + Number(item.mode).toString(8))
@@ -1164,10 +1241,10 @@ function validatePasteTarget() {
return {
ok: false,
message: t("removeConflictFirst", {
existingType: typeLabel(existing.type),
existingType: itemTypeLabel(existing),
name: item.name,
label: clipboard.op === "move" ? t("move") : t("copy"),
sourceType: typeLabel(item.type)
sourceType: itemTypeLabel(item)
})
};
}
@@ -1334,7 +1411,7 @@ function renderTasks(tasks) {
DOWNLOAD_OVERLAY_DISPLAY_DELAY : LOADING_DISPLAY_DELAY);
else {
hideTaskOverlay();
setBusy(false);
if (!localActionBusy) setBusy(false);
}
if (!hasActive) return;
@@ -1436,8 +1513,11 @@ async function pollTasks() {
}));
}
const wasBusy = busy;
const wasTaskBusy = Boolean(trackedTask || pendingOverlayText ||
!overlayEl.hidden || taskOverlayTimer);
renderTasks(tasks);
const becameIdle = wasBusy && !busy;
const taskBecameIdle = wasTaskBusy && !busy;
let shouldRefresh = false;
let sawTrackedTask = false;
for (const task of tasks) {
@@ -1458,9 +1538,9 @@ async function pollTasks() {
shouldRefresh = true;
clearTrackedTask();
}
if (becameIdle && !tasks.length) shouldRefresh = true;
if (taskBecameIdle && !tasks.length) shouldRefresh = true;
if (shouldRefresh) {
if (becameIdle) startContentLoadingTimer();
if (taskBecameIdle) startContentLoadingTimer();
clearSelection(false);
await load(taskRefreshPath || cwd, false);
await refreshSpaces();
@@ -1690,6 +1770,7 @@ document.getElementById("mkdirBtn").addEventListener("click", () => {
document.getElementById("copyBtn").addEventListener("click", actionCopy);
document.getElementById("moveBtn").addEventListener("click", actionMove);
pasteBtn.addEventListener("click", actionPaste);
installPkgBtn.addEventListener("click", actionInstallSelectedPkgs);
clearClipboardBtn.addEventListener("click", clearClipboard);
document.getElementById("renameBtn").addEventListener("click", actionRename);
downloadBtn.addEventListener("click", actionDownload);
@@ -1755,10 +1836,14 @@ filesEl.addEventListener("click", event => {
};
focusPath(item.path);
if (item.type === "parent" || item.type === "d") {
const revealPath = item.type === "parent" ? cwd : "";
setTimeout(async () => {
if (!(await load(item.path, undefined, false, true))) await load(cwd, false, true);
if (await load(item.path, item.type === "parent" ? false : undefined, false, true)) {
/* if (revealPath) revealPathInList(revealPath); */
} else await load(cwd, false, true);
}, 0);
}
else if (isPkgPackage(item)) installPkg(item);
else if (isPreviewableImage(item)) openImagePreview(item);
else if (isEditableText(item)) openTextEditor(item);
else togglePath(item.path, !selected.has(item.path));
+2 -2
View File
@@ -9,6 +9,7 @@
#include <ps5/kernel.h>
#include "asset.h"
#include "pkg_installer.h"
#define INCASSET(name, file) \
__asm__(".section .rodata\n" \
@@ -28,7 +29,6 @@
INCASSET(param_json, "assets/param.json");
INCASSET(icon0_png, "assets/icon0.png");
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void *);
static int
@@ -111,7 +111,7 @@ app_install_if_needed(void) {
printf("Installing launcher app %s\n", title_id);
if((err = sceAppInstUtilInitialize())) {
if((err = pkg_installer_initialize())) {
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
return -1;
}
+42
View File
@@ -18,6 +18,7 @@
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
#include "pkg_installer.h"
#include "websrv.h"
#define COPY_BUFFER_SIZE (8 * 1024 * 1024)
@@ -2008,6 +2009,42 @@ api_mkdir(struct MHD_Connection *conn) {
: send_json_ok(conn);
}
static enum MHD_Result
api_install_pkg(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
const char *extension;
struct stat st;
char error_code[16];
int result;
if(!path || !(extension = strrchr(path, '.')) ||
strcasecmp(extension, ".pkg")) {
free(path);
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
"file is not a PKG package",
"pkg_type_invalid", NULL);
}
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
result = pkg_installer_install(path);
free(path);
if(result == PKG_INSTALL_UNSUPPORTED) {
return send_json_error_detail(conn, MHD_HTTP_NOT_IMPLEMENTED,
"package installation is only available on PS5",
"pkg_install_unsupported", NULL);
}
if(result) {
snprintf(error_code, sizeof(error_code), "0x%08X", (unsigned int)result);
return send_json_error_detail(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"package installation failed",
"pkg_install_failed", error_code);
}
return send_json_ok(conn);
}
enum MHD_Result
filemgr_api_request(struct MHD_Connection *conn, const char *url,
const char *method, const char *body, size_t body_size) {
@@ -2029,6 +2066,11 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url,
if(!strcmp(url, "/api/upload/finish")) return api_upload_finish(conn);
if(!strcmp(url, "/api/rename")) return api_rename(conn);
if(!strcmp(url, "/api/mkdir")) return api_mkdir(conn);
if(!strcmp(url, "/api/install-pkg")) {
return strcmp(method, MHD_HTTP_METHOD_POST) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_install_pkg(conn);
}
if(!strcmp(url, "/api/text")) {
return strcmp(method, MHD_HTTP_METHOD_GET) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
+96
View File
@@ -0,0 +1,96 @@
#include "pkg_installer.h"
#ifndef __linux__
#include <pthread.h>
typedef struct pkg_metadata {
const char *uri;
const char *ex_uri;
const char *playgo_scenario_id;
const char *content_id;
const char *content_name;
const char *icon_url;
} pkg_metadata_t;
typedef struct pkg_info {
char content_id[48];
int type;
int platform;
} pkg_info_t;
typedef struct playgo_info {
char lang[8][30];
char scenario_ids[3][64];
char content_ids[64];
long unknown[810];
} playgo_info_t;
int sceAppInstUtilInitialize(void);
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
playgo_info_t *);
static pthread_mutex_t installer_lock = PTHREAD_MUTEX_INITIALIZER;
static int installer_initialized;
static int
initialize_locked(void) {
int result;
if(!installer_initialized) {
result = sceAppInstUtilInitialize();
if(result) return result;
installer_initialized = 1;
}
return 0;
}
int
pkg_installer_initialize(void) {
int result;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
pthread_mutex_unlock(&installer_lock);
return result;
}
int
pkg_installer_install(const char *path) {
pkg_metadata_t metadata = {
.uri = path,
.ex_uri = "",
.playgo_scenario_id = "",
.content_id = "",
.content_name = "",
.icon_url = ""
};
pkg_info_t pkg_info = {0};
playgo_info_t playgo_info = {0};
int result;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
if(result) {
pthread_mutex_unlock(&installer_lock);
return result;
}
result = sceAppInstUtilInstallByPackage(&metadata, &pkg_info, &playgo_info);
pthread_mutex_unlock(&installer_lock);
return result;
}
#else
int
pkg_installer_initialize(void) {
return PKG_INSTALL_UNSUPPORTED;
}
int
pkg_installer_install(const char *path) {
(void)path;
return PKG_INSTALL_UNSUPPORTED;
}
#endif
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
int pkg_installer_initialize(void);
int pkg_installer_install(const char *path);