Compare commits

..
11 Commits
Author SHA1 Message Date
owendswang 1a24160b8b Release v0.9 2026-07-19 00:20:15 +08:00
owendswang 3696468611 Fixed display for available spacee of interal M.2 2026-07-18 09:58:05 +08:00
owendswang 777bb4abd4 Release v0.8 2026-07-14 18:49:24 +08:00
owendswang 76f8fe21d2 Release v0.7 2026-07-09 20:09:02 +08:00
owendswang 5a91067c2e Release v0.6 2026-06-24 01:19:18 +08:00
owendswang ba0704d48c Release v0.5 2026-06-22 02:33:23 +08:00
owendswang 4e10dee5fb Update README.md 2026-06-20 23:45:21 +08:00
owendswang 259d0a6434 Release v0.4 2026-06-20 23:23:25 +08:00
owendswang 6f46847df2 Release v0.3 2026-06-19 13:23:00 +08:00
owendswang 91375e0537 Added screenshots 2026-06-18 23:05:34 +08:00
owendswang f460178ac8 Release v0.2 2026-06-17 23:57:33 +08:00
23 changed files with 3464 additions and 600 deletions

No files matched your search

+4 -1
View File
@@ -1,4 +1,7 @@
gen/
web-file-mgr.elf
*.elf
web-file-mgr-linux
*.o
*.d
tools/
test/
+48 -12
View File
@@ -1,43 +1,79 @@
ifdef PS5_PAYLOAD_SDK
ifneq ($(filter-out linux linux-deps clean,$(MAKECMDGOALS)),)
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
else
$(error PS5_PAYLOAD_SDK is undefined)
endif
endif
ifeq ($(MAKECMDGOALS),)
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
$(error PS5_PAYLOAD_SDK is undefined)
endif
endif
VERSION_TAG := v0.1
VERSION_TAG := v0.9
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
HOST_CC ?= cc
HOST_STRIP ?= strip
HOST_PKG_CONFIG ?= pkg-config
BIN := web-file-mgr.elf
SRCS := src/main.c src/websrv.c src/filemgr.c src/asset.c src/mime.c
SRCS += src/app_installer.c src/notify.c
ASSETS := $(wildcard assets/*)
GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c))
BIN := web-file-mgr.elf
LEGACY_BIN := web-file-mgr-legacy.elf
LINUX_BIN := web-file-mgr-linux
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/asset.c src/mime.c src/notify.c
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
LINUX_SRCS := $(COMMON_SRCS)
ASSETS := $(wildcard assets/*)
GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c))
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
LDFLAGS := -Wl,--gc-sections
LEGACY_CFLAGS := $(filter-out -ffunction-sections -fdata-sections,$(CFLAGS))
LEGACY_LDFLAGS :=
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
LDADD += -lSceIpmi -lSceAppInstUtil
LINUX_CFLAGS := -O2 -Wall -Werror -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
.PHONY: all deps clean
.PHONY: all legacy linux deps linux-deps clean
all: deps $(BIN)
legacy: deps $(LEGACY_BIN)
linux: linux-deps $(LINUX_BIN)
deps:
@$(PKG_CONFIG) --exists libmicrohttpd || ./install-libmicrohttpd.sh
linux-deps:
@$(HOST_PKG_CONFIG) --exists libmicrohttpd || \
(echo "libmicrohttpd development package is required for make linux" >&2; exit 1)
gen:
mkdir gen
clean:
rm -rf $(BIN) gen
rm -rf $(BIN) $(LEGACY_BIN) $(LINUX_BIN) gen
gen/%.c: assets/% gen-asset-module.py gen
gen/%.c: assets/% gen-asset-module.py | gen
$(PYTHON) gen-asset-module.py --path $* $< > $@
$(BIN): $(SRCS) $(GEN_SRCS)
$(BIN): $(PS5_SRCS) $(GEN_SRCS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDADD)
$(STRIP) $@
$(LEGACY_BIN): $(PS5_SRCS) $(GEN_SRCS)
$(CC) $(LEGACY_CFLAGS) $(LEGACY_LDFLAGS) -o $@ $^ $(LDADD)
$(STRIP) $@
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS)
$(HOST_CC) $(LINUX_CFLAGS) -o $@ $^ $(LINUX_LDADD)
$(HOST_STRIP) $@
+22 -2
View File
@@ -5,12 +5,24 @@ safely copying game dump folders from USB storage to internal storage.
## Brief
PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, installs a home screen launcher on startup when needed, and provides file operations from the PS5 browser. If `8888` is already in use, the payload tries the next port until one is available; the startup notification shows the actual listen port.
PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, installs a home screen launcher in Media catagory on startup when needed, and provides file operations from the PS5 browser. If `8888` is already in use, the payload tries the next port until one is available; the startup notification shows the actual listen port.
## Screenshots
<p>
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="31%" alt="PS5 Web File Manager screenshot 1"></a>
<a href="docs/screenshots/20260619_131432.399.jpg" target="_blank"><img src="docs/screenshots/20260619_131432.399.jpg" width="31%" alt="PS5 Web File Manager screenshot 2"></a>
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 Web File Manager screenshot 3"></a>
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 Web File Manager screenshot 4"></a>
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 Web File Manager screenshot 5"></a>
<a href="docs/screenshots/20260620_232728.533.jpg" target="_blank"><img src="docs/screenshots/20260620_232728.533.jpg" width="31%" alt="PS5 Web File Manager screenshot 6"></a>
</p>
## Features
- List files and folders.
- Copy, move, delete, rename, and create folders.
- Edit UTF-8 text files up to 1 MiB using the built-in plain text editor.
- Multi-select operations.
- Copy/move by choosing sources first, then pasting or moving them into the current folder.
- Conflict prompts for overwriting files and merging folders.
@@ -18,6 +30,8 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
- Copied/moved files and folders are set to `0777` where the filesystem supports Unix permissions. FAT/exFAT-style filesystems may ignore chmod.
- Chinese and English UI. The browser language is read from `navigator.languages` / `navigator.language`; Chinese uses `zh`, everything else uses English.
- Startup notification showing the app name, version, and listen port.
- 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`.
- Preview image files ending with `.png,`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
## Build
@@ -69,7 +83,7 @@ After the payload starts, the PS5 notification shows the app name, version, and
http://${PS5_IP_ADDRESS}:8888/
```
On first startup the payload installs a `PS5 Web File Manager` web shortcut on the PS5 home screen when needed. If the payload had to use a fallback port such as `8889`, use the port shown in the startup notification.
On first startup the payload installs a `PS5 Web File Manager` web shortcut in the Media category when needed. If the payload had to use a fallback port such as `8889`, use the port shown in the startup notification.
## Notes
@@ -77,6 +91,12 @@ On first startup the payload installs a `PS5 Web File Manager` web shortcut on t
- Delete is recursive and permanent. There is no recycle bin.
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting existing files when merging into an existing target folder.
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
## FAQ
- This is a homebrew app which does not affect system process or memory. It should crash itself instead of kernel panic theoretically. If kernel panic happens, make sure you'r using the latest jailbreak methods which includes latest elfldr. Or switch back to the stable method you have been using.
- If you're using P2JB, I suggest not to use this if kernel panic happened. It is suffering to wait another 50 min because of kernel panic. Stability first for P2JB.
- The preparing stage is calculating the folder size. It would be pretty slow if there are too many files in this folder. It make sure if there is enough available space for this folder you were trying to copy/move.
## Credits
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

+52 -10
View File
@@ -4,13 +4,34 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PS5 Web File Manager</title>
<style>
html, body { margin: 0; min-height: 100%; background: #111316; color: #edf0f2; }
.init-loading {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
background: #111316;
color: #aab4be;
font: 18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
</style>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<main class="shell">
<header class="topbar">
<div id="path" class="path">/</div>
<div id="spaceInfo" class="space-info"></div>
<div class="top-left">
<div id="path" class="path">/</div>
</div>
<div class="top-right">
<div id="spaceInfo" class="space-info"></div>
</div>
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
</header>
@@ -22,13 +43,14 @@
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
<span id="pasteTargetText" data-i18n="pasteToCurrent"></span>
</button>
<button id="clearClipboardBtn" data-i18n="cancel" hidden></button>
</div>
<div class="tool-right">
<button id="refreshBtn" data-i18n="refresh"></button>
<button id="newTextBtn" data-i18n="newText"></button>
<button id="mkdirBtn" data-i18n="mkdir"></button>
</div>
</section>
@@ -48,6 +70,9 @@
<tbody id="files"></tbody>
</table>
<div id="empty" class="empty" data-i18n="empty" hidden></div>
<div id="contentLoading" class="content-loading" hidden>
<div class="content-loading-text" data-i18n="readDir"></div>
</div>
</section>
<footer class="status">
@@ -56,19 +81,36 @@
</footer>
</main>
<div id="textEditorOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel">
<div id="textEditorPath" class="text-editor-path"></div>
<textarea id="textEditor" class="text-editor" wrap="off" spellcheck="false"
autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
<div id="textEditorStatus" class="text-editor-status"></div>
<div class="text-editor-actions">
<button id="textEditorCloseBtn" class="secondary" data-i18n="close"></button>
<button id="textEditorSaveBtn" class="primary" data-i18n="save"></button>
</div>
</section>
</div>
<div id="imagePreviewOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel image-preview-panel">
<div id="imagePreviewName" class="text-editor-path"></div>
<div class="image-preview-stage">
<img id="imagePreview" class="image-preview" alt="">
</div>
<button id="imagePreviewCloseBtn" class="secondary" data-i18n="close"></button>
</section>
</div>
<div id="taskOverlay" class="task-overlay" hidden>
<div class="task-panel">
<div id="tasks" class="tasks"></div>
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true">
<div class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
<script src="/main.js"></script>
</body>
+38 -3
View File
@@ -3,13 +3,19 @@ window.WFM_LANG = {
copy: "Copy",
move: "Move",
delete: "Delete",
copying: "Copying",
moving: "Moving",
deleting: "Deleting",
rename: "Rename",
paste: "Paste",
cancel: "Cancel",
close: "Close",
save: "Save",
exit: "Exit",
exiting: "Exiting...",
refresh: "Refresh",
mkdir: "New Folder",
newText: "New Text",
file: "File",
dir: "Folder",
parent: "Parent",
@@ -33,12 +39,17 @@ window.WFM_LANG = {
speedLabel: "Speed",
progressLabel: "Progress",
etaLabel: "ETA",
elapsedLabel: "Elapsed",
durationHours: "{hours}h {minutes}m",
durationMinutes: "{minutes}m {seconds}s",
durationSeconds: "{seconds}s",
preparingTask: "Preparing task",
canceling: "Canceling...",
selectedItems: "{name} and {count} items",
countItems: "{count} items",
totalItems: "{count} items",
readDir: "Reading folder...",
processing: "Processing...",
actionBusy: "{label}...",
taskCreated: "{label} task created",
actionDone: "{label} done",
@@ -52,15 +63,27 @@ window.WFM_LANG = {
overwriteFiles: "Files with the same name will be overwritten: {names}",
mergeDirs: "Folders with the same name will be merged. Internal files with the same name will be overwritten: {names}",
continueConfirm: "Continue?",
preparingPaste: "{label} preparing: calculating size and checking target space...",
preparingPaste: "Preparing: calculating size and checking target space...",
preparingStatus: "{label} preparing...",
cancelPrepare: "Preparation canceled",
cancelTaskConfirm: "Cancel current {label} task?",
cancelFailed: "Cancel failed: {error}",
tasksPollFailed: "Failed to read task status: {error}",
transferComplete: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}",
transferCompleteFiles: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
renamePrompt: "New name",
mkdirPrompt: "Folder name",
deleteConfirm: "Delete {name}?\n\nFolders will be deleted recursively.",
newTextPrompt: "Text file name",
creatingText: "Creating text file...",
createTextFailed: "Failed to create text file: {error}",
unsavedSaveConfirm: "The text has unsaved changes. Save before closing?\n\nCancel discards the changes and closes.",
loadingText: "Loading text...",
savingText: "Saving...",
textSaved: "Text saved",
openTextFailed: "Failed to open text: {error}",
saveTextFailed: "Failed to save text: {error}",
deleteConfirm: "Delete {name}?",
deleteConfirmRecursive: "Delete {name}?\n\nFolders will be deleted recursively.",
activeTask: "A task is running",
pasteTitle: "{label} {name} to {path}",
pasteToCurrent: "to current folder",
@@ -69,11 +92,14 @@ window.WFM_LANG = {
storageUsb: "USB",
storageM2: "M.2",
storageExtended: "Extended",
storageRoot: "Root",
storageCurrent: "Current",
err_target_dir_not_writable: "Target folder is not writable: {path}",
err_target_file_not_writable: "Target file is not writable: {path}",
err_target_parent_not_writable: "Target parent folder is not writable: {path}",
err_target_parent_not_writable: "Current folder is not writable: {path}",
err_target_check_failed: "Cannot check target path: {path}",
err_target_path_too_long: "Target path is too long",
err_path_too_long: "Path is too long",
err_space_check_failed: "Cannot read target free space: {path}",
err_no_space: "Not enough target space. Required {required}, available {available}",
err_active_task: "A task is running",
@@ -81,6 +107,15 @@ 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_file_not_found: "File not found",
err_file_already_exists: "A file with the same name already exists",
err_invalid_method: "Invalid request method",
err_text_type_not_editable: "This file type is not supported by the text editor",
err_text_file_too_large: "The text file is too large. The maximum size is 1 MiB",
err_text_invalid_utf8: "The file is not valid UTF-8 text",
err_text_file_changed: "The file changed after it was opened. Close and reopen it",
err_text_file_not_writable: "The text file is not writable",
err_request_body_too_large: "Too many selected items. Select fewer items and try again",
err_unknown_api: "Unknown API",
err_out_of_memory: "Out of memory",
err_no_source_paths: "No source paths",
+39 -4
View File
@@ -3,13 +3,19 @@ window.WFM_LANG = {
copy: "复制",
move: "移动",
delete: "删除",
copying: "复制",
moving: "移动",
deleting: "删除",
rename: "重命名",
paste: "粘贴",
cancel: "取消",
close: "关闭",
save: "保存",
exit: "退出",
exiting: "正在退出...",
refresh: "刷新",
mkdir: "新建目录",
newText: "新建文本",
file: "文件",
dir: "目录",
parent: "上级目录",
@@ -33,12 +39,17 @@ window.WFM_LANG = {
speedLabel: "速度",
progressLabel: "进度",
etaLabel: "剩余",
elapsedLabel: "耗时",
durationHours: "{hours}小时 {minutes}分",
durationMinutes: "{minutes}分 {seconds}秒",
durationSeconds: "{seconds}秒",
preparingTask: "正在准备任务",
canceling: "正在取消...",
selectedItems: "{name} 等 {count} 项",
countItems: "{count} 项",
totalItems: "共 {count} 项",
readDir: "读取目录...",
readDir: "读取目录中...",
processing: "处理中...",
actionBusy: "{label}...",
taskCreated: "{label}任务已创建",
actionDone: "{label}完成",
@@ -52,15 +63,27 @@ window.WFM_LANG = {
overwriteFiles: "同名文件将被覆盖: {names}",
mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}",
continueConfirm: "继续?",
preparingPaste: "{label}准备中:正在计算大小并检查目标空间...",
preparingPaste: "准备中:正在计算大小并检查目标空间...",
preparingStatus: "{label}准备中...",
cancelPrepare: "已取消准备操作",
cancelTaskConfirm: "取消当前{label}任务?",
cancelFailed: "取消失败: {error}",
tasksPollFailed: "任务状态读取失败: {error}",
transferComplete: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}",
transferCompleteFiles: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
renamePrompt: "新名称",
mkdirPrompt: "目录名",
deleteConfirm: "确认删除 {name}?\n\n目录会递归删除。",
newTextPrompt: "文本文件名",
creatingText: "正在新建文本...",
createTextFailed: "新建文本失败: {error}",
unsavedSaveConfirm: "文本有未保存的修改,是否保存后关闭?\n\n取消将放弃修改并关闭。",
loadingText: "正在读取文本...",
savingText: "正在保存...",
textSaved: "文本已保存",
openTextFailed: "打开文本失败: {error}",
saveTextFailed: "保存文本失败: {error}",
deleteConfirm: "确认删除 {name}?",
deleteConfirmRecursive: "确认删除 {name}?\n\n目录会递归删除。",
activeTask: "有任务正在执行",
pasteTitle: "{label} {name} 到 {path}",
pasteToCurrent: "到当前目录",
@@ -69,11 +92,14 @@ window.WFM_LANG = {
storageUsb: "USB存储",
storageM2: "M2扩充存储",
storageExtended: "扩展存储",
storageRoot: "根分区",
storageCurrent: "当前目录",
err_target_dir_not_writable: "目标目录不可写: {path}",
err_target_file_not_writable: "目标文件不可写: {path}",
err_target_parent_not_writable: "目标父目录不可写: {path}",
err_target_parent_not_writable: "当前目录不可写: {path}",
err_target_check_failed: "无法检查目标路径: {path}",
err_target_path_too_long: "目标路径过长",
err_path_too_long: "路径过长",
err_space_check_failed: "无法读取目标剩余空间: {path}",
err_no_space: "目标空间不足,需要 {required},可用 {available}",
err_active_task: "有任务正在执行",
@@ -81,6 +107,15 @@ window.WFM_LANG = {
err_source_destination_same: "源和目标相同",
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
err_invalid_path: "路径无效",
err_file_not_found: "文件不存在",
err_file_already_exists: "同名文件已存在",
err_invalid_method: "请求方式无效",
err_text_type_not_editable: "该文件类型不支持文本编辑",
err_text_file_too_large: "文本文件过大,最大支持 1 MiB",
err_text_invalid_utf8: "文件不是有效的 UTF-8 文本",
err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开",
err_text_file_not_writable: "文本文件不可写",
err_request_body_too_large: "选择的项目过多,请减少选择后重试",
err_unknown_api: "未知接口",
err_out_of_memory: "内存不足",
err_no_source_paths: "没有源路径",
+529 -102
View File
@@ -1,5 +1,4 @@
:root {
color-scheme: dark;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #111316;
color: #edf0f2;
@@ -15,46 +14,17 @@
.init-loading {
position: fixed;
inset: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20000;
display: grid;
place-items: center;
background: #111316;
}
.loading-dots {
display: flex;
gap: 12px;
}
.loading-dots span {
width: 14px;
height: 14px;
border-radius: 50%;
background: #8fa1b3;
animation: loading-pulse 1s ease-in-out infinite;
}
.loading-dots span:nth-child(2) {
animation-delay: .12s;
}
.loading-dots span:nth-child(3) {
animation-delay: .24s;
}
@keyframes loading-pulse {
0%,
80%,
100% {
opacity: .35;
transform: scale(.75);
}
40% {
opacity: 1;
transform: scale(1);
}
align-items: center;
justify-content: center;
background: #111316;
color: #aab4be;
font-size: 18px;
}
body {
@@ -68,24 +38,53 @@ body {
.shell {
min-height: 100vh;
height: 100vh;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
display: flex;
flex-direction: column;
}
.topbar {
position: relative;
flex: 0 0 auto;
height: auto;
min-height: 54px;
display: flex;
align-items: center;
gap: 20px;
padding: 12px 30px;
align-items: start;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 18px;
background: #1b2026;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.top-left,
.top-right {
height: auto;
display: flex;
align-items: center;
}
.top-left {
flex: 1 1 420px;
min-width: 0;
margin-top: 15px;
}
.top-right {
flex: 1 1 360px;
min-width: 0;
margin-left: auto;
justify-content: flex-end;
flex-wrap: wrap;
padding: 12px 40px 12px 0;
text-align: right;
}
.path {
color: #aab4be;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 22px;
line-height: 1.35;
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -94,19 +93,25 @@ body {
}
.space-info {
height: auto;
min-height: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 14px;
flex: 0 1 auto;
max-width: 58vw;
flex: 1 1 auto;
max-width: none;
color: #c9d3dc;
font-size: 18px;
line-height: 1.35;
line-height: 30px;
white-space: nowrap;
overflow: hidden;
overflow: visible;
text-align: right;
}
.space-item {
margin-left: 14px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -127,7 +132,6 @@ body {
width: 24px;
height: 24px;
padding: 0;
flex: 0 0 24px;
}
.power-button {
@@ -157,20 +161,30 @@ body {
transform: translateX(-50%);
}
#exitBtn {
position: absolute;
top: 15px;
right: 18px;
margin-left: 0;
}
.toolbar,
.tool-left,
.tool-right {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.toolbar {
flex: 0 0 auto;
height: auto;
min-height: 84px;
justify-content: space-between;
padding: 14px 30px;
padding: 15px 18px 0;
background: #15191e;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.tool-left {
@@ -178,13 +192,30 @@ body {
flex: 1 1 auto;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 54px;
flex-wrap: wrap;
}
.tool-left > *,
.tool-right > * {
margin: 0 12px 15px 0;
}
.tool-right > * {
margin-right: 0;
margin-left: 12px;
}
.tool-right {
justify-content: flex-end;
flex: 0 0 auto;
flex: 1 1 auto;
}
button,
.button {
height: 54px;
min-height: 54px;
min-width: 96px;
padding: 0 20px;
@@ -225,27 +256,76 @@ button:disabled,
background: #402326;
}
.primary {
border-color: #4f9f7b;
background: #285943;
}
button.primary:hover {
background: #347358;
}
.secondary {
border-color: #59636e;
background: #20262d;
}
.paste-action {
max-width: min(480px, 100%);
gap: 8px;
max-width: 480px;
}
.paste-action > * {
flex: 0 0 auto;
margin-right: 8px;
}
.paste-name {
display: inline-block;
max-width: min(180px, 18vw);
flex: 1 1 auto;
min-width: 0;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.paste-count {
white-space: nowrap;
}
.content {
position: relative;
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.content.loading {
pointer-events: none;
}
.content-loading {
position: absolute;
right: 0;
left: 0;
height: 100%;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: rgba(4, 6, 8, .72);
}
.content-loading-text {
color: #aab4be;
font-size: 18px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 1034px;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
@@ -258,6 +338,7 @@ td {
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
line-height: 24px;
}
th {
@@ -275,7 +356,7 @@ th {
}
.name-col {
min-width: 360px;
min-width: 600px;
}
td.name-col {
@@ -312,6 +393,7 @@ tr {
}
thead tr {
height: 78px;
cursor: default;
}
@@ -324,7 +406,12 @@ tr.selected {
background: #1d2d35;
}
tr[data-type="parent"] {
cursor: pointer;
}
.select-hit {
height: 78px;
min-height: 78px;
width: 100%;
display: flex;
@@ -336,25 +423,19 @@ tr.selected {
input[type="checkbox"] {
width: 34px;
height: 34px;
cursor: pointer;
}
.icon {
width: 32px;
height: 32px;
flex: 0 0 32px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon img {
width: 32px;
height: 32px;
display: block;
}
.row-action {
width: 100%;
height: 78px;
min-height: 78px;
padding: 16px 18px;
border: 0;
@@ -362,33 +443,40 @@ input[type="checkbox"] {
background: transparent;
color: #edf0f2;
justify-content: flex-start;
gap: 14px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.row-action .icon {
margin-right: 14px;
}
.name-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.empty,
.status {
.empty {
color: #aab4be;
padding: 10px 25px;
padding: 20px 90px;
}
.status {
color: #aab4be;
flex: 0 0 auto;
height: 46px;
min-height: 46px;
padding: 0 25px;
line-height: 46px;
border-top: 1px solid #2d343d;
background: #15191e;
font-size: 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.version-text {
@@ -396,12 +484,122 @@ input[type="checkbox"] {
color: #7f8a94;
}
.text-editor-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: rgba(4, 6, 8, .9);
}
.text-editor-panel {
width: 1200px;
height: calc(100vh - 96px);
min-width: 0;
max-width: calc(100vw - 96px);
display: flex;
flex-direction: column;
padding: 24px;
border: 1px solid #46515f;
border-radius: 10px;
background: #15191e;
box-shadow: 0 18px 60px rgba(0, 0, 0, .45);
}
.text-editor-path {
flex: 0 0 auto;
margin-bottom: 14px;
overflow: hidden;
color: #c9d3dc;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
white-space: nowrap;
text-overflow: ellipsis;
}
.text-editor {
width: 100%;
min-height: 0;
flex: 1 1 auto;
padding: 18px;
border: 1px solid #46515f;
border-radius: 6px;
outline: none;
resize: none;
background: #0f1215;
color: #edf0f2;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
line-height: 1.5;
}
.text-editor:focus {
border-color: #6fb1ff;
}
.text-editor-status {
min-height: 28px;
padding-top: 8px;
color: #aab4be;
font-size: 18px;
}
.text-editor-actions {
display: flex;
}
.text-editor-actions button {
flex: 1 1 0;
}
.text-editor-actions button + button {
margin-left: 14px;
}
.image-preview-stage {
width: 100%;
min-width: 0;
min-height: 0;
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-preview {
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
object-fit: contain;
pointer-events: none;
-webkit-user-drag: none;
user-select: none;
}
.image-preview-panel > button {
width: 100%;
margin-top: 14px;
}
.task-overlay {
position: fixed;
inset: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10000;
display: grid;
place-items: center;
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: rgba(4, 6, 8, .82);
}
@@ -411,7 +609,8 @@ input[type="checkbox"] {
}
.task-panel {
width: min(980px, calc(100vw - 96px));
width: 980px;
max-width: calc(100vw - 96px);
border: 1px solid #46515f;
border-radius: 10px;
background: #15191e;
@@ -420,20 +619,27 @@ input[type="checkbox"] {
}
.tasks {
display: grid;
gap: 18px;
display: block;
}
.task {
display: grid;
gap: 14px;
display: block;
}
.task > * + * {
margin-top: 14px;
}
.task .danger {
width: 100%;
}
.task-head {
display: flex;
align-items: start;
gap: 16px;
align-items: flex-start;
font-size: 24px;
justify-content: space-between;
position: relative;
}
.task-head.has-amount {
@@ -441,7 +647,7 @@ input[type="checkbox"] {
}
.task-amount {
min-width: max-content;
min-width: 160px;
white-space: nowrap;
text-align: right;
}
@@ -450,14 +656,22 @@ input[type="checkbox"] {
.task-current {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-current-plain {
text-overflow: ellipsis;
}
.task-name {
display: flex;
gap: 8px;
min-width: 0;
text-overflow: ellipsis;
flex: 1 1 auto;
}
.task-name > * {
margin-right: 8px;
}
.task-name-op,
@@ -467,27 +681,39 @@ input[type="checkbox"] {
.task-name-subject {
min-width: 0;
max-width: min(56vw, 560px);
max-width: 560px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-elapsed {
flex: 0 0 auto;
white-space: nowrap;
text-align: right;
margin-left: 16px;
color: #aab4be;
}
.task-meta {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 18px;
display: flex;
color: #c9d3dc;
font-size: 20px;
}
.task-meta div {
flex: 1 1 0;
min-width: 0;
margin-right: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-meta div:last-child {
margin-right: 0;
}
.task-meta div:nth-child(2) {
text-align: center;
}
@@ -517,9 +743,13 @@ input[type="checkbox"] {
.progress-text {
position: absolute;
inset: 0;
display: grid;
place-items: center;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #edf0f2;
font-size: 18px;
font-weight: 700;
@@ -533,18 +763,215 @@ input[type="checkbox"] {
background: #8a929b;
}
@media (max-width: 900px) {
.topbar {
align-items: stretch;
flex-direction: column;
@media (max-width: 815px) {
.top-left {
padding: 0 40px 0 0;
}
th:nth-child(4),
td:nth-child(4),
th:nth-child(5),
td:nth-child(5),
th:nth-child(6),
td:nth-child(6) {
.top-right {
padding: 10px 0;
}
}
@media (max-width: 640px) {
body {
font-size: 16px;
}
.topbar {
min-height: 42px;
padding: 0 10px;
align-items: center;
flex-wrap: nowrap;
}
.top-left {
flex: 1 1 auto;
margin-top: 0;
padding-right: 34px;
}
.top-right {
display: none;
}
.path {
font-size: 16px;
line-height: 42px;
}
#exitBtn {
top: 9px;
right: 10px;
}
.toolbar {
min-height: 0;
padding: 8px 8px 0;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 0;
}
.tool-left,
.tool-right {
flex: 1 1 100%;
}
.tool-right {
justify-content: flex-start;
}
.tool-left > *,
.tool-right > * {
margin: 0 8px 8px 0;
}
.tool-right > * {
margin-left: 0;
}
button,
.button {
height: 40px;
min-height: 40px;
min-width: 76px;
padding: 0 12px;
border-radius: 6px;
font-size: 16px;
}
.paste-action {
max-width: 100%;
}
.paste-name {
max-width: 120px;
}
table {
min-width: 960px;
}
.name-col {
width: 600px;
}
th,
td {
padding: 10px 12px;
font-size: 16px;
line-height: 20px;
}
tr,
thead tr,
.select-hit,
.row-action {
height: 56px;
min-height: 56px;
}
.row-action {
padding: 10px 12px;
}
input[type="checkbox"] {
width: 28px;
height: 28px;
}
.icon {
width: 26px;
height: 26px;
flex-basis: 26px;
}
.status {
height: 34px;
min-height: 34px;
padding: 0 12px;
line-height: 34px;
font-size: 14px;
}
.task-overlay {
padding: 18px;
align-items: center;
}
.task-panel {
width: 100%;
max-width: none;
padding: 18px;
border-radius: 8px;
}
.task > * + * {
margin-top: 12px;
}
.task-head {
display: block;
font-size: 20px;
}
.task-name {
display: flex;
max-width: none;
white-space: nowrap;
}
.task-name > * {
margin: 0 8px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-name-subject {
flex: 1 1 auto;
max-width: none;
min-width: 0;
}
.task-elapsed {
margin: 8px 0 0;
text-align: left;
font-size: 18px;
}
.task-current {
font-size: 18px;
white-space: normal;
word-break: break-all;
}
.task-meta {
display: block;
font-size: 18px;
}
.task-meta div,
.task-meta div:nth-child(2),
.task-meta div:nth-child(3) {
display: block;
margin: 0 0 8px;
text-align: left;
white-space: normal;
}
.task-meta div:last-child {
margin-bottom: 0;
}
.progress {
height: 32px;
}
.task .danger {
width: 100%;
}
}
+683 -136
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -1,4 +1,5 @@
{
"applicationCategoryType": 65536,
"titleId": "FMGR88888",
"deeplinkUri": "http://127.0.0.1:8888/",
"localizedParameters": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+12 -27
View File
@@ -4,8 +4,6 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <ps5/kernel.h>
@@ -33,9 +31,16 @@ int sceAppInstUtilAppInstallAll(void *);
static int
install_file(const char *path, const uint8_t *data, size_t size) {
struct stat st;
FILE *f;
if(!(f = fopen(path, "w"))) {
if(!stat(path, &st)) {
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(!(f = fopen(path, "wb"))) {
return -1;
}
if(fwrite(data, size, 1, f) != 1) {
@@ -65,32 +70,13 @@ install_app(const char *title_id, const char *dir) {
}
static int
needs_update(const char *path, const uint8_t *expected, size_t expected_size) {
needs_install_file(const char *path) {
struct stat st;
uint8_t *buf;
FILE *f;
int mismatch;
if(stat(path, &st) || st.st_size != (off_t)expected_size) {
if(stat(path, &st)) {
return 1;
}
if(!(f = fopen(path, "r"))) {
return 1;
}
if(!(buf = malloc(expected_size))) {
fclose(f);
return 1;
}
if(fread(buf, 1, expected_size, f) != expected_size) {
free(buf);
fclose(f);
return 1;
}
fclose(f);
mismatch = memcmp(buf, expected, expected_size);
free(buf);
return mismatch != 0;
return 0;
}
int
@@ -111,8 +97,7 @@ app_install_if_needed(void) {
if(stat(base_dir, &st)) {
update_needed = 1;
} else if(needs_update(param_path, param_json, param_json_size) ||
needs_update(icon_path, icon0_png, icon0_png_size)) {
} else if(needs_install_file(param_path) || needs_install_file(icon_path)) {
update_needed = 1;
}
+1936 -284
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -3,5 +3,6 @@
#include <microhttpd.h>
enum MHD_Result filemgr_api_request(struct MHD_Connection *conn,
const char *url);
const char *url, const char *method,
const char *body, size_t body_size);
enum MHD_Result filemgr_fs_request(struct MHD_Connection *conn);
+31 -13
View File
@@ -28,12 +28,12 @@ port_available(unsigned short port) {
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 0;
return -1;
}
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) {
perror("setsockopt");
close(fd);
return 0;
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
@@ -50,7 +50,11 @@ find_available_port(unsigned short start) {
unsigned int port;
for(port = start; port <= 65535; port++) {
if(port_available((unsigned short)port)) {
int available = port_available((unsigned short)port);
if(available < 0) {
return 0;
}
if(available) {
return (unsigned short)port;
}
}
@@ -99,7 +103,10 @@ find_pid(const char *name) {
int
main(int argc, char **argv) {
unsigned short port;
#ifdef __SCE__
unsigned short notified_port = 0;
pid_t pid;
#endif
(void)argc;
(void)argv;
@@ -114,26 +121,37 @@ main(int argc, char **argv) {
sleep(1);
}
#else
(void)pid;
#endif
port = find_available_port(DEFAULT_PORT);
if(!port) {
fprintf(stderr, "no available port from %u\n", DEFAULT_PORT);
return 1;
}
puts(PROCESS_NAME);
printf("version: %s\n", VERSION_TAG);
printf("listening on port %u\n", port);
#ifdef __SCE__
app_install_if_needed();
notify_user("Web File Manager\nVersion: %s\nPort: %u", VERSION_TAG, port);
#endif
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
return websrv_listen(port) ? 1 : 0;
while(1) {
port = find_available_port(DEFAULT_PORT);
if(!port) {
fprintf(stderr, "no available port from %u\n", DEFAULT_PORT);
sleep(3);
continue;
}
printf("listening on port %u\n", port);
#ifdef __SCE__
if(notified_port != port) {
notify_user("Web File Manager\nVersion: %s\nPort: %u", VERSION_TAG, port);
notified_port = port;
}
#endif
websrv_listen(port);
sleep(3);
}
return 0;
}
+3
View File
@@ -8,8 +8,10 @@ typedef struct mime_entry {
} mime_entry_t;
static const mime_entry_t g_mimes[] = {
{"bmp", "image/bmp"},
{"css", "text/css"},
{"elf", "application/octet-stream"},
{"gif", "image/gif"},
{"html", "text/html"},
{"jpeg", "image/jpeg"},
{"jpg", "image/jpeg"},
@@ -20,6 +22,7 @@ static const mime_entry_t g_mimes[] = {
{"png", "image/png"},
{"txt", "text/plain"},
{"xml", "text/xml"},
{"webp", "image/webp"},
{"zip", "application/zip"},
};
+64 -5
View File
@@ -13,6 +13,34 @@
#include "filemgr.h"
#include "websrv.h"
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
typedef struct request_context {
char *body;
size_t size;
int too_large;
} request_context_t;
static enum MHD_Result
websrv_body_too_large(struct MHD_Connection *conn) {
static const char json[] =
"{\"ok\":false,\"error\":\"request body is too large\","
"\"error_code\":\"request_body_too_large\",\"error_arg\":\"\"}";
struct MHD_Response *resp =
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
MHD_RESPMEM_PERSISTENT);
enum MHD_Result ret;
if(!resp) {
return MHD_NO;
}
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"application/json");
ret = websrv_queue_response(conn, MHD_HTTP_CONTENT_TOO_LARGE, resp);
MHD_destroy_response(resp);
return ret;
}
enum MHD_Result
websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
struct MHD_Response *resp) {
@@ -26,10 +54,9 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
const char *method, const char *version,
const char *upload_data, size_t *upload_data_size,
void **con_cls) {
request_context_t *ctx = *con_cls;
(void)cls;
(void)version;
(void)upload_data;
(void)upload_data_size;
if(strcmp(method, MHD_HTTP_METHOD_GET) &&
strcmp(method, MHD_HTTP_METHOD_POST) &&
@@ -37,13 +64,39 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
return MHD_NO;
}
if(!*con_cls) {
*con_cls = (void *)1;
if(!ctx) {
if(!(ctx = calloc(1, sizeof(*ctx)))) {
return MHD_NO;
}
*con_cls = ctx;
return MHD_YES;
}
if(*upload_data_size) {
size_t chunk_size = *upload_data_size;
if(chunk_size > REQUEST_BODY_MAX - ctx->size) {
ctx->too_large = 1;
} else if(!ctx->too_large) {
char *body = realloc(ctx->body, ctx->size + chunk_size + 1);
if(!body) {
return MHD_NO;
}
ctx->body = body;
memcpy(ctx->body + ctx->size, upload_data, chunk_size);
ctx->size += chunk_size;
ctx->body[ctx->size] = 0;
}
*upload_data_size = 0;
return MHD_YES;
}
if(ctx->too_large) {
return websrv_body_too_large(conn);
}
if(!strncmp(url, "/api/", 5)) {
return filemgr_api_request(conn, url);
return filemgr_api_request(conn, url, method, ctx->body, ctx->size);
}
if(!strcmp(url, "/fs")) {
return filemgr_fs_request(conn);
@@ -60,6 +113,12 @@ websrv_on_completed(void *cls, struct MHD_Connection *connection,
(void)cls;
(void)connection;
(void)toe;
request_context_t *ctx = *con_cls;
if(ctx) {
free(ctx->body);
free(ctx);
}
*con_cls = NULL;
}