mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 10:00:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72341d6710 | ||
|
|
6780599abd | ||
|
|
9e5f323813 | ||
|
|
5c66a65171 | ||
|
|
34de7d2483 | ||
|
|
bd4d1c45a6 | ||
|
|
28230105c4 | ||
|
|
c7fc108e48 |
No files matched your search
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v1.4
|
||||
VERSION_TAG := v1.7
|
||||
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 src/pkg_installer.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
|
||||
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
|
||||
LINUX_SRCS := $(COMMON_SRCS)
|
||||
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
|
||||
@@ -39,7 +39,7 @@ CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -
|
||||
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LDFLAGS := -Wl,--gc-sections
|
||||
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
|
||||
LINUX_CFLAGS := -O2 -flto -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
|
||||
|
||||
@@ -22,6 +22,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
|
||||
|
||||
- List files and folders.
|
||||
- Sort the list by name, type, size, modified time, or permissions. The selected sort mode is saved in browser local storage.
|
||||
- Change read/write/execute permissions from the permissions column, using checkboxes or a validated four-digit octal mode.
|
||||
- Copy, move, delete, rename, create files, and create folders.
|
||||
- Edit UTF-8 text files up to 1 MiB using the built-in plain text editor.
|
||||
- Multi-select operations.
|
||||
@@ -38,7 +39,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`.
|
||||
- Install/Preview PKG files ends with `.pkg`.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -123,9 +124,12 @@ This project was built with reference to these projects:
|
||||
|
||||
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP server structure, static asset embedding ideas, PS5 browser/websrv behavior and PKG install function. License: GPLv3+.
|
||||
- **[ps5-payload-dev/ftpsrv](https://github.com/ps5-payload-dev/ftpsrv):** PS5 payload conventions, home screen launcher/install flow reference, process handling style and startup installation reference. License: GPLv3+.
|
||||
- **[seregonwar/zftpd](https://github.com/seregonwar/zftpd):** PS5 TCP socket buffer tuning and high-throughput transfer behavior reference. License: MIT.
|
||||
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behavior. License: GPLv3.
|
||||
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. It is licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
|
||||
- **[ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk):** Payload building foundation. License: GPLv3+.
|
||||
- **[etaHEN](https://github.com/etaHEN/etaHEN):** ShellUI URI navigation used to return to the PS5 home screen before exit. License: GPLv3.
|
||||
- **[ezremote](https://github.com/cy33hc/ps5-ezremote-client):** Preview PKG info. License: GPLv2.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -123,6 +123,66 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="pkgInfoOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel pkg-info-panel" role="dialog" aria-modal="true" aria-labelledby="pkgInfoTitle">
|
||||
<h2 id="pkgInfoTitle" class="pkg-info-title" data-i18n="pkgInfoTitle"></h2>
|
||||
<div class="pkg-info-content">
|
||||
<div class="pkg-info-image-stage">
|
||||
<img id="pkgInfoImage" class="pkg-info-image" src="/icon-pkg.png" alt="">
|
||||
</div>
|
||||
<div id="pkgInfoFields" class="pkg-info-fields"></div>
|
||||
</div>
|
||||
<div class="text-editor-actions">
|
||||
<button id="pkgInfoCloseBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="pkgInfoInstallBtn" class="primary" data-i18n="install"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="permissionOverlay" class="permission-overlay" hidden>
|
||||
<section class="permission-panel" role="dialog" aria-modal="true" aria-labelledby="permissionTitle">
|
||||
<h2 id="permissionTitle" class="permission-title" data-i18n="permissionsTitle"></h2>
|
||||
<div id="permissionPath" class="permission-path"></div>
|
||||
<div class="permission-grid">
|
||||
<div class="permission-row permission-head" aria-hidden="true">
|
||||
<span class="permission-scope"></span>
|
||||
<span>R</span><span>W</span><span>X</span>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOwner"></span>
|
||||
<label><input id="permissionOwnerRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOwnerWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOwnerExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionGroup"></span>
|
||||
<label><input id="permissionGroupRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionGroupWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionGroupExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOther"></span>
|
||||
<label><input id="permissionOtherRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOtherWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOtherExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="permission-octal">
|
||||
<span data-i18n="permissionOctal"></span>
|
||||
<input id="permissionMode" type="text" inputmode="numeric" pattern="0[0-7]{3}"
|
||||
minlength="4" maxlength="4" autocomplete="off" spellcheck="false">
|
||||
</label>
|
||||
<label id="permissionRecursiveOption" class="permission-recursive" hidden>
|
||||
<input id="permissionRecursive" type="checkbox" checked>
|
||||
<span data-i18n="permissionRecursive"></span>
|
||||
</label>
|
||||
<div class="permission-actions">
|
||||
<button id="permissionCancelBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="permissionApplyBtn" class="primary" data-i18n="apply"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
|
||||
@@ -9,18 +9,23 @@ window.WFM_LANG = {
|
||||
copying: "Copying",
|
||||
moving: "Moving",
|
||||
deleting: "Deleting",
|
||||
changingPermissions: "Changing permissions",
|
||||
rename: "Rename",
|
||||
paste: "Paste",
|
||||
cancel: "Cancel",
|
||||
close: "Close",
|
||||
save: "Save",
|
||||
apply: "Apply",
|
||||
exit: "Exit",
|
||||
exitConfirm: "Exit and stop the file manager process?",
|
||||
exiting: "Exiting...",
|
||||
refresh: "Refresh",
|
||||
mkdir: "New Folder",
|
||||
newText: "New Text",
|
||||
install: "Install",
|
||||
installPackage: "Install package",
|
||||
pkgInfoTitle: "Package Information",
|
||||
pkgInfoLoading: "Reading package information...",
|
||||
pkgInstalling: "Starting package installation: {name}...",
|
||||
pkgInstallStarted: "Package installation started: {name}",
|
||||
file: "File",
|
||||
@@ -33,6 +38,19 @@ window.WFM_LANG = {
|
||||
size: "Size",
|
||||
mtime: "Modified",
|
||||
mode: "Mode",
|
||||
permissionsTitle: "Change permissions",
|
||||
permissionOwner: "Owner",
|
||||
permissionGroup: "Group",
|
||||
permissionOther: "Other",
|
||||
permissionOctal: "Octal mode",
|
||||
permissionRecursive: "Apply to all contents inside this folder",
|
||||
permissionObjectCount: "and {count} objects",
|
||||
permissionChangeTitle: "Change permissions for {name}",
|
||||
permissionInvalid: "Enter exactly four octal digits from 0 to 7, starting with 0. The original value has been restored.",
|
||||
unsavedPermissionConfirm: "The permissions have unsaved changes. Close without applying them?\n\nCancel keeps the permissions dialog open.",
|
||||
permissionChanging: "Changing permissions...",
|
||||
permissionChanged: "Permissions changed: {name} → {mode}",
|
||||
permissionChangeFailed: "Failed to change permissions: {error}",
|
||||
empty: "Folder is empty",
|
||||
ready: "Ready",
|
||||
freeSpace: "Free space",
|
||||
@@ -46,7 +64,9 @@ window.WFM_LANG = {
|
||||
checking: "Checking",
|
||||
pleaseWait: "Please wait",
|
||||
speedLabel: "Speed",
|
||||
itemsPerSecond: "{count} objects/s",
|
||||
progressLabel: "Progress",
|
||||
permissionProgress: "{done} / {total} objects",
|
||||
etaLabel: "ETA",
|
||||
elapsedLabel: "Elapsed",
|
||||
durationHours: "{hours}h {minutes}m",
|
||||
@@ -124,7 +144,10 @@ 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_invalid_mode: "Invalid permission mode",
|
||||
err_chmod_failed: "Cannot change permissions: {path}",
|
||||
err_pkg_type_invalid: "Only .pkg files can be installed",
|
||||
err_pkg_info_failed: "Could not read package information: {path}",
|
||||
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",
|
||||
|
||||
@@ -9,18 +9,23 @@ window.WFM_LANG = {
|
||||
copying: "复制",
|
||||
moving: "移动",
|
||||
deleting: "删除",
|
||||
changingPermissions: "修改权限",
|
||||
rename: "重命名",
|
||||
paste: "粘贴",
|
||||
cancel: "取消",
|
||||
close: "关闭",
|
||||
save: "保存",
|
||||
apply: "应用",
|
||||
exit: "退出",
|
||||
exitConfirm: "是否退出并关闭文件管理器进程?",
|
||||
exiting: "正在退出...",
|
||||
refresh: "刷新",
|
||||
mkdir: "新建目录",
|
||||
newText: "新建文本",
|
||||
install: "安装",
|
||||
installPackage: "安装 PKG",
|
||||
pkgInfoTitle: "PKG 信息",
|
||||
pkgInfoLoading: "正在读取 PKG 信息...",
|
||||
pkgInstalling: "正在提交安装:{name}...",
|
||||
pkgInstallStarted: "已开始安装:{name}",
|
||||
file: "文件",
|
||||
@@ -33,6 +38,19 @@ window.WFM_LANG = {
|
||||
size: "大小",
|
||||
mtime: "修改时间",
|
||||
mode: "权限",
|
||||
permissionsTitle: "修改权限",
|
||||
permissionOwner: "拥有者",
|
||||
permissionGroup: "组",
|
||||
permissionOther: "其他",
|
||||
permissionOctal: "八进制表示",
|
||||
permissionRecursive: "应用于文件夹内的所有内容",
|
||||
permissionObjectCount: "等 {count} 个对象",
|
||||
permissionChangeTitle: "修改 {name} 的权限",
|
||||
permissionInvalid: "请输入以 0 开头、由 0 到 7 组成的四位八进制权限。已恢复为原权限值。",
|
||||
unsavedPermissionConfirm: "权限有未保存的修改,是否不保存并关闭?\n\n取消将留在权限弹窗中。",
|
||||
permissionChanging: "正在修改权限...",
|
||||
permissionChanged: "权限已修改:{name} → {mode}",
|
||||
permissionChangeFailed: "修改权限失败:{error}",
|
||||
empty: "目录为空",
|
||||
ready: "就绪",
|
||||
freeSpace: "可用空间",
|
||||
@@ -46,7 +64,9 @@ window.WFM_LANG = {
|
||||
checking: "检查中",
|
||||
pleaseWait: "请稍候",
|
||||
speedLabel: "速度",
|
||||
itemsPerSecond: "{count} 个对象/秒",
|
||||
progressLabel: "进度",
|
||||
permissionProgress: "{done} / {total} 个对象",
|
||||
etaLabel: "剩余",
|
||||
elapsedLabel: "耗时",
|
||||
durationHours: "{hours}小时 {minutes}分",
|
||||
@@ -124,7 +144,10 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "源和目标相同",
|
||||
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
|
||||
err_invalid_path: "路径无效",
|
||||
err_invalid_mode: "权限格式无效",
|
||||
err_chmod_failed: "无法修改权限:{path}",
|
||||
err_pkg_type_invalid: "只能安装 .pkg 文件",
|
||||
err_pkg_info_failed: "无法读取 PKG 信息:{path}",
|
||||
err_pkg_install_failed: "PKG 安装失败({arg})",
|
||||
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
|
||||
err_file_not_found: "文件不存在",
|
||||
|
||||
+316
-3
@@ -110,20 +110,55 @@ body {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.space-item {
|
||||
.space-info .space-item {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: 14px;
|
||||
padding: 0 2px;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.space-info .space-item:hover:not(:disabled),
|
||||
.space-info .space-item:focus:not(:disabled) {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.space-info .space-item:focus-visible {
|
||||
box-shadow: 0 0 0 2px #87bfff;
|
||||
}
|
||||
|
||||
.space-info .space-item:active {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.space-info .space-item:disabled {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.space-prefix {
|
||||
color: #8f9aa5;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.space-item.current {
|
||||
.space-info .space-item.current {
|
||||
color: #f2f5f7;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -553,6 +588,22 @@ th.size-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.mode-col {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mode-action {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 78px;
|
||||
min-height: 78px;
|
||||
padding: 0 8px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.select-cell {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
@@ -767,6 +818,234 @@ input[type="checkbox"] {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.pkg-info-panel {
|
||||
width: 1100px;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 96px);
|
||||
}
|
||||
|
||||
.pkg-info-title {
|
||||
margin: 0 0 18px;
|
||||
color: #edf0f2;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pkg-info-content {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.pkg-info-image-stage {
|
||||
width: 340px;
|
||||
flex: 0 0 340px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.pkg-info-image {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 340px;
|
||||
pointer-events: none;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pkg-info-fields {
|
||||
min-width: 0;
|
||||
min-height: 80px;
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid #2d343d;
|
||||
}
|
||||
|
||||
.pkg-info-row {
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #2d343d;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pkg-info-key {
|
||||
width: 244px;
|
||||
flex: 0 0 244px;
|
||||
padding-right: 24px;
|
||||
color: #8f9ba6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pkg-info-value {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: #d9e0e6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.permission-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
background: rgba(4, 6, 8, .9);
|
||||
}
|
||||
|
||||
.permission-panel {
|
||||
width: 620px;
|
||||
max-width: calc(100vw - 96px);
|
||||
padding: 28px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 10px;
|
||||
background: #15191e;
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, .45);
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
margin: 0;
|
||||
color: #edf0f2;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.permission-path {
|
||||
display: flex;
|
||||
margin: 10px 0 22px;
|
||||
color: #9ba7b2;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.permission-path-names {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.permission-path-count {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.permission-grid {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #2d343d;
|
||||
border-bottom: 1px solid #2d343d;
|
||||
}
|
||||
|
||||
.permission-row {
|
||||
display: flex;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.permission-row > * {
|
||||
width: 96px;
|
||||
flex: 0 0 96px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.permission-row .permission-scope {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: #c9d3dc;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.permission-head {
|
||||
min-height: 28px;
|
||||
color: #7f8a94;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.permission-row label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.permission-row label input {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.permission-octal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 22px 0;
|
||||
color: #c9d3dc;
|
||||
}
|
||||
|
||||
.permission-octal span {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.permission-octal input {
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
background: #0f1215;
|
||||
color: #edf0f2;
|
||||
font: 22px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.permission-octal input:focus {
|
||||
border-color: #6fb1ff;
|
||||
outline: 3px solid rgba(111, 177, 255, .25);
|
||||
}
|
||||
|
||||
.permission-octal input:invalid {
|
||||
border-color: #b86262;
|
||||
}
|
||||
|
||||
.permission-recursive {
|
||||
min-height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: -8px 0 18px;
|
||||
color: #c9d3dc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.permission-recursive input {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 12px 0 0;
|
||||
}
|
||||
|
||||
.permission-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.permission-actions button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.permission-actions button + button {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.task-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -940,6 +1219,30 @@ input[type="checkbox"] {
|
||||
background: #8a929b;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pkg-info-panel {
|
||||
width: 760px;
|
||||
}
|
||||
|
||||
.pkg-info-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pkg-info-image-stage {
|
||||
width: auto;
|
||||
height: 260px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.pkg-info-image {
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
.pkg-info-fields {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 815px) {
|
||||
.top-left {
|
||||
padding: 0 40px 0 0;
|
||||
@@ -1072,11 +1375,21 @@ input[type="checkbox"] {
|
||||
.name-head,
|
||||
.parent-nav-button,
|
||||
.select-hit,
|
||||
.row-action {
|
||||
.row-action,
|
||||
.mode-action {
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.permission-overlay {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.permission-panel {
|
||||
max-width: calc(100vw - 36px);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.parent-nav-button {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
|
||||
+453
-64
@@ -25,11 +25,17 @@ let textEditorPath = null;
|
||||
let textEditorVersion = null;
|
||||
let textEditorOriginal = "";
|
||||
let textEditorBusy = false;
|
||||
let permissionItems = [];
|
||||
let permissionOriginal = "";
|
||||
let permissionBusy = false;
|
||||
let pkgInfoItem = null;
|
||||
let pkgInfoRequestId = 0;
|
||||
let downloadFrame = null;
|
||||
let uploadXhr = null;
|
||||
let uploadTerminalAbort = false;
|
||||
let L = {};
|
||||
|
||||
const APP_VERSION = "v1.4";
|
||||
const APP_VERSION = "v1.7";
|
||||
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
|
||||
const SORT_KEY = "ps5-web-file-mgr:list-sort";
|
||||
const LOADING_DISPLAY_DELAY = 250;
|
||||
@@ -80,6 +86,30 @@ const imagePreviewOverlayEl = document.getElementById("imagePreviewOverlay");
|
||||
const imagePreviewNameEl = document.getElementById("imagePreviewName");
|
||||
const imagePreviewEl = document.getElementById("imagePreview");
|
||||
const imagePreviewCloseBtn = document.getElementById("imagePreviewCloseBtn");
|
||||
const pkgInfoOverlayEl = document.getElementById("pkgInfoOverlay");
|
||||
const pkgInfoImageEl = document.getElementById("pkgInfoImage");
|
||||
const pkgInfoTitleEl = document.getElementById("pkgInfoTitle");
|
||||
const pkgInfoFieldsEl = document.getElementById("pkgInfoFields");
|
||||
const pkgInfoCloseBtn = document.getElementById("pkgInfoCloseBtn");
|
||||
const pkgInfoInstallBtn = document.getElementById("pkgInfoInstallBtn");
|
||||
const permissionOverlayEl = document.getElementById("permissionOverlay");
|
||||
const permissionPathEl = document.getElementById("permissionPath");
|
||||
const permissionModeEl = document.getElementById("permissionMode");
|
||||
const permissionRecursiveOptionEl = document.getElementById("permissionRecursiveOption");
|
||||
const permissionRecursiveEl = document.getElementById("permissionRecursive");
|
||||
const permissionCancelBtn = document.getElementById("permissionCancelBtn");
|
||||
const permissionApplyBtn = document.getElementById("permissionApplyBtn");
|
||||
const permissionChecks = [
|
||||
document.getElementById("permissionOwnerRead"),
|
||||
document.getElementById("permissionOwnerWrite"),
|
||||
document.getElementById("permissionOwnerExecute"),
|
||||
document.getElementById("permissionGroupRead"),
|
||||
document.getElementById("permissionGroupWrite"),
|
||||
document.getElementById("permissionGroupExecute"),
|
||||
document.getElementById("permissionOtherRead"),
|
||||
document.getElementById("permissionOtherWrite"),
|
||||
document.getElementById("permissionOtherExecute")
|
||||
];
|
||||
|
||||
function chooseLanguage() {
|
||||
const langs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || ""];
|
||||
@@ -316,21 +346,25 @@ function displayPath(path) {
|
||||
return decodeFsText(path);
|
||||
}
|
||||
|
||||
function renderAddressPath() {
|
||||
const path = displayPath(cwd);
|
||||
pathEl.title = path;
|
||||
pathEl.textContent = path;
|
||||
if (pathEl.scrollWidth <= pathEl.clientWidth) return;
|
||||
function renderEndOfPath(element, path) {
|
||||
element.textContent = path;
|
||||
if (element.scrollWidth <= element.clientWidth) return;
|
||||
|
||||
let low = 0;
|
||||
let high = path.length;
|
||||
while (low < high) {
|
||||
const count = Math.ceil((low + high) / 2);
|
||||
pathEl.textContent = "..." + path.slice(-count);
|
||||
if (pathEl.scrollWidth <= pathEl.clientWidth) low = count;
|
||||
element.textContent = "..." + path.slice(path.length - count);
|
||||
if (element.scrollWidth <= element.clientWidth) low = count;
|
||||
else high = count - 1;
|
||||
}
|
||||
pathEl.textContent = "..." + path.slice(-low);
|
||||
element.textContent = "..." + path.slice(path.length - low);
|
||||
}
|
||||
|
||||
function renderAddressPath() {
|
||||
const path = displayPath(cwd);
|
||||
pathEl.title = path;
|
||||
renderEndOfPath(pathEl, path);
|
||||
}
|
||||
|
||||
async function fetchText(path, params) {
|
||||
@@ -398,11 +432,11 @@ function taskElapsed(task) {
|
||||
}
|
||||
|
||||
function opLabel(op) {
|
||||
return { copy: t("copy"), move: t("move"), delete: t("delete"), download: t("download"), upload: t("upload") }[op] || op;
|
||||
return { copy: t("copy"), move: t("move"), delete: t("delete"), chmod: t("permissionsTitle"), download: t("download"), upload: t("upload"), pkg_install: t("installPackage") }[op] || op;
|
||||
}
|
||||
|
||||
function taskOpLabel(op) {
|
||||
return { copy: t("copying"), move: t("moving"), delete: t("deleting"), download: t("downloading"), upload: t("uploading") }[op] || op;
|
||||
return { copy: t("copying"), move: t("moving"), delete: t("deleting"), chmod: t("changingPermissions"), download: t("downloading"), upload: t("uploading"), pkg_install: t("installPackage") }[op] || op;
|
||||
}
|
||||
|
||||
function isPlayStationBrowser() {
|
||||
@@ -437,6 +471,10 @@ function clearTrackedTask() {
|
||||
downloadFrame.parentNode.removeChild(downloadFrame);
|
||||
downloadFrame = null;
|
||||
}
|
||||
if (trackedTask && trackedTask.op === "upload" && uploadXhr) {
|
||||
uploadTerminalAbort = true;
|
||||
uploadXhr.abort();
|
||||
}
|
||||
trackedTask = null;
|
||||
}
|
||||
|
||||
@@ -453,6 +491,14 @@ function taskFailureMessage(task) {
|
||||
}
|
||||
|
||||
function handleTerminalTask(task) {
|
||||
if (task.op === "pkg_install") {
|
||||
if (task.state === "failed") {
|
||||
const message = taskFailureMessage(task);
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (task.state === "failed") {
|
||||
clearTrackedTask();
|
||||
const message = taskFailureMessage(task);
|
||||
@@ -482,6 +528,12 @@ function setBusy(value) {
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function setModalBackgroundLocked(value) {
|
||||
contentEl.classList.toggle("loading", value);
|
||||
if (value) clearHoverPath();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function showTaskOverlay(immediate, delay) {
|
||||
setBusy(true);
|
||||
if (!overlayEl.hidden || taskOverlayTimer) return;
|
||||
@@ -560,7 +612,8 @@ function historyBlocked() {
|
||||
return Boolean(busy || loadingPath || pendingAbortController ||
|
||||
pendingOverlayText || taskOverlayTimer || !overlayEl.hidden ||
|
||||
!contentLoadingEl.hidden || contentEl.classList.contains("loading") ||
|
||||
!textEditorOverlayEl.hidden || !imagePreviewOverlayEl.hidden);
|
||||
!textEditorOverlayEl.hidden || !imagePreviewOverlayEl.hidden ||
|
||||
!pkgInfoOverlayEl.hidden || !permissionOverlayEl.hidden);
|
||||
}
|
||||
|
||||
function savePath(path) {
|
||||
@@ -604,13 +657,20 @@ async function refreshSpaces() {
|
||||
spaceInfoEl.appendChild(prefix);
|
||||
}
|
||||
for (const item of data.spaces || []) {
|
||||
const span = document.createElement("span");
|
||||
span.className = "space-item" + (item.current ? " current" : "");
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "space-item" + (item.current ? " current" : "");
|
||||
const label = item.label_key ? t(item.label_key) : item.label;
|
||||
span.textContent = label + ": " +
|
||||
button.textContent = label + ": " +
|
||||
formatBytes(item.free, false) + "/" + formatBytes(item.total, false);
|
||||
span.title = item.path + " " + span.textContent;
|
||||
spaceInfoEl.appendChild(span);
|
||||
button.title = item.path + " " + button.textContent;
|
||||
if (item.current) button.setAttribute("aria-current", "location");
|
||||
button.disabled = spaceNavigationLocked();
|
||||
bindPress(button, () => {
|
||||
if (!item.path || spaceNavigationLocked()) return;
|
||||
load(item.path, undefined, false, true, "push");
|
||||
});
|
||||
spaceInfoEl.appendChild(button);
|
||||
}
|
||||
renderAddressPath();
|
||||
} catch (err) {
|
||||
@@ -619,6 +679,11 @@ async function refreshSpaces() {
|
||||
}
|
||||
}
|
||||
|
||||
function spaceNavigationLocked() {
|
||||
return Boolean(busy || loadingPath || !contentLoadingEl.hidden ||
|
||||
contentEl.classList.contains("loading"));
|
||||
}
|
||||
|
||||
function updateSpecialStoragePaths(spaces) {
|
||||
const next = {};
|
||||
let hasMnt = false;
|
||||
@@ -684,44 +749,37 @@ function itemTypeLabel(item) {
|
||||
return t("file");
|
||||
}
|
||||
|
||||
async function installPkg(item) {
|
||||
if (busy || loadingPath) return;
|
||||
async function queuePkgInstall(items) {
|
||||
if (busy || loadingPath || !items.length) 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) }));
|
||||
setStatus(t("pkgInstalling", { name: itemTitle(items) }));
|
||||
await apiForm("/api/install-pkg", {
|
||||
paths: items.map(item => item.path).join("\n")
|
||||
});
|
||||
clearSelection();
|
||||
setStatus(t("pkgInstallStarted", { name: itemTitle(items) }));
|
||||
await pollTasks();
|
||||
} catch (err) {
|
||||
showActionFailed(t("installPackage"), err.message);
|
||||
} finally {
|
||||
localActionBusy = false;
|
||||
setBusy(false);
|
||||
if (overlayEl.hidden && !taskOverlayTimer) 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 installPkg(item) {
|
||||
return queuePkgInstall([item]);
|
||||
}
|
||||
|
||||
function actionInstallSelectedPkgs() {
|
||||
return queuePkgInstall(selectedEntries().filter(isPkgPackage));
|
||||
}
|
||||
|
||||
function openImagePreview(item) {
|
||||
if (busy) return;
|
||||
setModalBackgroundLocked(true);
|
||||
imagePreviewNameEl.textContent = displayName(item);
|
||||
imagePreviewNameEl.title = displayName(item);
|
||||
imagePreviewEl.src = "/fs?path=" + encodeURIComponent(item.path);
|
||||
@@ -734,6 +792,215 @@ function closeImagePreview() {
|
||||
imagePreviewNameEl.textContent = "";
|
||||
imagePreviewNameEl.title = "";
|
||||
imagePreviewEl.removeAttribute("src");
|
||||
setModalBackgroundLocked(false);
|
||||
}
|
||||
|
||||
function addPkgInfoRow(name, value) {
|
||||
if (value === undefined || value === null || String(value) === "") return;
|
||||
const row = document.createElement("div");
|
||||
row.className = "pkg-info-row";
|
||||
const key = document.createElement("div");
|
||||
key.className = "pkg-info-key";
|
||||
key.textContent = name;
|
||||
const text = document.createElement("div");
|
||||
text.className = "pkg-info-value";
|
||||
text.textContent = String(value);
|
||||
appendChildren(row, key, text);
|
||||
pkgInfoFieldsEl.appendChild(row);
|
||||
}
|
||||
|
||||
function pkgHex(value) {
|
||||
return "0x" + ("00000000" + (Number(value || 0) >>> 0).toString(16).toUpperCase()).slice(-8);
|
||||
}
|
||||
|
||||
async function openPkgInfo(item) {
|
||||
if (busy || loadingPath) return;
|
||||
const requestId = ++pkgInfoRequestId;
|
||||
pkgInfoItem = item;
|
||||
setModalBackgroundLocked(true);
|
||||
pkgInfoFieldsEl.innerHTML = "";
|
||||
pkgInfoImageEl.src = "/icon-pkg.png";
|
||||
pkgInfoInstallBtn.disabled = true;
|
||||
setStatus(t("pkgInfoLoading"));
|
||||
try {
|
||||
const data = await api("/api/pkg-info", { path: item.path }, { method: "GET" });
|
||||
if (requestId !== pkgInfoRequestId || pkgInfoItem !== item) return;
|
||||
pkgInfoFieldsEl.innerHTML = "";
|
||||
const platform = data.platform === "PS5" ? "PS5" : "PS4";
|
||||
pkgInfoTitleEl.textContent = platform + " " + t("pkgInfoTitle");
|
||||
const fields = data.fields || [];
|
||||
const priority = [
|
||||
"TITLE", "titleName", "TITLE_ID", "titleId",
|
||||
"VERSION", "masterVersion", "APP_VER", "contentVersion"
|
||||
];
|
||||
const shown = {};
|
||||
for (const name of priority) {
|
||||
const field = fields.find(item => item.name === name);
|
||||
if (field) {
|
||||
addPkgInfoRow(field.name, field.value);
|
||||
shown[name] = true;
|
||||
}
|
||||
}
|
||||
addPkgInfoRow("PKG_SIZE", formatBytes(data.size, true));
|
||||
for (const field of fields) {
|
||||
if (!shown[field.name]) addPkgInfoRow(field.name, field.value);
|
||||
}
|
||||
if (!fields.some(field => field.name === "CONTENT_ID" || field.name === "contentId")) {
|
||||
addPkgInfoRow("PKG_CONTENT_ID", data.content_id);
|
||||
}
|
||||
addPkgInfoRow("PKG_CONTENT_TYPE", pkgHex(data.content_type));
|
||||
addPkgInfoRow("PKG_CONTENT_FLAGS", pkgHex(data.content_flags));
|
||||
if (data.has_icon) {
|
||||
pkgInfoImageEl.src = "/api/pkg-icon?path=" + encodeURIComponent(item.path) +
|
||||
"&mtime=" + encodeURIComponent(item.mtime || 0);
|
||||
}
|
||||
pkgInfoInstallBtn.disabled = false;
|
||||
pkgInfoOverlayEl.hidden = false;
|
||||
pkgInfoFieldsEl.scrollTop = 0;
|
||||
pkgInfoInstallBtn.focus();
|
||||
await nextPaint();
|
||||
if (requestId === pkgInfoRequestId) pkgInfoFieldsEl.scrollTop = 0;
|
||||
} catch (err) {
|
||||
if (requestId !== pkgInfoRequestId) return;
|
||||
pkgInfoItem = null;
|
||||
pkgInfoFieldsEl.innerHTML = "";
|
||||
setModalBackgroundLocked(false);
|
||||
showActionFailed(t("pkgInfoTitle"), err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closePkgInfo() {
|
||||
pkgInfoRequestId++;
|
||||
pkgInfoOverlayEl.hidden = true;
|
||||
pkgInfoItem = null;
|
||||
pkgInfoTitleEl.textContent = t("pkgInfoTitle");
|
||||
pkgInfoFieldsEl.innerHTML = "";
|
||||
pkgInfoImageEl.src = "/icon-pkg.png";
|
||||
pkgInfoInstallBtn.disabled = true;
|
||||
setModalBackgroundLocked(false);
|
||||
}
|
||||
|
||||
function installPkgFromInfo() {
|
||||
const item = pkgInfoItem;
|
||||
if (!item || pkgInfoInstallBtn.disabled) return;
|
||||
closePkgInfo();
|
||||
installPkg(item);
|
||||
}
|
||||
|
||||
function permissionModeText(mode) {
|
||||
const octal = (Number(mode || 0) & 0x1ff).toString(8);
|
||||
return "0" + ("000" + octal).slice(-3);
|
||||
}
|
||||
|
||||
function validPermissionMode(value) {
|
||||
return /^0[0-7]{3}$/.test(String(value || ""));
|
||||
}
|
||||
|
||||
function syncPermissionChecks(value) {
|
||||
if (!validPermissionMode(value)) return;
|
||||
const mode = parseInt(value, 8);
|
||||
const bits = [0x100, 0x080, 0x040, 0x020, 0x010, 0x008, 0x004, 0x002, 0x001];
|
||||
for (let i = 0; i < permissionChecks.length; i++) {
|
||||
permissionChecks[i].checked = Boolean(mode & bits[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function syncPermissionMode() {
|
||||
const bits = [0x100, 0x080, 0x040, 0x020, 0x010, 0x008, 0x004, 0x002, 0x001];
|
||||
let mode = 0;
|
||||
for (let i = 0; i < permissionChecks.length; i++) {
|
||||
if (permissionChecks[i].checked) mode |= bits[i];
|
||||
}
|
||||
permissionModeEl.value = permissionModeText(mode);
|
||||
}
|
||||
|
||||
function validatePermissionMode() {
|
||||
if (validPermissionMode(permissionModeEl.value)) {
|
||||
syncPermissionChecks(permissionModeEl.value);
|
||||
return true;
|
||||
}
|
||||
alert(t("permissionInvalid"));
|
||||
permissionModeEl.value = permissionModeText(permissionItems.length ? permissionItems[0].mode : 0);
|
||||
syncPermissionChecks(permissionModeEl.value);
|
||||
permissionModeEl.focus();
|
||||
permissionModeEl.select();
|
||||
return false;
|
||||
}
|
||||
|
||||
function setPermissionBusy(value) {
|
||||
permissionBusy = value;
|
||||
permissionModeEl.disabled = value;
|
||||
permissionRecursiveEl.disabled = value;
|
||||
permissionCancelBtn.disabled = value;
|
||||
permissionApplyBtn.disabled = value;
|
||||
for (const checkbox of permissionChecks) checkbox.disabled = value;
|
||||
}
|
||||
|
||||
function openPermissionDialog(item) {
|
||||
if (busy || loadingPath || permissionBusy) return;
|
||||
const items = selected.has(item.path) ? selectedEntries() : [item];
|
||||
setModalBackgroundLocked(true);
|
||||
permissionItems = items;
|
||||
renderPermissionItems(items);
|
||||
permissionModeEl.value = permissionModeText(item.mode);
|
||||
permissionOriginal = permissionModeEl.value;
|
||||
permissionRecursiveOptionEl.hidden = !items.some(entry => entry.type === "d");
|
||||
permissionRecursiveEl.checked = true;
|
||||
syncPermissionChecks(permissionModeEl.value);
|
||||
setPermissionBusy(false);
|
||||
permissionOverlayEl.hidden = false;
|
||||
renderSinglePermissionPath();
|
||||
permissionModeEl.focus();
|
||||
permissionModeEl.select();
|
||||
}
|
||||
|
||||
function closePermissionDialog() {
|
||||
if (permissionBusy) return;
|
||||
permissionOverlayEl.hidden = true;
|
||||
permissionItems = [];
|
||||
permissionOriginal = "";
|
||||
permissionPathEl.textContent = "";
|
||||
permissionPathEl.title = "";
|
||||
permissionRecursiveOptionEl.hidden = true;
|
||||
setModalBackgroundLocked(false);
|
||||
}
|
||||
|
||||
function requestClosePermissionDialog() {
|
||||
if (permissionBusy) return;
|
||||
if (validPermissionMode(permissionModeEl.value) &&
|
||||
(permissionModeEl.value !== permissionOriginal ||
|
||||
(!permissionRecursiveOptionEl.hidden && !permissionRecursiveEl.checked)) &&
|
||||
!confirm(t("unsavedPermissionConfirm"))) return;
|
||||
closePermissionDialog();
|
||||
}
|
||||
|
||||
async function applyPermissionMode() {
|
||||
if (permissionBusy || !permissionItems.length || !validatePermissionMode()) return;
|
||||
const items = permissionItems.slice();
|
||||
const mode = permissionModeEl.value;
|
||||
setPermissionBusy(true);
|
||||
setStatus(t("permissionChanging"));
|
||||
taskRefreshPath = cwd;
|
||||
try {
|
||||
const data = await apiForm("/api/chmod", {
|
||||
paths: items.map(item => item.path).join("\n"),
|
||||
mode,
|
||||
recursive: items.some(item => item.type === "d") && permissionRecursiveEl.checked ? "1" : "0"
|
||||
});
|
||||
setPermissionBusy(false);
|
||||
closePermissionDialog();
|
||||
setBusy(true);
|
||||
trackTask(data.task_id, "chmod", false);
|
||||
clearSelection(false);
|
||||
setStatus(t("taskCreated", { label: t("permissionsTitle") }));
|
||||
await pollTasks();
|
||||
} catch (err) {
|
||||
setPermissionBusy(false);
|
||||
const message = t("permissionChangeFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
permissionModeEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function setTextEditorBusy(value, status) {
|
||||
@@ -747,6 +1014,7 @@ function setTextEditorBusy(value, status) {
|
||||
function closeTextEditor() {
|
||||
if (textEditorBusy) return;
|
||||
textEditorOverlayEl.hidden = true;
|
||||
setModalBackgroundLocked(false);
|
||||
textEditorPath = null;
|
||||
textEditorVersion = null;
|
||||
textEditorOriginal = "";
|
||||
@@ -763,6 +1031,7 @@ function requestCloseTextEditor() {
|
||||
}
|
||||
|
||||
function showTextEditor(item, text, version) {
|
||||
setModalBackgroundLocked(true);
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = version;
|
||||
textEditorPathEl.textContent = displayPath(item.path);
|
||||
@@ -776,6 +1045,7 @@ function showTextEditor(item, text, version) {
|
||||
|
||||
async function openTextEditor(item) {
|
||||
if (busy || textEditorBusy) return;
|
||||
setModalBackgroundLocked(true);
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = null;
|
||||
textEditorOriginal = "";
|
||||
@@ -790,6 +1060,7 @@ async function openTextEditor(item) {
|
||||
} catch (err) {
|
||||
setTextEditorBusy(false, "");
|
||||
textEditorOverlayEl.hidden = true;
|
||||
setModalBackgroundLocked(false);
|
||||
textEditorPath = null;
|
||||
const message = t("openTextFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
@@ -885,6 +1156,30 @@ function itemListTitle(items, limit) {
|
||||
return items.length > limit ? t("selectedItems", { name: shown, count: items.length }) : shown;
|
||||
}
|
||||
|
||||
function renderSinglePermissionPath() {
|
||||
if (permissionOverlayEl.hidden || permissionItems.length !== 1) return;
|
||||
const namesEl = permissionPathEl.querySelector(".permission-path-names");
|
||||
if (namesEl) renderEndOfPath(namesEl, displayPath(permissionItems[0].path));
|
||||
}
|
||||
|
||||
function renderPermissionItems(items) {
|
||||
const names = items.length === 1 ? displayPath(items[0].path) :
|
||||
items.map(displayName).join(", ");
|
||||
const count = items.length > 1 ? t("permissionObjectCount", { count: items.length }) : "";
|
||||
const namesEl = document.createElement("span");
|
||||
namesEl.className = "permission-path-names";
|
||||
namesEl.textContent = names;
|
||||
permissionPathEl.innerHTML = "";
|
||||
permissionPathEl.appendChild(namesEl);
|
||||
if (count) {
|
||||
const countEl = document.createElement("span");
|
||||
countEl.className = "permission-path-count";
|
||||
countEl.textContent = count;
|
||||
permissionPathEl.appendChild(countEl);
|
||||
}
|
||||
permissionPathEl.title = count ? names + " " + count : names;
|
||||
}
|
||||
|
||||
function compareText(a, b) {
|
||||
return String(a || "").localeCompare(String(b || ""));
|
||||
}
|
||||
@@ -985,6 +1280,9 @@ function singleSelected() {
|
||||
function updateButtons() {
|
||||
const items = selectedEntries();
|
||||
const locked = busy || contentEl.classList.contains("loading");
|
||||
for (const button of spaceInfoEl.querySelectorAll(".space-item")) {
|
||||
button.disabled = spaceNavigationLocked();
|
||||
}
|
||||
document.getElementById("copyBtn").disabled = locked || items.length === 0;
|
||||
document.getElementById("moveBtn").disabled = locked || items.length === 0;
|
||||
document.getElementById("renameBtn").disabled = locked || items.length !== 1;
|
||||
@@ -996,8 +1294,11 @@ function updateButtons() {
|
||||
uploadFolderBtn.disabled = locked;
|
||||
document.getElementById("mkdirBtn").disabled = locked;
|
||||
newTextBtn.disabled = locked;
|
||||
for (const button of filesEl.querySelectorAll(".row-action, .mode-action")) button.disabled = locked;
|
||||
for (const checkbox of filesEl.querySelectorAll(".select-cell input")) checkbox.disabled = locked;
|
||||
selectAllEl.disabled = locked;
|
||||
parentBtn.disabled = cwd === "/";
|
||||
parentBtn.disabled = locked || cwd === "/";
|
||||
exitBtn.disabled = locked;
|
||||
selectAllEl.checked = entries.length > 0 && items.length === entries.length;
|
||||
selectAllEl.indeterminate = items.length > 0 && items.length < entries.length;
|
||||
renderInstallPkgButton(items, locked);
|
||||
@@ -1182,6 +1483,25 @@ function render() {
|
||||
nameBtn.appendChild(nameText);
|
||||
nameTd.appendChild(nameBtn);
|
||||
|
||||
const modeTd = document.createElement("td");
|
||||
modeTd.className = "mode-col";
|
||||
if (!isParent) {
|
||||
const modeBtn = document.createElement("button");
|
||||
modeBtn.className = "mode-action";
|
||||
modeBtn.type = "button";
|
||||
modeBtn.textContent = permissionModeText(item.mode);
|
||||
modeBtn.title = t("permissionChangeTitle", { name: displayName(item) });
|
||||
modeBtn.setAttribute("aria-label", modeBtn.title);
|
||||
modeBtn.disabled = busy || Boolean(loadingPath);
|
||||
modeBtn.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
focusPath(item.path);
|
||||
openPermissionDialog(item);
|
||||
});
|
||||
modeTd.appendChild(modeBtn);
|
||||
}
|
||||
|
||||
appendChildren(
|
||||
tr,
|
||||
selectTd,
|
||||
@@ -1189,7 +1509,7 @@ function render() {
|
||||
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))
|
||||
modeTd
|
||||
);
|
||||
fragment.appendChild(tr);
|
||||
}
|
||||
@@ -1234,6 +1554,7 @@ async function load(path, scrollTop, force, alertOnError, historyMode) {
|
||||
const shouldScrollTop = scrollTop !== false;
|
||||
if ((busy && !force) || loadingPath) return;
|
||||
loadingPath = path;
|
||||
updateButtons();
|
||||
startContentLoadingTimer();
|
||||
try {
|
||||
setStatus(t("readDir"));
|
||||
@@ -1510,7 +1831,8 @@ async function actionPaste() {
|
||||
}
|
||||
|
||||
function renderTasks(tasks) {
|
||||
const active = tasks.find(task => task.state === "queued" || task.state === "running");
|
||||
const fileTasks = tasks.filter(task => task.op !== "pkg_install");
|
||||
const active = fileTasks.find(task => task.state === "queued" || task.state === "running");
|
||||
if (!active && pendingOverlayText) {
|
||||
renderPendingOverlay(pendingOverlayText, pendingOverlayLabel);
|
||||
showTaskOverlay();
|
||||
@@ -1534,7 +1856,8 @@ function renderTasks(tasks) {
|
||||
const speed = Number(task.speed || 0);
|
||||
const isDelete = task.op === "delete";
|
||||
const isDownload = task.op === "download";
|
||||
const isPreparing = (task.op === "copy" || task.op === "move") &&
|
||||
const isChmod = task.op === "chmod";
|
||||
const isPreparing = (task.op === "copy" || task.op === "move" || isChmod) &&
|
||||
task.state === "running" && done === 0;
|
||||
const isFinishing = !isDelete && !isDownload && task.state === "running" && total > 0 && done >= total;
|
||||
const pct = total > 0 ? Math.min(100, Math.floor(done * 100 / total)) : 0;
|
||||
@@ -1563,9 +1886,11 @@ function renderTasks(tasks) {
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "task-meta";
|
||||
const speedItem = document.createElement("div");
|
||||
speedItem.textContent = t("speedLabel") + ": " + formatSpeed(speed);
|
||||
speedItem.textContent = t("speedLabel") + ": " + (isChmod ?
|
||||
t("itemsPerSecond", { count: Math.round(speed) }) : formatSpeed(speed));
|
||||
const progressItem = document.createElement("div");
|
||||
progressItem.textContent = t("progressLabel") + ": " + formatSize(done) + " / " + formatSize(total);
|
||||
progressItem.textContent = t("progressLabel") + ": " + (isChmod ?
|
||||
t("permissionProgress", { done, total }) : formatSize(done) + " / " + formatSize(total));
|
||||
const etaItem = document.createElement("div");
|
||||
etaItem.textContent = t("etaLabel") + ": " + averageEta(task, done, total);
|
||||
appendChildren(meta, speedItem, progressItem, etaItem);
|
||||
@@ -1590,8 +1915,11 @@ function renderTasks(tasks) {
|
||||
if (confirm(t("cancelTaskConfirm", { label: opLabel(task.op) }))) {
|
||||
requestTaskCancel(task.id);
|
||||
api("/api/cancel", { id: task.id }).then(() => {
|
||||
if (task.op === "upload" && uploadXhr) uploadXhr.abort();
|
||||
return pollTasks();
|
||||
if (task.op !== "upload") return pollTasks();
|
||||
if (uploadXhr) uploadXhr.abort();
|
||||
return api("/api/upload/finish", { task_id: task.id })
|
||||
.catch(() => {})
|
||||
.then(pollTasks);
|
||||
}).catch(err => {
|
||||
if (trackedTask && trackedTask.id === task.id) trackedTask.cancelRequested = false;
|
||||
setStatus(t("cancelFailed", { error: err.message }));
|
||||
@@ -1611,6 +1939,7 @@ async function pollTasks() {
|
||||
const data = await api("/api/tasks");
|
||||
taskPollFailedAlertShown = false;
|
||||
const tasks = data.tasks || [];
|
||||
const fileTasks = tasks.filter(task => task.op !== "pkg_install");
|
||||
const completion = data.completion;
|
||||
if (lastCompletionId === null) {
|
||||
lastCompletionId = completion ? completion.id : 0;
|
||||
@@ -1640,7 +1969,7 @@ async function pollTasks() {
|
||||
trackedTask.state = task.state;
|
||||
}
|
||||
if (isTerminalTask(task)) {
|
||||
shouldRefresh = true;
|
||||
if (task.op !== "pkg_install") shouldRefresh = true;
|
||||
handleTerminalTask(task);
|
||||
}
|
||||
}
|
||||
@@ -1650,7 +1979,7 @@ async function pollTasks() {
|
||||
shouldRefresh = true;
|
||||
clearTrackedTask();
|
||||
}
|
||||
if (taskBecameIdle && !tasks.length) shouldRefresh = true;
|
||||
if (taskBecameIdle && !fileTasks.length) shouldRefresh = true;
|
||||
if (shouldRefresh) {
|
||||
if (taskBecameIdle) startContentLoadingTimer();
|
||||
clearSelection(false);
|
||||
@@ -1733,7 +2062,7 @@ function uploadConflicts(rels) {
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
function uploadFileRequest(taskId, file, rel, overwrite) {
|
||||
function uploadFileRequest(taskId, file, rel, overwrite, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
uploadXhr = xhr;
|
||||
@@ -1759,12 +2088,25 @@ function uploadFileRequest(taskId, file, rel, overwrite) {
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
uploadXhr = null;
|
||||
reject(new Error(xhr.statusText || t("backendError")));
|
||||
const error = new Error(xhr.statusText || t("backendError"));
|
||||
api("/api/tasks").then(data => {
|
||||
const task = (data.tasks || []).find(item => item.id === taskId);
|
||||
if (task && Number(task.completed_count || 0) >= index) {
|
||||
resolve();
|
||||
} else if (task && task.state === "failed") {
|
||||
reject(new Error(backendErrorText(task.error_code, task.error_arg,
|
||||
task.error || task.current)));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
}).catch(() => reject(error));
|
||||
};
|
||||
xhr.onabort = () => {
|
||||
uploadXhr = null;
|
||||
const terminal = uploadTerminalAbort;
|
||||
uploadTerminalAbort = false;
|
||||
const err = new Error("aborted");
|
||||
err.name = "AbortError";
|
||||
err.name = terminal ? "TerminalTaskAbort" : "AbortError";
|
||||
reject(err);
|
||||
};
|
||||
xhr.send(file);
|
||||
@@ -1822,21 +2164,32 @@ async function uploadFiles(files) {
|
||||
const file = list[i];
|
||||
const rel = rels[i];
|
||||
setStatus(t("uploadingStatus", { index: i + 1, count: list.length, name: rel }));
|
||||
await uploadFileRequest(taskId, file, rel, overwrite);
|
||||
await uploadFileRequest(taskId, file, rel, overwrite, i + 1);
|
||||
}
|
||||
await api("/api/upload/finish", { task_id: taskId });
|
||||
await pollTasks();
|
||||
setStatus(t("uploadDone", { count: list.length }));
|
||||
} catch (err) {
|
||||
const terminalAbort = err && err.name === "TerminalTaskAbort";
|
||||
const canceled = err && err.name === "AbortError";
|
||||
if (canceled) {
|
||||
setStatus(t("taskCanceled", { label: opLabel("upload") }));
|
||||
} else {
|
||||
const message = t("uploadFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
if (!terminalAbort) {
|
||||
if (taskId) {
|
||||
try {
|
||||
await api("/api/cancel", { id: taskId });
|
||||
await api("/api/upload/finish", { task_id: taskId });
|
||||
} catch (cancelErr) {
|
||||
/* The server may already have moved a failed upload to a terminal state. */
|
||||
}
|
||||
}
|
||||
await pollTasks();
|
||||
if (canceled) {
|
||||
setStatus(t("taskCanceled", { label: opLabel("upload") }));
|
||||
} else {
|
||||
const message = t("uploadFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
await pollTasks();
|
||||
} finally {
|
||||
uploadXhr = null;
|
||||
uploadFilesEl.value = "";
|
||||
@@ -1863,6 +2216,7 @@ function toggleUploadMenu() {
|
||||
}
|
||||
|
||||
function actionExit() {
|
||||
if (!confirm(t("exitConfirm"))) return;
|
||||
setStatus(t("exiting"));
|
||||
exitBtn.disabled = true;
|
||||
api("/api/exit").catch(err => {
|
||||
@@ -1898,10 +2252,39 @@ textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
|
||||
textEditorSaveBtn.addEventListener("click", saveTextEditor);
|
||||
newTextBtn.addEventListener("click", actionNewText);
|
||||
imagePreviewCloseBtn.addEventListener("click", closeImagePreview);
|
||||
window.addEventListener("resize", renderAddressPath);
|
||||
pkgInfoCloseBtn.addEventListener("click", closePkgInfo);
|
||||
pkgInfoInstallBtn.addEventListener("click", installPkgFromInfo);
|
||||
pkgInfoImageEl.addEventListener("error", () => {
|
||||
if (pkgInfoImageEl.getAttribute("src") !== "/icon-pkg.png") {
|
||||
pkgInfoImageEl.src = "/icon-pkg.png";
|
||||
}
|
||||
});
|
||||
permissionCancelBtn.addEventListener("click", requestClosePermissionDialog);
|
||||
permissionApplyBtn.addEventListener("click", applyPermissionMode);
|
||||
permissionModeEl.addEventListener("input", () => {
|
||||
if (validPermissionMode(permissionModeEl.value)) syncPermissionChecks(permissionModeEl.value);
|
||||
});
|
||||
permissionModeEl.addEventListener("change", validatePermissionMode);
|
||||
for (const checkbox of permissionChecks) checkbox.addEventListener("change", syncPermissionMode);
|
||||
window.addEventListener("resize", () => {
|
||||
renderAddressPath();
|
||||
renderSinglePermissionPath();
|
||||
});
|
||||
window.addEventListener("popstate", event => {
|
||||
if (historyBlocked()) {
|
||||
writeHistoryPath(cwd, false);
|
||||
if (!textEditorOverlayEl.hidden) {
|
||||
requestCloseTextEditor();
|
||||
return;
|
||||
}
|
||||
if (!permissionOverlayEl.hidden) {
|
||||
requestClosePermissionDialog();
|
||||
return;
|
||||
}
|
||||
if (!pkgInfoOverlayEl.hidden) {
|
||||
closePkgInfo();
|
||||
return;
|
||||
}
|
||||
setStatus(t("activeTask"));
|
||||
return;
|
||||
}
|
||||
@@ -1928,6 +2311,12 @@ textEditorOverlayEl.addEventListener("click", event => {
|
||||
imagePreviewOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === imagePreviewOverlayEl) closeImagePreview();
|
||||
});
|
||||
pkgInfoOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === pkgInfoOverlayEl) closePkgInfo();
|
||||
});
|
||||
permissionOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === permissionOverlayEl) requestClosePermissionDialog();
|
||||
});
|
||||
contentEl.addEventListener("click", event => {
|
||||
if (!busy && !loadingPath) return;
|
||||
event.preventDefault();
|
||||
@@ -1970,7 +2359,7 @@ filesEl.addEventListener("click", event => {
|
||||
} else await load(cwd, false, true);
|
||||
}, 0);
|
||||
}
|
||||
else if (isPkgPackage(item)) installPkg(item);
|
||||
else if (isPkgPackage(item)) openPkgInfo(item);
|
||||
else if (isPreviewableImage(item)) openImagePreview(item);
|
||||
else if (isEditableText(item)) openTextEditor(item);
|
||||
else togglePath(item.path, !selected.has(item.path));
|
||||
|
||||
+27
-20
@@ -16,6 +16,7 @@
|
||||
#include "websrv.h"
|
||||
|
||||
#define DOWNLOAD_ARCHIVE_NAME_LIMIT 20
|
||||
#define DOWNLOAD_BUFFER_SIZE (2 * 1024 * 1024)
|
||||
|
||||
typedef struct tar_frame {
|
||||
char path[PATH_MAX];
|
||||
@@ -47,7 +48,7 @@ typedef struct tar_stream {
|
||||
|
||||
typedef struct download_file_stream {
|
||||
file_task_t *task;
|
||||
FILE *file;
|
||||
int fd;
|
||||
unsigned long long size;
|
||||
unsigned long long sent;
|
||||
int done;
|
||||
@@ -295,6 +296,8 @@ static ssize_t
|
||||
tar_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
tar_stream_t *s = cls;
|
||||
size_t out = 0;
|
||||
unsigned long long progress = 0;
|
||||
char progress_current[PATH_MAX] = {0};
|
||||
(void)pos;
|
||||
|
||||
while(out < max && !s->done) {
|
||||
@@ -340,10 +343,10 @@ tar_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
}
|
||||
out += (size_t)n;
|
||||
s->file_remaining -= (unsigned long long)n;
|
||||
if(s->task) {
|
||||
task_update(s->task, TASK_RUNNING,
|
||||
s->current_file[0] ? s->current_file : NULL,
|
||||
(unsigned long long)n, NULL);
|
||||
progress += (unsigned long long)n;
|
||||
if(s->current_file[0]) {
|
||||
snprintf(progress_current, sizeof(progress_current), "%s",
|
||||
s->current_file);
|
||||
}
|
||||
if(!s->file_remaining) {
|
||||
close(s->fd);
|
||||
@@ -354,6 +357,11 @@ tar_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if(s->task && progress) {
|
||||
task_update(s->task, TASK_RUNNING,
|
||||
progress_current[0] ? progress_current : NULL,
|
||||
progress, NULL);
|
||||
}
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
|
||||
@@ -589,22 +597,18 @@ api_download_prepare(struct MHD_Connection *conn, const char *body,
|
||||
static ssize_t
|
||||
download_file_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
download_file_stream_t *s = cls;
|
||||
size_t len;
|
||||
(void)pos;
|
||||
ssize_t len;
|
||||
|
||||
if(task_cancel_requested(s->task)) {
|
||||
s->error = ECANCELED;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(fseek(s->file, (long)pos, SEEK_SET)) {
|
||||
len = pread(s->fd, buf, max, (off_t)pos);
|
||||
if(len < 0) {
|
||||
s->error = errno ? errno : EIO;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!(len = fread(buf, 1, max, s->file))) {
|
||||
if(ferror(s->file)) {
|
||||
s->error = errno ? errno : EIO;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!len) {
|
||||
s->done = 1;
|
||||
return MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
@@ -619,7 +623,7 @@ download_file_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
}
|
||||
task_update(s->task, TASK_RUNNING, s->task->src, add, NULL);
|
||||
}
|
||||
return (ssize_t)len;
|
||||
return len;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -630,8 +634,8 @@ download_file_close(void *cls) {
|
||||
if(!s) {
|
||||
return;
|
||||
}
|
||||
if(s->file) {
|
||||
fclose(s->file);
|
||||
if(s->fd >= 0) {
|
||||
close(s->fd);
|
||||
}
|
||||
canceled = task_cancel_requested(s->task);
|
||||
if(canceled) {
|
||||
@@ -692,15 +696,17 @@ api_download(struct MHD_Connection *conn) {
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
file_stream->task = task;
|
||||
file_stream->fd = -1;
|
||||
file_stream->size = (unsigned long long)st.st_size;
|
||||
file_stream->done = file_stream->size == 0;
|
||||
file_stream->file = fopen(paths[0], "rb");
|
||||
if(!file_stream->file) {
|
||||
file_stream->fd = open(paths[0], O_RDONLY);
|
||||
if(file_stream->fd < 0) {
|
||||
free(file_stream);
|
||||
task_update(task, TASK_FAILED, task->src, 0, strerror(errno));
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
resp = MHD_create_response_from_callback((uint64_t)st.st_size, 32 * 0x4000,
|
||||
resp = MHD_create_response_from_callback((uint64_t)st.st_size,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
download_file_read, file_stream,
|
||||
download_file_close);
|
||||
if(!resp) {
|
||||
@@ -738,7 +744,8 @@ api_download(struct MHD_Connection *conn) {
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
}
|
||||
resp = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN, 32 * 0x4000,
|
||||
resp = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
tar_read, stream, tar_close);
|
||||
if(!resp) {
|
||||
tar_close(stream);
|
||||
|
||||
+391
-30
@@ -7,6 +7,7 @@
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
@@ -15,9 +16,14 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef __linux__
|
||||
#include <ps5/kernel.h>
|
||||
#endif
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "pkg_info.h"
|
||||
#include "pkg_installer.h"
|
||||
#include "websrv.h"
|
||||
|
||||
@@ -30,6 +36,59 @@
|
||||
#define LARGE_FILE_THRESHOLD (256LL * 1024 * 1024)
|
||||
#define TRANSFER_ALERT_THRESHOLD (10 * 60)
|
||||
|
||||
#ifndef __linux__
|
||||
typedef struct shell_ui_uri_param {
|
||||
uint32_t size;
|
||||
uint32_t user_id;
|
||||
} shell_ui_uri_param_t;
|
||||
|
||||
int sceKernelLoadStartModule(const char *, size_t, const void *, uint32_t,
|
||||
void *, int *);
|
||||
int sceUserServiceInitialize(const int *);
|
||||
int sceUserServiceGetForegroundUser(int *);
|
||||
|
||||
static int
|
||||
navigate_to_home(void) {
|
||||
int (*initialize)(void);
|
||||
int (*launch_by_uri)(const char *, shell_ui_uri_param_t *);
|
||||
shell_ui_uri_param_t param = {.size = sizeof(param)};
|
||||
const char *module_path =
|
||||
"/system_ex/common_ex/lib/libSceShellUIUtil.sprx";
|
||||
const char *uri = "pshomeui:navigateToHome?bootCondition=psButton";
|
||||
const int priority = 256;
|
||||
int module;
|
||||
int result;
|
||||
|
||||
(void)sceUserServiceInitialize(&priority);
|
||||
module = sceKernelLoadStartModule(module_path, 0, NULL, 0, NULL, NULL);
|
||||
if(module < 0) {
|
||||
printf("load libSceShellUIUtil: 0x%08X\n", (unsigned int)module);
|
||||
return module;
|
||||
}
|
||||
initialize = (void *)kernel_dynlib_dlsym(
|
||||
-1, (uint32_t)module, "sceShellUIUtilInitialize");
|
||||
launch_by_uri = (void *)kernel_dynlib_dlsym(
|
||||
-1, (uint32_t)module, "sceShellUIUtilLaunchByUri");
|
||||
if(!initialize || !launch_by_uri) {
|
||||
printf("resolve libSceShellUIUtil URI functions failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
result = initialize();
|
||||
if(result < 0) {
|
||||
printf("sceShellUIUtilInitialize: 0x%08X\n", (unsigned int)result);
|
||||
}
|
||||
result = sceUserServiceGetForegroundUser((int *)¶m.user_id);
|
||||
if(result < 0) {
|
||||
printf("sceUserServiceGetForegroundUser: 0x%08X\n",
|
||||
(unsigned int)result);
|
||||
}
|
||||
result = launch_by_uri(uri, ¶m);
|
||||
printf("sceShellUIUtilLaunchByUri: 0x%08X\n", (unsigned int)result);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
typedef struct task_completion {
|
||||
unsigned long id;
|
||||
task_op_t op;
|
||||
@@ -90,6 +149,11 @@ typedef struct copy_queue {
|
||||
#endif
|
||||
|
||||
static task_completion_t g_last_completion;
|
||||
#ifndef __linux__
|
||||
static pthread_cond_t g_pkg_tasks_cond = PTHREAD_COND_INITIALIZER;
|
||||
static pthread_t g_pkg_worker_thread;
|
||||
static int g_pkg_worker_started;
|
||||
#endif
|
||||
|
||||
void
|
||||
record_task_completion_locked(file_task_t *task, time_t completed_at) {
|
||||
@@ -255,6 +319,8 @@ send_json_error_detail(struct MHD_Connection *conn, unsigned int status,
|
||||
|
||||
static int task_target_path(file_task_t *task, const char *src,
|
||||
char *out, size_t size);
|
||||
static int chmod_task_path(file_task_t *task, const char *path,
|
||||
unsigned int mode, int recursive);
|
||||
|
||||
static int count_path_bytes_sync(file_task_t *task, const char *path,
|
||||
const char *display,
|
||||
@@ -1679,6 +1745,37 @@ task_worker(void *arg) {
|
||||
for(i = 0; i < task->src_count && !ret; i++) {
|
||||
ret = remove_path(task, task->srcs[i]);
|
||||
}
|
||||
} else if(task->op == TASK_CHMOD) {
|
||||
unsigned long long ignored_bytes = 0;
|
||||
size_t i;
|
||||
|
||||
ret = 0;
|
||||
if(task->recursive) {
|
||||
for(i = 0; i < task->src_count; i++) {
|
||||
size_t files_before = task->file_count;
|
||||
size_t dirs_before = task->dir_count;
|
||||
|
||||
if(count_task_path_bytes(task, task->srcs[i], task->srcs[i],
|
||||
&ignored_bytes, &task->file_count,
|
||||
&task->dir_count)) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if(files_before == task->file_count && dirs_before == task->dir_count) {
|
||||
task->file_count++;
|
||||
}
|
||||
}
|
||||
total = task->file_count + task->dir_count;
|
||||
} else {
|
||||
total = task->src_count;
|
||||
}
|
||||
if(!ret) {
|
||||
task_set_total(task, total);
|
||||
for(i = 0; i < task->src_count && !ret; i++) {
|
||||
ret = chmod_task_path(task, task->srcs[i], task->chmod_mode,
|
||||
task->recursive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(ret) {
|
||||
@@ -1686,6 +1783,10 @@ task_worker(void *arg) {
|
||||
task_update(task, TASK_CANCELED, task->current[0] ? task->current : task->src,
|
||||
0, "canceled");
|
||||
} else {
|
||||
if(task->op == TASK_CHMOD) {
|
||||
task_set_error_code(task, "chmod_failed",
|
||||
task->current[0] ? task->current : task->src);
|
||||
}
|
||||
task_update(task, TASK_FAILED, task->current[0] ? task->current : task->src,
|
||||
0, strerror(errno));
|
||||
}
|
||||
@@ -1706,8 +1807,8 @@ task_worker(void *arg) {
|
||||
|
||||
static enum MHD_Result
|
||||
create_task_response(struct MHD_Connection *conn, task_op_t op,
|
||||
char **srcs, size_t src_count,
|
||||
const char *dst, int overwrite) {
|
||||
char **srcs, size_t src_count, const char *dst,
|
||||
int overwrite, unsigned int chmod_mode, int recursive) {
|
||||
file_task_t *task = calloc(1, sizeof(file_task_t));
|
||||
strbuf_t b = {0};
|
||||
struct stat st;
|
||||
@@ -1745,6 +1846,8 @@ create_task_response(struct MHD_Connection *conn, task_op_t op,
|
||||
|
||||
task->op = op;
|
||||
task->state = TASK_QUEUED;
|
||||
task->chmod_mode = chmod_mode;
|
||||
task->recursive = recursive;
|
||||
task->srcs = srcs;
|
||||
task->src_count = src_count;
|
||||
snprintf(task->src, sizeof(task->src), "%s%s",
|
||||
@@ -1814,13 +1917,15 @@ api_tasks(struct MHD_Connection *conn) {
|
||||
json_escape(&b, task->error_code);
|
||||
strbuf_append(&b, ",\"error_arg\":");
|
||||
json_escape(&b, task->error_arg);
|
||||
strbuf_printf(&b, ",\"src_count\":%zu,\"total\":%llu,\"done\":%llu,\"speed\":%llu,\"eta\":%llu,\"cancel_requested\":%s,\"created_at\":%lld,\"elapsed\":%lld,\"total_elapsed\":%lld,\"updated_at\":%lld}",
|
||||
task->src_count, task->total, task->done, task->speed, task->eta,
|
||||
strbuf_printf(&b, ",\"src_count\":%zu,\"completed_count\":%zu,\"total\":%llu,\"done\":%llu,\"speed\":%llu,\"eta\":%llu,\"cancel_requested\":%s,\"created_at\":%lld,\"elapsed\":%lld,\"total_elapsed\":%lld,\"updated_at\":%lld}",
|
||||
task->src_count, task->upload_completed,
|
||||
task->total, task->done, task->speed, task->eta,
|
||||
task->cancel_requested ? "true" : "false",
|
||||
(long long)task->created_at,
|
||||
task->transfer_started_at ? (long long)(now - task->transfer_started_at) : 0LL,
|
||||
task->created_at ? (long long)(now - task->created_at) : 0LL,
|
||||
(long long)task->updated_at);
|
||||
if(!task_is_active(task)) task->reported = 1;
|
||||
}
|
||||
strbuf_printf(&b, "],\"now\":%lld,\"completion\":", (long long)now);
|
||||
if(g_last_completion.id) {
|
||||
@@ -1850,7 +1955,7 @@ api_cancel(struct MHD_Connection *conn) {
|
||||
free(idstr);
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task->id == id && task_is_active(task)) {
|
||||
if(task->id == id && task->op != TASK_PKG_INSTALL && task_is_active(task)) {
|
||||
task->cancel_requested = 1;
|
||||
if(task->op == TASK_DOWNLOAD && task->state == TASK_QUEUED) {
|
||||
task->state = TASK_CANCELED;
|
||||
@@ -1871,6 +1976,11 @@ static void *
|
||||
stop_websrv_later(void *arg) {
|
||||
(void)arg;
|
||||
usleep(250000);
|
||||
#ifndef __linux__
|
||||
(void)navigate_to_home();
|
||||
/* Keep this process alive while ShellUI handles the asynchronous URI. */
|
||||
usleep(1000000);
|
||||
#endif
|
||||
websrv_stop();
|
||||
return NULL;
|
||||
}
|
||||
@@ -1907,7 +2017,7 @@ api_copy(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
}
|
||||
}
|
||||
ret = create_task_response(conn, TASK_COPY, paths, count, dst,
|
||||
overwrite && !strcmp(overwrite, "1"));
|
||||
overwrite && !strcmp(overwrite, "1"), 0, 0);
|
||||
free(paths_raw); free(dst); free(overwrite);
|
||||
return ret;
|
||||
}
|
||||
@@ -1926,7 +2036,7 @@ api_move(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, NULL);
|
||||
}
|
||||
ret = create_task_response(conn, TASK_MOVE, paths, count, dst,
|
||||
overwrite && !strcmp(overwrite, "1"));
|
||||
overwrite && !strcmp(overwrite, "1"), 0, 0);
|
||||
free(paths_raw); free(dst); free(overwrite);
|
||||
return ret;
|
||||
}
|
||||
@@ -1948,7 +2058,7 @@ api_delete(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
}
|
||||
ret = create_task_response(conn, TASK_DELETE, paths, count, NULL, 0);
|
||||
ret = create_task_response(conn, TASK_DELETE, paths, count, NULL, 0, 0, 0);
|
||||
free(paths_raw);
|
||||
return ret;
|
||||
}
|
||||
@@ -2009,40 +2119,276 @@ 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;
|
||||
static int
|
||||
parse_permission_mode(const char *text, unsigned int *mode) {
|
||||
size_t i;
|
||||
|
||||
if(!text || strlen(text) != 4 || text[0] != '0') {
|
||||
return -1;
|
||||
}
|
||||
for(i = 1; i < 4; i++) {
|
||||
if(text[i] < '0' || text[i] > '7') {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
*mode = (unsigned int)((text[1] - '0') << 6) |
|
||||
(unsigned int)((text[2] - '0') << 3) |
|
||||
(unsigned int)(text[3] - '0');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
chmod_task_path(file_task_t *task, const char *path,
|
||||
unsigned int mode, int recursive) {
|
||||
struct stat st;
|
||||
char error_code[16];
|
||||
|
||||
if(task_cancel_requested(task)) {
|
||||
return -1;
|
||||
}
|
||||
task_update(task, TASK_RUNNING, path, 0, NULL);
|
||||
if(lstat(path, &st)) {
|
||||
return -1;
|
||||
}
|
||||
if(recursive && S_ISDIR(st.st_mode)) {
|
||||
DIR *dir = opendir(path);
|
||||
struct dirent *entry;
|
||||
|
||||
if(!dir) {
|
||||
return -1;
|
||||
}
|
||||
while((entry = readdir(dir))) {
|
||||
char child[PATH_MAX];
|
||||
int error;
|
||||
|
||||
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
if(task_cancel_requested(task) ||
|
||||
path_join(child, sizeof(child), path, entry->d_name) ||
|
||||
lstat(child, &st)) {
|
||||
error = errno;
|
||||
closedir(dir);
|
||||
errno = error;
|
||||
return -1;
|
||||
}
|
||||
/* A symlink has no portable mode of its own; never change its target. */
|
||||
if(!S_ISLNK(st.st_mode) && chmod_task_path(task, child, mode, 1)) {
|
||||
error = errno;
|
||||
closedir(dir);
|
||||
errno = error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
task_update(task, TASK_RUNNING, path, 0, NULL);
|
||||
if(chmod_path_mode(path, mode)) {
|
||||
return -1;
|
||||
}
|
||||
task_update(task, TASK_RUNNING, path, 1, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
api_chmod(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
char *paths_raw = body_form_value(body, body_size, "paths");
|
||||
char *mode_text = body_form_value(body, body_size, "mode");
|
||||
char *recursive_text = body_form_value(body, body_size, "recursive");
|
||||
char **paths = NULL;
|
||||
size_t count = 0;
|
||||
unsigned int mode;
|
||||
enum MHD_Result ret;
|
||||
|
||||
if(!paths_raw || parse_paths(paths_raw, &paths, &count)) {
|
||||
free(paths_raw); free(mode_text); free(recursive_text);
|
||||
free_paths(paths, count);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(parse_permission_mode(mode_text, &mode)) {
|
||||
free(paths_raw); free(mode_text); free(recursive_text);
|
||||
free_paths(paths, count);
|
||||
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"invalid permission mode",
|
||||
"invalid_mode", NULL);
|
||||
}
|
||||
ret = create_task_response(conn, TASK_CHMOD, paths, count, NULL, 0, mode,
|
||||
recursive_text && !strcmp(recursive_text, "1"));
|
||||
free(paths_raw); free(mode_text); free(recursive_text);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifndef __linux__
|
||||
static file_task_t *
|
||||
next_pkg_task_locked(void) {
|
||||
file_task_t *task;
|
||||
file_task_t *next = NULL;
|
||||
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task->op == TASK_PKG_INSTALL && task->state == TASK_QUEUED &&
|
||||
(!next || task->id < next->id)) next = task;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
static void *
|
||||
pkg_task_worker(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
while(1) {
|
||||
file_task_t *task;
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
while(!(task = next_pkg_task_locked())) {
|
||||
pthread_cond_wait(&g_pkg_tasks_cond, &g_tasks_lock);
|
||||
}
|
||||
task->state = TASK_RUNNING;
|
||||
snprintf(task->current, sizeof(task->current), "%s", task->src);
|
||||
task->updated_at = time(NULL);
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
result = pkg_installer_install(task->srcs[0]);
|
||||
if(result) {
|
||||
char error_code[16];
|
||||
snprintf(error_code, sizeof(error_code), "0x%08X", (unsigned int)result);
|
||||
task_set_error_code(task, "pkg_install_failed", error_code);
|
||||
task_update(task, TASK_FAILED, task->src, 0,
|
||||
"package installation failed");
|
||||
} else {
|
||||
task_update(task, TASK_DONE, task->src, 0, NULL);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static file_task_t *
|
||||
active_pkg_task_locked(const char *path) {
|
||||
file_task_t *task;
|
||||
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task->op == TASK_PKG_INSTALL && task_is_active(task) &&
|
||||
!strcmp(task->srcs[0], path)) return task;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int
|
||||
enqueue_pkg_tasks(char **paths, size_t count, unsigned long *ids) {
|
||||
#ifdef __linux__
|
||||
(void)paths; (void)count; (void)ids;
|
||||
return PKG_INSTALL_UNSUPPORTED;
|
||||
#else
|
||||
file_task_t **pending = calloc(count, sizeof(*pending));
|
||||
int result = 0;
|
||||
|
||||
if(!pending) return -1;
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
file_task_t *task = calloc(1, sizeof(*task));
|
||||
if(!task || !(task->srcs = calloc(1, sizeof(*task->srcs))) ||
|
||||
!(task->srcs[0] = strdup(paths[i]))) {
|
||||
free_task(task);
|
||||
result = -1;
|
||||
goto done;
|
||||
}
|
||||
task->op = TASK_PKG_INSTALL;
|
||||
task->state = TASK_QUEUED;
|
||||
task->src_count = 1;
|
||||
snprintf(task->src, sizeof(task->src), "%s", paths[i]);
|
||||
task->created_at = time(NULL);
|
||||
task->updated_at = task->created_at;
|
||||
pending[i] = task;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
if(!g_pkg_worker_started) {
|
||||
result = pthread_create(&g_pkg_worker_thread, NULL, pkg_task_worker, NULL);
|
||||
if(!result) {
|
||||
pthread_detach(g_pkg_worker_thread);
|
||||
g_pkg_worker_started = 1;
|
||||
}
|
||||
}
|
||||
if(!result) {
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
file_task_t *existing = active_pkg_task_locked(paths[i]);
|
||||
if(existing) {
|
||||
ids[i] = existing->id;
|
||||
free_task(pending[i]);
|
||||
} else {
|
||||
pending[i]->id = g_next_task_id++;
|
||||
ids[i] = pending[i]->id;
|
||||
pending[i]->next = g_tasks;
|
||||
g_tasks = pending[i];
|
||||
}
|
||||
pending[i] = NULL;
|
||||
}
|
||||
pthread_cond_signal(&g_pkg_tasks_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
done:
|
||||
for(size_t i = 0; i < count; i++) free_task(pending[i]);
|
||||
free(pending);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
api_install_pkg(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size) {
|
||||
char *paths_raw = body_form_value(body, body_size, "paths");
|
||||
char **paths = NULL;
|
||||
unsigned long *ids = NULL;
|
||||
size_t count = 0;
|
||||
strbuf_t json = {0};
|
||||
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(!paths_raw || parse_paths(paths_raw, &paths, &count) ||
|
||||
!(ids = calloc(count, sizeof(*ids)))) {
|
||||
free(paths_raw); free_paths(paths, count); free(ids);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
const char *extension = strrchr(paths[i], '.');
|
||||
struct stat st;
|
||||
|
||||
if(!extension || strcasecmp(extension, ".pkg")) {
|
||||
enum MHD_Result ret = send_json_error_detail(
|
||||
conn, MHD_HTTP_BAD_REQUEST, "file is not a PKG package",
|
||||
"pkg_type_invalid", paths[i]);
|
||||
free(paths_raw); free_paths(paths, count); free(ids);
|
||||
return ret;
|
||||
}
|
||||
if(stat(paths[i], &st) || !S_ISREG(st.st_mode)) {
|
||||
free(paths_raw); free_paths(paths, count); free(ids);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
}
|
||||
|
||||
result = pkg_installer_install(path);
|
||||
free(path);
|
||||
result = enqueue_pkg_tasks(paths, count, ids);
|
||||
free(paths_raw); free_paths(paths, count);
|
||||
if(result == PKG_INSTALL_UNSUPPORTED) {
|
||||
free(ids);
|
||||
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);
|
||||
free(ids);
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"could not queue package installation");
|
||||
}
|
||||
return send_json_ok(conn);
|
||||
|
||||
/* Keep the action from completing instantly after an accidental press. */
|
||||
usleep(600000);
|
||||
strbuf_append(&json, "{\"ok\":true,\"task_ids\":[");
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
if(i) strbuf_append(&json, ",");
|
||||
strbuf_printf(&json, "%lu", ids[i]);
|
||||
}
|
||||
strbuf_append(&json, "]}");
|
||||
free(ids);
|
||||
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
@@ -2066,10 +2412,25 @@ 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/chmod")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_POST) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_chmod(conn, body, body_size);
|
||||
}
|
||||
if(!strcmp(url, "/api/pkg-info")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_GET) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_pkg_info(conn);
|
||||
}
|
||||
if(!strcmp(url, "/api/pkg-icon")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_GET) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_pkg_icon(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);
|
||||
api_install_pkg(conn, body, body_size);
|
||||
}
|
||||
if(!strcmp(url, "/api/text")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_GET) ?
|
||||
|
||||
@@ -13,8 +13,10 @@ typedef enum task_op {
|
||||
TASK_COPY,
|
||||
TASK_MOVE,
|
||||
TASK_DELETE,
|
||||
TASK_CHMOD,
|
||||
TASK_DOWNLOAD,
|
||||
TASK_UPLOAD,
|
||||
TASK_PKG_INSTALL,
|
||||
} task_op_t;
|
||||
|
||||
typedef enum task_state {
|
||||
@@ -44,6 +46,9 @@ typedef struct file_task {
|
||||
size_t src_count;
|
||||
size_t file_count;
|
||||
size_t dir_count;
|
||||
size_t upload_completed;
|
||||
unsigned int chmod_mode;
|
||||
int recursive;
|
||||
unsigned long long total;
|
||||
unsigned long long done;
|
||||
unsigned long long speed;
|
||||
@@ -54,6 +59,8 @@ typedef struct file_task {
|
||||
unsigned int eta_sample_next;
|
||||
unsigned int eta_sample_count;
|
||||
int cancel_requested;
|
||||
int reported; /* Terminal state has been included in /api/tasks. */
|
||||
unsigned int active_streams;
|
||||
time_t created_at;
|
||||
time_t transfer_started_at;
|
||||
time_t updated_at;
|
||||
@@ -88,6 +95,7 @@ enum MHD_Result send_buffer(struct MHD_Connection *conn, unsigned int status,
|
||||
char *data, const char *mime);
|
||||
|
||||
int ensure_parent_dirs(const char *base, const char *rel);
|
||||
int chmod_path_mode(const char *path, unsigned int mode);
|
||||
int chmod_path_0777(const char *path);
|
||||
int fchmod_0777(int fd);
|
||||
int ignore_chmod_error(int err);
|
||||
|
||||
@@ -65,6 +65,21 @@ fd_has_unix_modes(int fd) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
chmod_path_mode(const char *path, unsigned int mode) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
return 0;
|
||||
}
|
||||
if(chmod(path, (mode_t)(mode & 0777))) {
|
||||
/* A filesystem that does not implement Unix modes is compatible with the
|
||||
paste behavior. Real permission and read-only errors must reach the UI. */
|
||||
if(errno != ENOTSUP && errno != EINVAL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
chmod_path_0777(const char *path) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
#include "pkg_info.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define PKG_CNT_MAGIC 0x7f434e54u
|
||||
#define PKG_FIH_MAGIC 0x7f464948u
|
||||
#define PKG_LIH_MAGIC 0x7f4c4948u
|
||||
#define PKG_HEADER_SIZE 0xa0
|
||||
#define PKG_ENTRY_SIZE 0x20
|
||||
#define PKG_ENTRY_PARAM_SFO 0x1000u
|
||||
#define PKG_ENTRY_ICON0_PNG 0x1200u
|
||||
#define PKG_ENTRY_ICON0_LOCALIZED_LAST 0x121fu
|
||||
#define PKG_ENTRY_PARAM_JSON 0x2000u
|
||||
#define PKG_ENTRY_FLAG_ENCRYPTED 0x80000000u
|
||||
#define PKG_ENTRY_MAX 65536u
|
||||
#define PKG_PARAM_MAX (4u * 1024u * 1024u)
|
||||
#define PKG_ICON_MAX (32u * 1024u * 1024u)
|
||||
#define SFO_MAGIC 0x46535000u
|
||||
#define SFO_ENTRY_SIZE 0x10
|
||||
#define SFO_ENTRY_MAX 256u
|
||||
#define JSON_TOKEN_MAX 4096u
|
||||
|
||||
typedef enum pkg_param_type {
|
||||
PKG_PARAM_SFO,
|
||||
PKG_PARAM_JSON,
|
||||
} pkg_param_type_t;
|
||||
|
||||
typedef struct pkg_source {
|
||||
int fd;
|
||||
uint64_t size;
|
||||
uint32_t content_type;
|
||||
uint32_t content_flags;
|
||||
char content_id[37];
|
||||
uint64_t param_offset;
|
||||
uint32_t param_size;
|
||||
pkg_param_type_t param_type;
|
||||
uint64_t icon_offset;
|
||||
uint32_t icon_size;
|
||||
} pkg_source_t;
|
||||
|
||||
typedef enum json_token_type {
|
||||
JSON_OBJECT,
|
||||
JSON_ARRAY,
|
||||
JSON_STRING,
|
||||
JSON_PRIMITIVE,
|
||||
} json_token_type_t;
|
||||
|
||||
typedef struct json_token {
|
||||
json_token_type_t type;
|
||||
size_t start;
|
||||
size_t end;
|
||||
int parent;
|
||||
} json_token_t;
|
||||
|
||||
static uint16_t
|
||||
read_le16(const unsigned char *p) {
|
||||
return (uint16_t)p[0] | (uint16_t)p[1] << 8;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_le32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] | (uint32_t)p[1] << 8 |
|
||||
(uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
read_le64(const unsigned char *p) {
|
||||
return (uint64_t)read_le32(p) | (uint64_t)read_le32(p + 4) << 32;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_be32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 |
|
||||
(uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static int
|
||||
range_valid(uint64_t offset, uint64_t size, uint64_t file_size) {
|
||||
return offset <= file_size && size <= file_size - offset;
|
||||
}
|
||||
|
||||
static int
|
||||
read_at(int fd, void *buffer, size_t size, uint64_t offset) {
|
||||
unsigned char *p = buffer;
|
||||
size_t done = 0;
|
||||
|
||||
while(done < size) {
|
||||
ssize_t n = pread(fd, p + done, size - done, (off_t)(offset + done));
|
||||
if(n <= 0) {
|
||||
if(n < 0 && errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
done += (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
png_signature_valid(const unsigned char *data, size_t size) {
|
||||
static const unsigned char signature[] = "\x89PNG\r\n\x1a\n";
|
||||
|
||||
return size >= sizeof(signature) - 1 &&
|
||||
!memcmp(data, signature, sizeof(signature) - 1);
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_icon_is_png(const pkg_source_t *pkg, uint64_t offset, uint32_t size) {
|
||||
unsigned char signature[8];
|
||||
|
||||
return size >= sizeof(signature) &&
|
||||
!read_at(pkg->fd, signature, sizeof(signature), offset) &&
|
||||
png_signature_valid(signature, sizeof(signature));
|
||||
}
|
||||
|
||||
static void
|
||||
pkg_source_close(pkg_source_t *pkg) {
|
||||
if(pkg->fd >= 0) close(pkg->fd);
|
||||
pkg->fd = -1;
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_source_open(const char *path, pkg_source_t *pkg) {
|
||||
unsigned char header[PKG_HEADER_SIZE];
|
||||
unsigned char *table = NULL;
|
||||
struct stat st;
|
||||
uint64_t container_offset = 0;
|
||||
uint64_t container_size;
|
||||
uint32_t magic;
|
||||
uint32_t entry_count;
|
||||
uint32_t table_offset;
|
||||
size_t table_size;
|
||||
int ret = -1;
|
||||
|
||||
memset(pkg, 0, sizeof(*pkg));
|
||||
pkg->fd = -1;
|
||||
if((pkg->fd = open(path, O_RDONLY)) < 0 || fstat(pkg->fd, &st) ||
|
||||
!S_ISREG(st.st_mode) || st.st_size < PKG_HEADER_SIZE ||
|
||||
read_at(pkg->fd, header, sizeof(header), 0)) goto done;
|
||||
|
||||
pkg->size = (uint64_t)st.st_size;
|
||||
magic = read_be32(header);
|
||||
if(magic == PKG_FIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x58);
|
||||
} else if(magic == PKG_LIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x30);
|
||||
} else if(magic != PKG_CNT_MAGIC) {
|
||||
goto done;
|
||||
}
|
||||
if(container_offset) {
|
||||
if(!range_valid(container_offset, sizeof(header), pkg->size) ||
|
||||
read_at(pkg->fd, header, sizeof(header), container_offset) ||
|
||||
read_be32(header) != PKG_CNT_MAGIC) goto done;
|
||||
}
|
||||
container_size = pkg->size - container_offset;
|
||||
|
||||
memcpy(pkg->content_id, header + 0x40, 36);
|
||||
pkg->content_id[36] = 0;
|
||||
pkg->content_type = read_be32(header + 0x74);
|
||||
pkg->content_flags = read_be32(header + 0x78);
|
||||
entry_count = read_be32(header + 0x10);
|
||||
table_offset = read_be32(header + 0x18);
|
||||
if(!entry_count || entry_count > PKG_ENTRY_MAX) goto done;
|
||||
table_size = (size_t)entry_count * PKG_ENTRY_SIZE;
|
||||
if(!range_valid(table_offset, table_size, container_size) ||
|
||||
!(table = malloc(table_size)) ||
|
||||
read_at(pkg->fd, table, table_size, container_offset + table_offset)) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
for(uint32_t i = 0; i < entry_count; i++) {
|
||||
const unsigned char *entry = table + (size_t)i * PKG_ENTRY_SIZE;
|
||||
uint32_t id = read_be32(entry);
|
||||
uint32_t flags1 = read_be32(entry + 0x08);
|
||||
uint32_t offset = read_be32(entry + 0x10);
|
||||
uint32_t size = read_be32(entry + 0x14);
|
||||
int encrypted = (flags1 & PKG_ENTRY_FLAG_ENCRYPTED) != 0;
|
||||
|
||||
if(!range_valid(offset, size, container_size)) goto done;
|
||||
if(encrypted || !size) continue;
|
||||
if(id == PKG_ENTRY_PARAM_SFO && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_SFO;
|
||||
} else if(id == PKG_ENTRY_PARAM_JSON && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_JSON;
|
||||
} else if(id >= PKG_ENTRY_ICON0_PNG &&
|
||||
id <= PKG_ENTRY_ICON0_LOCALIZED_LAST &&
|
||||
size <= PKG_ICON_MAX &&
|
||||
(id == PKG_ENTRY_ICON0_PNG || !pkg->icon_size) &&
|
||||
pkg_icon_is_png(pkg, container_offset + offset, size)) {
|
||||
/* Prefer icon0.png; otherwise keep the first valid localized icon. */
|
||||
pkg->icon_offset = container_offset + offset;
|
||||
pkg->icon_size = size;
|
||||
}
|
||||
}
|
||||
if(!pkg->param_size) goto done;
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
free(table);
|
||||
if(ret) {
|
||||
errno = EINVAL;
|
||||
pkg_source_close(pkg);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
append_sfo_fields(strbuf_t *json, const unsigned char *sfo, size_t size) {
|
||||
uint32_t key_offset;
|
||||
uint32_t value_offset;
|
||||
uint32_t count;
|
||||
uint64_t index_end;
|
||||
int first = 1;
|
||||
|
||||
if(size < 20 || read_le32(sfo) != SFO_MAGIC) return -1;
|
||||
key_offset = read_le32(sfo + 8);
|
||||
value_offset = read_le32(sfo + 12);
|
||||
count = read_le32(sfo + 16);
|
||||
index_end = 20 + (uint64_t)count * SFO_ENTRY_SIZE;
|
||||
if(count > SFO_ENTRY_MAX || index_end > size || key_offset < index_end ||
|
||||
value_offset < key_offset || value_offset > size) return -1;
|
||||
|
||||
strbuf_append(json, "[");
|
||||
for(uint32_t i = 0; i < count; i++) {
|
||||
const unsigned char *entry = sfo + 20 + (size_t)i * SFO_ENTRY_SIZE;
|
||||
uint16_t name_offset = read_le16(entry);
|
||||
uint16_t format = read_le16(entry + 2);
|
||||
uint32_t value_size = read_le32(entry + 4);
|
||||
uint32_t value_max = read_le32(entry + 8);
|
||||
uint32_t data_offset = read_le32(entry + 12);
|
||||
uint64_t key_pos = (uint64_t)key_offset + name_offset;
|
||||
uint64_t value_pos = (uint64_t)value_offset + data_offset;
|
||||
const unsigned char *key_end;
|
||||
char number[32];
|
||||
char *value = NULL;
|
||||
|
||||
if(key_pos >= value_offset || value_pos > size ||
|
||||
(value_max && value_size > value_max)) continue;
|
||||
key_end = memchr(sfo + key_pos, 0, value_offset - (size_t)key_pos);
|
||||
if(!key_end) continue;
|
||||
if(format == 0x0204) {
|
||||
size_t length;
|
||||
if(!value_size || !range_valid(value_pos, value_size, size)) continue;
|
||||
length = strnlen((const char *)sfo + value_pos, value_size);
|
||||
if(!(value = malloc(length + 1))) continue;
|
||||
memcpy(value, sfo + value_pos, length);
|
||||
value[length] = 0;
|
||||
} else if(format == 0x0404 && range_valid(value_pos, 4, size)) {
|
||||
snprintf(number, sizeof(number), "%u", read_le32(sfo + value_pos));
|
||||
value = strdup(number);
|
||||
}
|
||||
if(!value) continue;
|
||||
if(!first) strbuf_append(json, ",");
|
||||
first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, (const char *)sfo + key_pos);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
json_escape(json, value);
|
||||
strbuf_append(json, "}");
|
||||
free(value);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_add_token(json_token_t *tokens, size_t *count, json_token_type_t type,
|
||||
size_t start, int parent) {
|
||||
if(*count >= JSON_TOKEN_MAX) return -1;
|
||||
tokens[*count] = (json_token_t){
|
||||
.type = type, .start = start, .end = 0, .parent = parent
|
||||
};
|
||||
return (int)(*count)++;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_json_tokens(const unsigned char *data, size_t size, json_token_t *tokens,
|
||||
size_t *token_count) {
|
||||
int parent = -1;
|
||||
size_t count = 0;
|
||||
|
||||
for(size_t i = 0; i < size; i++) {
|
||||
unsigned char c = data[i];
|
||||
if(c == '{' || c == '[') {
|
||||
int index = json_add_token(tokens, &count,
|
||||
c == '{' ? JSON_OBJECT : JSON_ARRAY,
|
||||
i, parent);
|
||||
if(index < 0) return -1;
|
||||
parent = index;
|
||||
} else if(c == '}' || c == ']') {
|
||||
json_token_type_t type = c == '}' ? JSON_OBJECT : JSON_ARRAY;
|
||||
if(parent < 0 || tokens[parent].type != type) return -1;
|
||||
tokens[parent].end = i + 1;
|
||||
parent = tokens[parent].parent;
|
||||
} else if(c == '"') {
|
||||
int index = json_add_token(tokens, &count, JSON_STRING, i + 1, parent);
|
||||
if(index < 0) return -1;
|
||||
for(i++; i < size && data[i] != '"'; i++) {
|
||||
if(data[i] < 0x20) return -1;
|
||||
if(data[i] == '\\') {
|
||||
if(++i >= size || !strchr("\"\\/bfnrtu", data[i])) return -1;
|
||||
if(data[i] == 'u') {
|
||||
for(unsigned int n = 0; n < 4; n++) {
|
||||
if(++i >= size || !((data[i] >= '0' && data[i] <= '9') ||
|
||||
(data[i] >= 'a' && data[i] <= 'f') ||
|
||||
(data[i] >= 'A' && data[i] <= 'F'))) return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(i >= size) return -1;
|
||||
tokens[index].end = i;
|
||||
} else if(c == ':' || c == ',' || c == ' ' || c == '\t' ||
|
||||
c == '\r' || c == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
int index = json_add_token(tokens, &count, JSON_PRIMITIVE, i, parent);
|
||||
if(index < 0) return -1;
|
||||
while(i < size && data[i] != ',' && data[i] != ']' && data[i] != '}' &&
|
||||
data[i] != ' ' && data[i] != '\t' && data[i] != '\r' &&
|
||||
data[i] != '\n') i++;
|
||||
if(tokens[index].start == i) return -1;
|
||||
tokens[index].end = i;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if(parent >= 0 || !count || tokens[0].type != JSON_OBJECT ||
|
||||
!tokens[0].end) return -1;
|
||||
*token_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_token_equals(const unsigned char *data, const json_token_t *token,
|
||||
const char *text) {
|
||||
size_t length = strlen(text);
|
||||
return token->type == JSON_STRING && token->end - token->start == length &&
|
||||
!memcmp(data + token->start, text, length);
|
||||
}
|
||||
|
||||
static int
|
||||
json_next_child(const json_token_t *tokens, size_t count, int parent,
|
||||
size_t start) {
|
||||
for(size_t i = start; i < count; i++) {
|
||||
if(tokens[i].parent == parent) return (int)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const char *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(json_token_equals(data, &tokens[item], key)) return value;
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value_token(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const json_token_t *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
size_t key_size = key->end - key->start;
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
tokens[item].end - tokens[item].start == key_size &&
|
||||
!memcmp(data + tokens[item].start, data + key->start, key_size)) {
|
||||
return value;
|
||||
}
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_token_string(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *token) {
|
||||
strbuf_append(json, "\"");
|
||||
strbuf_printf(json, "%.*s", (int)(token->end - token->start),
|
||||
data + token->start);
|
||||
strbuf_append(json, "\"");
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_field(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *key, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
append_json_token_string(json, data, key);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static void
|
||||
append_named_json_field(strbuf_t *json, const char *name,
|
||||
const unsigned char *data, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, name);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static int
|
||||
append_param_json_fields(strbuf_t *json, const unsigned char *data,
|
||||
size_t size) {
|
||||
json_token_t *tokens = calloc(JSON_TOKEN_MAX, sizeof(*tokens));
|
||||
size_t count = 0;
|
||||
int first = 1;
|
||||
int localized;
|
||||
int title = -1;
|
||||
|
||||
if(!tokens || parse_json_tokens(data, size, tokens, &count)) {
|
||||
free(tokens);
|
||||
return -1;
|
||||
}
|
||||
strbuf_append(json, "[");
|
||||
|
||||
localized = json_object_value(data, tokens, count, 0, "localizedParameters");
|
||||
if(localized >= 0 && tokens[localized].type == JSON_OBJECT) {
|
||||
int language = json_object_value(data, tokens, count, localized,
|
||||
"defaultLanguage");
|
||||
int language_data = language >= 0 && tokens[language].type == JSON_STRING ?
|
||||
json_object_value_token(data, tokens, count, localized,
|
||||
&tokens[language]) : -1;
|
||||
if(language_data >= 0 && tokens[language_data].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, language_data,
|
||||
"titleName");
|
||||
}
|
||||
if(title < 0) {
|
||||
int english = json_object_value(data, tokens, count, localized, "en-US");
|
||||
if(english >= 0 && tokens[english].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, english, "titleName");
|
||||
}
|
||||
}
|
||||
if(title < 0) {
|
||||
int item = json_next_child(tokens, count, localized,
|
||||
(size_t)localized + 1);
|
||||
while(item >= 0 && title < 0) {
|
||||
int value = json_next_child(tokens, count, localized,
|
||||
(size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[value].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, value, "titleName");
|
||||
}
|
||||
item = json_next_child(tokens, count, localized, (size_t)value + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(title >= 0 && tokens[title].type == JSON_STRING) {
|
||||
append_named_json_field(json, "titleName", data, &tokens[title], &first);
|
||||
}
|
||||
|
||||
for(int item = json_next_child(tokens, count, 0, 1); item >= 0;) {
|
||||
int value = json_next_child(tokens, count, 0, (size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
(tokens[value].type == JSON_STRING ||
|
||||
tokens[value].type == JSON_PRIMITIVE)) {
|
||||
append_json_field(json, data, &tokens[item], &tokens[value], &first);
|
||||
}
|
||||
item = json_next_child(tokens, count, 0, (size_t)value + 1);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
free(tokens);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
pkg_error(struct MHD_Connection *conn, const char *path) {
|
||||
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"could not read package information",
|
||||
"pkg_info_failed", path);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_info(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *param;
|
||||
strbuf_t json = {0};
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
if(!(param = malloc(pkg.param_size)) ||
|
||||
read_at(pkg.fd, param, pkg.param_size, pkg.param_offset)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
strbuf_printf(&json,
|
||||
"{\"ok\":true,\"size\":%llu,\"content_id\":",
|
||||
(unsigned long long)pkg.size);
|
||||
json_escape(&json, pkg.content_id);
|
||||
strbuf_printf(&json,
|
||||
",\"platform\":\"%s\",\"content_type\":%u,"
|
||||
"\"content_flags\":%u,\"has_icon\":%s,\"fields\":",
|
||||
pkg.param_type == PKG_PARAM_JSON ? "PS5" : "PS4",
|
||||
pkg.content_type, pkg.content_flags,
|
||||
pkg.icon_size ? "true" : "false");
|
||||
if((pkg.param_type == PKG_PARAM_JSON &&
|
||||
append_param_json_fields(&json, param, pkg.param_size)) ||
|
||||
(pkg.param_type == PKG_PARAM_SFO &&
|
||||
append_sfo_fields(&json, param, pkg.param_size))) {
|
||||
enum MHD_Result ret;
|
||||
free(json.data);
|
||||
ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
strbuf_append(&json, "}");
|
||||
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_icon(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *icon;
|
||||
struct MHD_Response *response;
|
||||
enum MHD_Result ret;
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg) || !pkg.icon_size ||
|
||||
!(icon = malloc(pkg.icon_size)) ||
|
||||
read_at(pkg.fd, icon, pkg.icon_size, pkg.icon_offset)) {
|
||||
if(path && pkg.fd >= 0) pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "package icon not found");
|
||||
}
|
||||
if(!png_signature_valid(icon, pkg.icon_size)) {
|
||||
free(icon);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND,
|
||||
"package icon not found");
|
||||
}
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
|
||||
response = MHD_create_response_from_buffer(pkg.icon_size, icon,
|
||||
MHD_RESPMEM_MUST_FREE);
|
||||
if(!response) {
|
||||
free(icon);
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, "image/png");
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, response);
|
||||
MHD_destroy_response(response);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
enum MHD_Result api_pkg_info(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_pkg_icon(struct MHD_Connection *conn);
|
||||
+15
-4
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -14,8 +15,13 @@ typedef struct pkg_metadata {
|
||||
const char *content_id;
|
||||
const char *content_name;
|
||||
const char *icon_url;
|
||||
uint32_t slot;
|
||||
uint32_t is_playgo_enabled;
|
||||
} pkg_metadata_t;
|
||||
|
||||
_Static_assert(sizeof(pkg_metadata_t) == 0x38,
|
||||
"sceAppInstUtil metadata ABI mismatch");
|
||||
|
||||
typedef struct pkg_info {
|
||||
char content_id[48];
|
||||
int type;
|
||||
@@ -23,12 +29,15 @@ typedef struct pkg_info {
|
||||
} pkg_info_t;
|
||||
|
||||
typedef struct playgo_info {
|
||||
char lang[8][30];
|
||||
char scenario_ids[3][64];
|
||||
char content_ids[64];
|
||||
char languages[30][8];
|
||||
char scenario_ids[64][3];
|
||||
char content_ids[64][48];
|
||||
long unknown[810];
|
||||
} playgo_info_t;
|
||||
|
||||
_Static_assert(sizeof(playgo_info_t) == 0x2700,
|
||||
"sceAppInstUtil PlayGoInfo ABI mismatch");
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
|
||||
playgo_info_t *);
|
||||
@@ -68,7 +77,9 @@ pkg_installer_install(const char *path) {
|
||||
.playgo_scenario_id = "",
|
||||
.content_id = "",
|
||||
.content_name = "",
|
||||
.icon_url = ""
|
||||
.icon_url = "",
|
||||
.slot = 0,
|
||||
.is_playgo_enabled = 0
|
||||
};
|
||||
pkg_info_t pkg_info = {0};
|
||||
playgo_info_t playgo_info = {0};
|
||||
|
||||
+5
-2
@@ -20,8 +20,10 @@ task_op_name(task_op_t op) {
|
||||
case TASK_COPY: return "copy";
|
||||
case TASK_MOVE: return "move";
|
||||
case TASK_DELETE: return "delete";
|
||||
case TASK_CHMOD: return "chmod";
|
||||
case TASK_DOWNLOAD: return "download";
|
||||
case TASK_UPLOAD: return "upload";
|
||||
case TASK_PKG_INSTALL: return "pkg_install";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -48,7 +50,7 @@ has_active_task_locked(void) {
|
||||
file_task_t *task;
|
||||
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task_is_active(task)) {
|
||||
if(task->op != TASK_PKG_INSTALL && task_is_active(task)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,8 @@ remove_finished_tasks_locked(void) {
|
||||
while(*link) {
|
||||
file_task_t *task = *link;
|
||||
|
||||
if(task_is_active(task)) {
|
||||
if(task_is_active(task) || task->active_streams ||
|
||||
(task->op == TASK_PKG_INSTALL && !task->reported)) {
|
||||
link = &task->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
+142
-40
@@ -14,9 +14,13 @@
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
|
||||
#define UPLOAD_BUFFER_SIZE (1024 * 1024)
|
||||
|
||||
typedef struct upload_context {
|
||||
file_task_t *task;
|
||||
int fd;
|
||||
char *buffer;
|
||||
size_t buffered;
|
||||
char temp[PATH_MAX];
|
||||
char target[PATH_MAX];
|
||||
unsigned long long expected;
|
||||
@@ -24,8 +28,51 @@ typedef struct upload_context {
|
||||
int failed;
|
||||
int error;
|
||||
int task_done;
|
||||
const char *stage;
|
||||
char error_message[256];
|
||||
} upload_context_t;
|
||||
|
||||
static const char *
|
||||
upload_error_message(upload_context_t *ctx) {
|
||||
if(!ctx->error_message[0]) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s failed%s%s: %s",
|
||||
ctx->stage ? ctx->stage : "upload",
|
||||
ctx->target[0] ? " for " : "",
|
||||
ctx->target[0] ? ctx->target : "",
|
||||
strerror(ctx->error ? ctx->error : EIO));
|
||||
}
|
||||
return ctx->error_message;
|
||||
}
|
||||
|
||||
static int
|
||||
upload_flush(upload_context_t *ctx) {
|
||||
size_t written = 0;
|
||||
|
||||
while(written < ctx->buffered) {
|
||||
ssize_t n;
|
||||
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
n = write(ctx->fd, ctx->buffer + written, ctx->buffered - written);
|
||||
if(n <= 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EIO;
|
||||
return -1;
|
||||
}
|
||||
written += (size_t)n;
|
||||
ctx->written += (unsigned long long)n;
|
||||
}
|
||||
if(ctx->task && written) {
|
||||
task_update(ctx->task, TASK_RUNNING, ctx->target,
|
||||
(unsigned long long)written, NULL);
|
||||
}
|
||||
ctx->buffered = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_task(file_task_t *task, task_state_t state, const char *current,
|
||||
const char *error) {
|
||||
@@ -48,11 +95,29 @@ finish_upload_task(file_task_t *task, task_state_t state, const char *current,
|
||||
if(error) {
|
||||
snprintf(task->error, sizeof(task->error), "%s", error);
|
||||
}
|
||||
if(state == TASK_FAILED) {
|
||||
snprintf(task->error_code, sizeof(task->error_code), "upload_failed");
|
||||
snprintf(task->error_arg, sizeof(task->error_arg), "%s",
|
||||
current ? current : task->src);
|
||||
}
|
||||
task->updated_at = completed_at;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_context_error(upload_context_t *ctx) {
|
||||
if(!ctx->task || ctx->task_done) {
|
||||
return;
|
||||
}
|
||||
upload_error_message(ctx);
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message);
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
|
||||
static file_task_t *
|
||||
upload_task_from_conn(struct MHD_Connection *conn) {
|
||||
char *idstr = request_value(conn, "X-WFM-Task-ID", "task_id");
|
||||
@@ -267,12 +332,18 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
|
||||
return -1;
|
||||
}
|
||||
ctx->fd = -1;
|
||||
ctx->stage = "preparing upload";
|
||||
*upload_ctx = ctx;
|
||||
|
||||
if(size_text) {
|
||||
ctx->expected = strtoull(size_text, NULL, 10);
|
||||
}
|
||||
ctx->task = task;
|
||||
if(task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->active_streams++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
if(task && task_cancel_requested(task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
@@ -283,17 +354,20 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
|
||||
ctx->error = EBUSY;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "validating target path";
|
||||
if(!base || !rel || path_join_relative(ctx->target, sizeof(ctx->target),
|
||||
base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EINVAL;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating target folders";
|
||||
if(ensure_parent_dirs(base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target path";
|
||||
if(!lstat(ctx->target, &st)) {
|
||||
if(S_ISDIR(st.st_mode) || !overwrite || strcmp(overwrite, "1")) {
|
||||
ctx->failed = 1;
|
||||
@@ -305,19 +379,26 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target permissions";
|
||||
if(check_target_writable(ctx->target, &checked_dirs, &checked_dir_count,
|
||||
error, sizeof(error), code, sizeof(code),
|
||||
arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "target is not writable");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target space";
|
||||
if(check_target_space(ctx->target, ctx->expected, error, sizeof(error),
|
||||
code, sizeof(code), arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "not enough target space");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : ENOSPC;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating temporary path";
|
||||
n = snprintf(ctx->temp, sizeof(ctx->temp), "%s.wfm-upload-%ld-%lld.tmp",
|
||||
ctx->target, (long)getpid(), (long long)time(NULL));
|
||||
if(n < 0 || (size_t)n >= sizeof(ctx->temp)) {
|
||||
@@ -325,24 +406,25 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
|
||||
ctx->error = ENAMETOOLONG;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "opening temporary file";
|
||||
ctx->fd = open(ctx->temp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if(ctx->fd < 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "allocating upload buffer";
|
||||
if(!(ctx->buffer = malloc(UPLOAD_BUFFER_SIZE))) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
fail:
|
||||
free_paths(checked_dirs, checked_dir_count);
|
||||
free(base); free(rel); free(overwrite); free(size_text);
|
||||
if(ctx->failed) {
|
||||
if(ctx->task) {
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO));
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
finish_upload_context_error(ctx);
|
||||
if(ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
ctx->fd = -1;
|
||||
@@ -353,13 +435,13 @@ fail:
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return -1;
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
filemgr_upload_data(void *upload_ctx, const char *data, size_t size) {
|
||||
upload_context_t *ctx = upload_ctx;
|
||||
size_t written = 0;
|
||||
|
||||
if(!ctx || ctx->failed) {
|
||||
return -1;
|
||||
@@ -367,21 +449,21 @@ filemgr_upload_data(void *upload_ctx, const char *data, size_t size) {
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
while(written < size) {
|
||||
ssize_t n = write(ctx->fd, data + written, size - written);
|
||||
if(n <= 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EIO;
|
||||
while(size) {
|
||||
size_t space = UPLOAD_BUFFER_SIZE - ctx->buffered;
|
||||
size_t take = size < space ? size : space;
|
||||
|
||||
memcpy(ctx->buffer + ctx->buffered, data, take);
|
||||
ctx->buffered += take;
|
||||
data += take;
|
||||
size -= take;
|
||||
if(ctx->buffered == UPLOAD_BUFFER_SIZE && upload_flush(ctx)) {
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
written += (size_t)n;
|
||||
ctx->written += (unsigned long long)n;
|
||||
if(ctx->task) {
|
||||
task_update(ctx->task, TASK_RUNNING, ctx->target,
|
||||
(unsigned long long)n, NULL);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -395,42 +477,55 @@ filemgr_upload_finish(struct MHD_Connection *conn, void *upload_ctx) {
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(ctx->failed) {
|
||||
if(ctx->task && !ctx->task_done) {
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO));
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error(conn, errno == EEXIST ? MHD_HTTP_CONFLICT :
|
||||
errno == EBUSY ? MHD_HTTP_CONFLICT :
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
return send_json_error_detail(conn,
|
||||
errno == EEXIST ? MHD_HTTP_CONFLICT :
|
||||
errno == EBUSY ? MHD_HTTP_CONFLICT :
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
if(ctx->buffered && upload_flush(ctx)) {
|
||||
ctx->error = ctx->error ? ctx->error : EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "verifying uploaded size";
|
||||
if(ctx->expected && ctx->written != ctx->expected) {
|
||||
ctx->error = EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "setting file permissions";
|
||||
if(fchmod_0777(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "syncing file data";
|
||||
if(fsync(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "closing temporary file";
|
||||
if(close(ctx->fd)) {
|
||||
ctx->fd = -1;
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->fd = -1;
|
||||
ctx->stage = "moving temporary file into place";
|
||||
if(rename(ctx->temp, ctx->target)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
ctx->task->upload_completed++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
@@ -439,19 +534,18 @@ done:
|
||||
ctx->fd = -1;
|
||||
}
|
||||
if(ret) {
|
||||
upload_error_message(ctx);
|
||||
if(ctx->temp[0]) {
|
||||
unlink(ctx->temp);
|
||||
}
|
||||
if(ctx->task && !ctx->task_done) {
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO));
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error(conn, errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
return send_json_error_detail(conn,
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
return send_json_ok(conn);
|
||||
}
|
||||
@@ -476,5 +570,13 @@ filemgr_upload_free(void *upload_ctx) {
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
}
|
||||
free(ctx->buffer);
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
if(ctx->task->active_streams) {
|
||||
ctx->task->active_streams--;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
+27
-1
@@ -6,6 +6,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <microhttpd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -14,10 +15,24 @@
|
||||
#include "websrv.h"
|
||||
|
||||
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_LIMIT (8 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_INCREMENT (2 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_RCVBUF_SIZE (4 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_SNDBUF_SIZE (4 * 1024 * 1024)
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested;
|
||||
static int g_listen_fd = -1;
|
||||
|
||||
static void
|
||||
websrv_tune_connection_socket(int fd) {
|
||||
const int sndbuf = HTTP_SOCKET_SNDBUF_SIZE;
|
||||
const int nodelay = 1;
|
||||
|
||||
/* OrbisOS HTTP sockets need an explicit send buffer to fill a GbE link. */
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
|
||||
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
}
|
||||
|
||||
typedef struct request_context {
|
||||
char *body;
|
||||
size_t size;
|
||||
@@ -190,6 +205,12 @@ websrv_listen(unsigned short port) {
|
||||
close(srvfd);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
const int rcvbuf = HTTP_SOCKET_RCVBUF_SIZE;
|
||||
|
||||
/* Set before listen so accepted PS5 sockets inherit the larger window. */
|
||||
(void)setsockopt(srvfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
|
||||
}
|
||||
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
@@ -211,8 +232,12 @@ websrv_listen(unsigned short port) {
|
||||
|
||||
if(!(httpd = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_ITC |
|
||||
MHD_USE_NO_LISTEN_SOCKET | MHD_USE_DEBUG |
|
||||
MHD_USE_INTERNAL_POLLING_THREAD,
|
||||
MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_TURBO,
|
||||
0, NULL, NULL, &websrv_on_request, NULL,
|
||||
MHD_OPTION_CONNECTION_MEMORY_LIMIT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_LIMIT,
|
||||
MHD_OPTION_CONNECTION_MEMORY_INCREMENT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_INCREMENT,
|
||||
MHD_OPTION_NOTIFY_COMPLETED,
|
||||
&websrv_on_completed, NULL, MHD_OPTION_END))) {
|
||||
perror("MHD_start_daemon");
|
||||
@@ -226,6 +251,7 @@ websrv_listen(unsigned short port) {
|
||||
if(!g_stop_requested) perror("accept");
|
||||
break;
|
||||
}
|
||||
websrv_tune_connection_socket(connfd);
|
||||
if(MHD_add_connection(httpd, connfd, (struct sockaddr *)&client_addr,
|
||||
addr_len) != MHD_YES) {
|
||||
perror("MHD_add_connection");
|
||||
|
||||
Reference in new issue
Block a user