Compare commits

..
24 Commits
Author SHA1 Message Date
owendswang 48d669b848 Circle button to go back to parent folder
Block browser closing while processing tasks and displaying overlay

Auto focus to parent folder after going back
2026-08-11 22:01:25 +08:00
owendswang dafbd8dcbf Optimize address bar for path display overflow 2026-08-09 14:50:38 +08:00
owendswang 939257eda0 Fixed PKG install from internal storage.
Adjust back button display effects
2026-08-09 12:38:23 +08:00
owendswang 3efaf24385 Highlight specific directories' icon.
Moved the back button to the header of the name column.
2026-08-08 17:47:54 +08:00
owendswang 5302e971f9 Enhance credits with PKG install function reference
Updated the credits section to include PKG install function in the websrv reference.
2026-08-06 21:18:15 +08:00
owendswang 582d154cdf Changed the way to end the daemon process 2026-08-06 14:51:43 +08:00
owendswang a61ac54e1f Added PKG install function 2026-08-06 14:33:02 +08:00
owendswang 6a110f95ae v1.1 2026-08-03 23:35:41 +08:00
owendswang b463c3f86b Small optimization of directory deletion 2026-08-01 00:11:46 +08:00
owendswang f9be83061d Update README.md 2026-07-29 23:18:19 +08:00
owendswang ae23c8fdaf Optimization phase 2 2026-07-29 23:12:49 +08:00
owendswang 2dc1ff2c42 Optimization phase 1 2026-07-26 19:12:37 +08:00
owendswang 9d56cd5039 Add upload/download 2026-07-24 23:51:26 +08:00
owendswang 5dba332ad4 Added list sorting and browser caching for path and sorting 2026-07-19 17:57:23 +08:00
owendswang 1a24160b8b Release v0.9 2026-07-19 00:20:15 +08:00
owendswang 3696468611 Fixed display for available spacee of interal M.2 2026-07-18 09:58:05 +08:00
owendswang 777bb4abd4 Release v0.8 2026-07-14 18:49:24 +08:00
owendswang 76f8fe21d2 Release v0.7 2026-07-09 20:09:02 +08:00
owendswang 5a91067c2e Release v0.6 2026-06-24 01:19:18 +08:00
owendswang ba0704d48c Release v0.5 2026-06-22 02:33:23 +08:00
owendswang 4e10dee5fb Update README.md 2026-06-20 23:45:21 +08:00
owendswang 259d0a6434 Release v0.4 2026-06-20 23:23:25 +08:00
owendswang 6f46847df2 Release v0.3 2026-06-19 13:23:00 +08:00
owendswang 91375e0537 Added screenshots 2026-06-18 23:05:34 +08:00
40 changed files with 6704 additions and 1293 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@ gen/
web-file-mgr-linux
*.o
*.d
tools/
test/
+13 -17
View File
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
endif
endif
VERSION_TAG := v0.2
VERSION_TAG := v1.4
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
@@ -23,31 +23,31 @@ HOST_STRIP ?= strip
HOST_PKG_CONFIG ?= pkg-config
BIN := web-file-mgr.elf
LEGACY_BIN := web-file-mgr-legacy.elf
LINUX_BIN := web-file-mgr-linux
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/asset.c src/mime.c src/notify.c
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
LINUX_SRCS := $(COMMON_SRCS)
ASSETS := $(wildcard assets/*)
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
ifneq ($(filter linux,$(MAKECMDGOALS)),)
ASSETS := $(BASE_ASSETS)
else
ASSETS := $(filter-out assets/icon0.png,$(BASE_ASSETS))
endif
GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c))
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
LDFLAGS := -Wl,--gc-sections
LEGACY_CFLAGS := $(filter-out -ffunction-sections -fdata-sections,$(CFLAGS))
LEGACY_LDFLAGS :=
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
LDADD += -lSceIpmi -lSceAppInstUtil
LINUX_CFLAGS := -O2 -Wall -Werror -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
LINUX_CFLAGS := -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
.PHONY: all legacy linux deps linux-deps clean
.PHONY: all linux deps linux-deps clean
all: deps $(BIN)
legacy: deps $(LEGACY_BIN)
linux: linux-deps $(LINUX_BIN)
deps:
@@ -61,17 +61,13 @@ gen:
mkdir gen
clean:
rm -rf $(BIN) $(LEGACY_BIN) $(LINUX_BIN) gen
rm -rf $(BIN) $(LINUX_BIN) gen
gen/%.c: assets/% gen-asset-module.py gen
gen/%.c: assets/% gen-asset-module.py | gen
$(PYTHON) gen-asset-module.py --path $* $< > $@
$(BIN): $(PS5_SRCS) $(GEN_SRCS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDADD)
$(STRIP) $@
$(LEGACY_BIN): $(PS5_SRCS) $(GEN_SRCS)
$(CC) $(LEGACY_CFLAGS) $(LEGACY_LDFLAGS) -o $@ $^ $(LDADD)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(filter %.c,$^) $(LDADD)
$(STRIP) $@
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS)
+45 -6
View File
@@ -5,19 +5,40 @@ safely copying game dump folders from USB storage to internal storage.
## Brief
PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, installs a home screen launcher in Media catagory on startup when needed, and provides file operations from the PS5 browser. If `8888` is already in use, the payload tries the next port until one is available; the startup notification shows the actual listen port.
PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, installs a home screen launcher in the Media category on startup when needed, and provides file operations from the PS5 browser or another browser on the same network. If `8888` is already in use, the payload tries the next port until one is available; the startup notification shows the actual listen port.
## Screenshots
<p>
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="31%" alt="PS5 Web File Manager screenshot 1"></a>
<a href="docs/screenshots/20260619_131432.399.jpg" target="_blank"><img src="docs/screenshots/20260619_131432.399.jpg" width="31%" alt="PS5 Web File Manager screenshot 2"></a>
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 Web File Manager screenshot 3"></a>
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 Web File Manager screenshot 4"></a>
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 Web File Manager screenshot 5"></a>
<a href="docs/screenshots/20260620_232728.533.jpg" target="_blank"><img src="docs/screenshots/20260620_232728.533.jpg" width="31%" alt="PS5 Web File Manager screenshot 6"></a>
</p>
## Features
- List files and folders.
- Copy, move, delete, rename, and create folders.
- Sort the list by name, type, size, modified time, or permissions. The selected sort mode is saved in browser local storage.
- 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.
- Copy/move by choosing sources first, then pasting or moving them into the current folder.
- Conflict prompts for overwriting files and merging folders.
- Full-screen task overlay with progress, speed, ETA, cancel support, and task recovery after reopening the browser while the payload process is still running.
- Upload files or folders from a remote browser. Upload is hidden in the PS5 browser because it is intended for another device on the network.
- Download a single file directly, or download folders/multiple selections as a `.tar` archive. Download is hidden in the PS5 browser.
- Full-screen task overlay with delayed display, progress, speed, ETA, cancel support, and task recovery after reopening the browser while the payload process is still running.
- Copied/moved files and folders are set to `0777` where the filesystem supports Unix permissions. FAT/exFAT-style filesystems may ignore chmod.
- Uploaded files are written through temporary files and are renamed into place after the upload completes.
- Chinese and English UI. The browser language is read from `navigator.languages` / `navigator.language`; Chinese uses `zh`, everything else uses English.
- Responsive layout for narrow mobile browsers, including wrapped toolbars and horizontally scrollable file lists.
- Startup notification showing the app name, version, and listen port.
- Home screen launcher icon and browser favicon use the same embedded `icon0.png` data in the PS5 build to avoid storing the icon twice in the ELF.
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`, `.shn`.
- Preview image files ending with `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
- Install PKG files ends with `.pkg`.
## Build
@@ -52,6 +73,15 @@ The output is:
web-file-mgr.elf
```
For local browser/UI testing on Linux:
```sh
make linux
./web-file-mgr-linux
```
The Linux build does not include the PS5 home screen installer.
## Usage
Start an ELF loader on the PS5. The common listener port is `9021`.
@@ -69,20 +99,29 @@ After the payload starts, the PS5 notification shows the app name, version, and
http://${PS5_IP_ADDRESS}:8888/
```
On first startup the payload installs a `PS5 Web File Manager` web shortcut in the Media category when needed. If the payload had to use a fallback port such as `8889`, use the port shown in the startup notification.
On first startup the payload installs a `PS5 Web File Manager` web shortcut in the Media category when needed. Existing installed launcher files are not overwritten; missing launcher files are written and then the install step is triggered. If the payload had to use a fallback port such as `8889`, use the port shown in the startup notification.
## Notes
- Copy, move, and delete run as single background tasks. While one task is running, other file operations are rejected.
- Copy, move, delete, upload, and download run as single background tasks. While one task is running, other file operations are rejected.
- Delete is recursive and permanent. There is no recycle bin.
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting existing files when merging into an existing target folder.
- Upload tasks can be canceled. A partially uploaded temporary file is removed when possible.
- Downloading a folder or multiple selected items produces a tar stream. The tar archive is generated by the payload and is not written to PS5 storage first.
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
- File names are transmitted as UTF-8 through the web API. The payload also attempts to preserve legacy byte-oriented names returned by mounted filesystems so mixed USB filename encodings can still be displayed and operated on.
## FAQ
- This is a homebrew app and should not intentionally modify system processes or kernel memory. If a kernel panic happens, make sure you are using a recent jailbreak method and ELF loader, or switch back to the stable method you normally use.
- If you are using P2JB and this payload causes a kernel panic, avoid using it with that setup. Stability matters more than convenience when each retry takes a long time.
- The preparing stage may calculate folder size and check available space. It can be slow when a folder contains many files, but it helps prevent starting a copy, move, upload, or download operation that cannot be completed safely.
## Credits
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, and PS5 browser/websrv behavior. License: GPLv3+.
- **[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+.
- **[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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+76 -15
View File
@@ -4,13 +4,35 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PS5 Web File Manager</title>
<link rel="icon" type="image/png" href="/icon0.png">
<style>
html, body { margin: 0; min-height: 100%; background: #111316; color: #edf0f2; }
.init-loading {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
background: #111316;
color: #aab4be;
font: 18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
</style>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<main class="shell">
<header class="topbar">
<div id="path" class="path">/</div>
<div id="spaceInfo" class="space-info"></div>
<div class="top-left">
<div id="path" class="path">/</div>
</div>
<div class="top-right">
<div id="spaceInfo" class="space-info"></div>
</div>
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
</header>
@@ -19,35 +41,57 @@
<button id="copyBtn" data-i18n="copy"></button>
<button id="moveBtn" data-i18n="move"></button>
<button id="renameBtn" data-i18n="rename"></button>
<button id="downloadBtn" class="remote-only" data-i18n="download"></button>
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="installPkgBtn" class="install-action" data-i18n="install" hidden></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
<span id="pasteTargetText" data-i18n="pasteToCurrent"></span>
</button>
<button id="clearClipboardBtn" data-i18n="cancel" hidden></button>
</div>
<div class="tool-right">
<button id="refreshBtn" data-i18n="refresh"></button>
<div id="uploadMenu" class="split-button remote-only">
<button id="uploadBtn" class="split-main" data-i18n="upload"></button>
<button id="uploadMenuBtn" class="split-arrow" type="button" aria-label="Upload menu"></button>
<div class="split-menu">
<button id="uploadFolderBtn" type="button" data-i18n="uploadFolder"></button>
</div>
</div>
<button id="newTextBtn" data-i18n="newText"></button>
<button id="mkdirBtn" data-i18n="mkdir"></button>
</div>
</section>
<input id="uploadFiles" type="file" multiple hidden>
<input id="uploadFolder" type="file" multiple webkitdirectory hidden>
<section id="content" class="content">
<table>
<thead>
<tr>
<th class="select-col"><label class="select-hit"><input id="selectAll" type="checkbox"></label></th>
<th class="name-col" data-i18n="name"></th>
<th class="type-col" data-i18n="type"></th>
<th class="size-col" data-i18n="size"></th>
<th class="time-col" data-i18n="mtime"></th>
<th class="mode-col" data-i18n="mode"></th>
<th class="name-col sortable" data-sort="name">
<div class="name-head">
<button id="parentBtn" class="parent-nav-button" type="button" aria-label="Parent directory" disabled>
<img src="/icon-back.png" alt="">
</button>
<span class="name-heading" data-i18n="name"></span>
</div>
</th>
<th class="type-col sortable" data-sort="type" data-i18n="type"></th>
<th class="size-col sortable" data-sort="size" data-i18n="size"></th>
<th class="time-col sortable" data-sort="mtime" data-i18n="mtime"></th>
<th class="mode-col sortable" data-sort="mode" data-i18n="mode"></th>
</tr>
</thead>
<tbody id="files"></tbody>
</table>
<div id="empty" class="empty" data-i18n="empty" hidden></div>
<div id="contentLoading" class="content-loading" hidden>
<div class="content-loading-text" data-i18n="readDir"></div>
</div>
</section>
<footer class="status">
@@ -56,19 +100,36 @@
</footer>
</main>
<div id="textEditorOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel">
<div id="textEditorPath" class="text-editor-path"></div>
<textarea id="textEditor" class="text-editor" wrap="off" spellcheck="false"
autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
<div id="textEditorStatus" class="text-editor-status"></div>
<div class="text-editor-actions">
<button id="textEditorCloseBtn" class="secondary" data-i18n="close"></button>
<button id="textEditorSaveBtn" class="primary" data-i18n="save"></button>
</div>
</section>
</div>
<div id="imagePreviewOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel image-preview-panel">
<div id="imagePreviewName" class="text-editor-path"></div>
<div class="image-preview-stage">
<img id="imagePreview" class="image-preview" alt="">
</div>
<button id="imagePreviewCloseBtn" class="secondary" data-i18n="close"></button>
</section>
</div>
<div id="taskOverlay" class="task-overlay" hidden>
<div class="task-panel">
<div id="tasks" class="tasks"></div>
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true">
<div class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
<script src="/main.js"></script>
</body>
+56 -2
View File
@@ -3,14 +3,29 @@ window.WFM_LANG = {
copy: "Copy",
move: "Move",
delete: "Delete",
download: "Download",
upload: "Upload",
uploadFolder: "Upload Folder",
copying: "Copying",
moving: "Moving",
deleting: "Deleting",
rename: "Rename",
paste: "Paste",
cancel: "Cancel",
close: "Close",
save: "Save",
exit: "Exit",
exiting: "Exiting...",
refresh: "Refresh",
mkdir: "New Folder",
newText: "New Text",
install: "Install",
installPackage: "Install package",
pkgInstalling: "Starting package installation: {name}...",
pkgInstallStarted: "Package installation started: {name}",
file: "File",
textFile: "Text",
image: "Image",
dir: "Folder",
parent: "Parent",
name: "Name",
@@ -33,12 +48,17 @@ window.WFM_LANG = {
speedLabel: "Speed",
progressLabel: "Progress",
etaLabel: "ETA",
elapsedLabel: "Elapsed",
durationHours: "{hours}h {minutes}m",
durationMinutes: "{minutes}m {seconds}s",
durationSeconds: "{seconds}s",
preparingTask: "Preparing task",
canceling: "Canceling...",
selectedItems: "{name} and {count} items",
countItems: "{count} items",
totalItems: "{count} items",
readDir: "Reading folder...",
processing: "Processing...",
actionBusy: "{label}...",
taskCreated: "{label} task created",
actionDone: "{label} done",
@@ -47,19 +67,38 @@ window.WFM_LANG = {
taskCanceled: "{label} canceled",
selectedForPaste: "Selected {name}. Enter the target folder, then choose {verb}",
clipboardCleared: "Pending operation canceled",
downloadStarted: "Download started: {name}",
uploadFolderChoice: "Upload a folder?\n\nOK: choose a folder\nCancel: choose files",
uploadOverwriteConfirm: "Items with the same name already exist: {names}\n\nOverwrite matching files and merge matching folders?",
uploadingStatus: "Uploading {index}/{count}: {name}",
uploadDone: "Upload done, {count} items",
uploadFailed: "Upload failed: {error}",
downloading: "Downloading",
uploading: "Uploading",
sameSourceTarget: "Source and destination are the same. Cannot {label} {name}",
removeConflictFirst: "A {existingType} named {name} already exists. To {label} this {sourceType}, delete that {existingType} first.",
overwriteFiles: "Files with the same name will be overwritten: {names}",
mergeDirs: "Folders with the same name will be merged. Internal files with the same name will be overwritten: {names}",
continueConfirm: "Continue?",
preparingPaste: "{label} preparing: calculating size and checking target space...",
preparingPaste: "Preparing: calculating size and checking target space...",
preparingStatus: "{label} preparing...",
cancelPrepare: "Preparation canceled",
cancelTaskConfirm: "Cancel current {label} task?",
cancelFailed: "Cancel failed: {error}",
tasksPollFailed: "Failed to read task status: {error}",
transferComplete: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}",
transferCompleteFiles: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
renamePrompt: "New name",
mkdirPrompt: "Folder name",
newTextPrompt: "Text file name",
creatingText: "Creating text file...",
createTextFailed: "Failed to create text file: {error}",
unsavedSaveConfirm: "The text has unsaved changes. Close without saving?\n\nCancel keeps the editor open.",
loadingText: "Loading text...",
savingText: "Saving...",
textSaved: "Text saved",
openTextFailed: "Failed to open text: {error}",
saveTextFailed: "Failed to save text: {error}",
deleteConfirm: "Delete {name}?",
deleteConfirmRecursive: "Delete {name}?\n\nFolders will be deleted recursively.",
activeTask: "A task is running",
@@ -70,11 +109,14 @@ window.WFM_LANG = {
storageUsb: "USB",
storageM2: "M.2",
storageExtended: "Extended",
storageRoot: "Root",
storageCurrent: "Current",
err_target_dir_not_writable: "Target folder is not writable: {path}",
err_target_file_not_writable: "Target file is not writable: {path}",
err_target_parent_not_writable: "Target parent folder is not writable: {path}",
err_target_parent_not_writable: "Current folder is not writable: {path}",
err_target_check_failed: "Cannot check target path: {path}",
err_target_path_too_long: "Target path is too long",
err_path_too_long: "Path is too long",
err_space_check_failed: "Cannot read target free space: {path}",
err_no_space: "Not enough target space. Required {required}, available {available}",
err_active_task: "A task is running",
@@ -82,6 +124,18 @@ window.WFM_LANG = {
err_source_destination_same: "Source and destination are the same",
err_destination_inside_source: "Cannot copy or move a folder inside itself",
err_invalid_path: "Invalid path",
err_pkg_type_invalid: "Only .pkg files can be installed",
err_pkg_install_failed: "Package installation failed ({arg})",
err_pkg_install_unsupported: "Package installation is only available on PS5",
err_file_not_found: "File not found",
err_file_already_exists: "A file with the same name already exists",
err_invalid_method: "Invalid request method",
err_text_type_not_editable: "This file type is not supported by the text editor",
err_text_file_too_large: "The text file is too large. The maximum size is 1 MiB",
err_text_invalid_utf8: "The file is not valid UTF-8 text",
err_text_file_changed: "The file changed after it was opened. Close and reopen it",
err_text_file_not_writable: "The text file is not writable",
err_request_body_too_large: "Too many selected items. Select fewer items and try again",
err_unknown_api: "Unknown API",
err_out_of_memory: "Out of memory",
err_no_source_paths: "No source paths",
+57 -3
View File
@@ -3,14 +3,29 @@ window.WFM_LANG = {
copy: "复制",
move: "移动",
delete: "删除",
download: "下载",
upload: "上传",
uploadFolder: "上传文件夹",
copying: "复制",
moving: "移动",
deleting: "删除",
rename: "重命名",
paste: "粘贴",
cancel: "取消",
close: "关闭",
save: "保存",
exit: "退出",
exiting: "正在退出...",
refresh: "刷新",
mkdir: "新建目录",
newText: "新建文本",
install: "安装",
installPackage: "安装 PKG",
pkgInstalling: "正在提交安装:{name}...",
pkgInstallStarted: "已开始安装:{name}",
file: "文件",
textFile: "文本",
image: "图片",
dir: "目录",
parent: "上级目录",
name: "名称",
@@ -33,12 +48,17 @@ window.WFM_LANG = {
speedLabel: "速度",
progressLabel: "进度",
etaLabel: "剩余",
elapsedLabel: "耗时",
durationHours: "{hours}小时 {minutes}分",
durationMinutes: "{minutes}分 {seconds}秒",
durationSeconds: "{seconds}秒",
preparingTask: "正在准备任务",
canceling: "正在取消...",
selectedItems: "{name} 等 {count} 项",
countItems: "{count} 项",
totalItems: "共 {count} 项",
readDir: "读取目录...",
readDir: "读取目录中...",
processing: "处理中...",
actionBusy: "{label}...",
taskCreated: "{label}任务已创建",
actionDone: "{label}完成",
@@ -47,19 +67,38 @@ window.WFM_LANG = {
taskCanceled: "{label}已取消",
selectedForPaste: "已选择 {name},进入目标目录后点击{verb}",
clipboardCleared: "已清除待操作项目",
downloadStarted: "已开始下载 {name}",
uploadFolderChoice: "选择要上传的目录?\n\n确定:选择目录\n取消:选择文件",
uploadOverwriteConfirm: "目标中已存在同名项目: {names}\n\n是否覆盖同名文件并合并同名目录?",
uploadingStatus: "正在上传 {index}/{count}: {name}",
uploadDone: "上传完成,共 {count} 项",
uploadFailed: "上传失败: {error}",
downloading: "下载中",
uploading: "上传中",
sameSourceTarget: "源和目标相同,不能{label} {name}",
removeConflictFirst: "目标中已存在同名{existingType} {name}。要{label}{sourceType},请先删除该{existingType}才能继续。",
overwriteFiles: "同名文件将被覆盖: {names}",
mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}",
continueConfirm: "继续?",
preparingPaste: "{label}准备中:正在计算大小并检查目标空间...",
preparingPaste: "准备中:正在计算大小并检查目标空间...",
preparingStatus: "{label}准备中...",
cancelPrepare: "已取消准备操作",
cancelTaskConfirm: "取消当前{label}任务?",
cancelFailed: "取消失败: {error}",
tasksPollFailed: "任务状态读取失败: {error}",
transferComplete: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}",
transferCompleteFiles: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
renamePrompt: "新名称",
mkdirPrompt: "目录名",
newTextPrompt: "文本文件名",
creatingText: "正在新建文本...",
createTextFailed: "新建文本失败: {error}",
unsavedSaveConfirm: "文本有未保存的修改,是否不保存并关闭?\n\n取消将留在编辑器中。",
loadingText: "正在读取文本...",
savingText: "正在保存...",
textSaved: "文本已保存",
openTextFailed: "打开文本失败: {error}",
saveTextFailed: "保存文本失败: {error}",
deleteConfirm: "确认删除 {name}?",
deleteConfirmRecursive: "确认删除 {name}?\n\n目录会递归删除。",
activeTask: "有任务正在执行",
@@ -70,11 +109,14 @@ window.WFM_LANG = {
storageUsb: "USB存储",
storageM2: "M2扩充存储",
storageExtended: "扩展存储",
storageRoot: "根分区",
storageCurrent: "当前目录",
err_target_dir_not_writable: "目标目录不可写: {path}",
err_target_file_not_writable: "目标文件不可写: {path}",
err_target_parent_not_writable: "目标父目录不可写: {path}",
err_target_parent_not_writable: "当前目录不可写: {path}",
err_target_check_failed: "无法检查目标路径: {path}",
err_target_path_too_long: "目标路径过长",
err_path_too_long: "路径过长",
err_space_check_failed: "无法读取目标剩余空间: {path}",
err_no_space: "目标空间不足,需要 {required},可用 {available}",
err_active_task: "有任务正在执行",
@@ -82,6 +124,18 @@ window.WFM_LANG = {
err_source_destination_same: "源和目标相同",
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
err_invalid_path: "路径无效",
err_pkg_type_invalid: "只能安装 .pkg 文件",
err_pkg_install_failed: "PKG 安装失败({arg})",
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
err_file_not_found: "文件不存在",
err_file_already_exists: "同名文件已存在",
err_invalid_method: "请求方式无效",
err_text_type_not_editable: "该文件类型不支持文本编辑",
err_text_file_too_large: "文本文件过大,最大支持 1 MiB",
err_text_invalid_utf8: "文件不是有效的 UTF-8 文本",
err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开",
err_text_file_not_writable: "文本文件不可写",
err_request_body_too_large: "选择的项目过多,请减少选择后重试",
err_unknown_api: "未知接口",
err_out_of_memory: "内存不足",
err_no_source_paths: "没有源路径",
+670 -75
View File
@@ -23,41 +23,8 @@
align-items: center;
justify-content: center;
background: #111316;
}
.loading-dots {
display: flex;
}
.loading-dots span {
width: 14px;
height: 14px;
margin: 0 6px;
border-radius: 50%;
background: #8fa1b3;
animation: loading-pulse 1s ease-in-out infinite;
}
.loading-dots span:nth-child(2) {
animation-delay: .12s;
}
.loading-dots span:nth-child(3) {
animation-delay: .24s;
}
@keyframes loading-pulse {
0%,
80%,
100% {
opacity: .35;
transform: scale(.75);
}
40% {
opacity: 1;
transform: scale(1);
}
color: #aab4be;
font-size: 18px;
}
body {
@@ -76,39 +43,77 @@ body {
}
.topbar {
position: relative;
flex: 0 0 auto;
height: auto;
min-height: 54px;
display: flex;
align-items: center;
padding: 12px 30px;
align-items: start;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 18px;
background: #1b2026;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.top-left,
.top-right {
height: auto;
display: flex;
align-items: center;
}
.top-left {
flex: 1 1 0;
min-width: 0;
margin-top: 15px;
}
.top-right {
flex: 0 1 auto;
min-width: 0;
margin-left: auto;
justify-content: flex-end;
flex-wrap: wrap;
padding: 12px 40px 12px 0;
text-align: right;
}
.path {
color: #aab4be;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 22px;
line-height: 1.35;
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
flex: 1 1 auto;
min-width: 0;
}
.space-info {
height: auto;
min-height: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
flex: 0 1 auto;
max-width: 58vw;
flex: 1 1 auto;
max-width: none;
color: #c9d3dc;
font-size: 18px;
line-height: 1.35;
line-height: 30px;
white-space: nowrap;
overflow: hidden;
overflow: visible;
text-align: right;
}
.space-item {
flex: 0 0 auto;
margin-left: 14px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -129,8 +134,6 @@ body {
width: 24px;
height: 24px;
padding: 0;
flex: 0 0 24px;
margin-left: 16px;
}
.power-button {
@@ -160,6 +163,13 @@ body {
transform: translateX(-50%);
}
#exitBtn {
position: absolute;
top: 15px;
right: 18px;
margin-left: 0;
}
.toolbar,
.tool-left,
.tool-right {
@@ -169,10 +179,14 @@ body {
}
.toolbar {
flex: 0 0 auto;
height: auto;
min-height: 84px;
justify-content: space-between;
padding: 14px 30px;
padding: 15px 18px 0;
background: #15191e;
border-bottom: 1px solid #2d343d;
overflow: visible;
}
.tool-left {
@@ -180,9 +194,15 @@ body {
flex: 1 1 auto;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 54px;
flex-wrap: wrap;
}
.tool-left > *,
.tool-right > * {
margin: 0 12px 0 0;
margin: 0 12px 15px 0;
}
.tool-right > * {
@@ -192,11 +212,70 @@ body {
.tool-right {
justify-content: flex-end;
flex: 0 0 auto;
flex: 1 1 auto;
}
.split-button {
position: relative;
display: inline-flex;
align-items: stretch;
height: 54px;
min-height: 54px;
margin-bottom: 15px;
}
.split-button .split-main {
min-width: 82px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin: 0;
}
.split-button .split-arrow {
min-width: 38px;
width: 38px;
padding: 0;
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin: 0;
}
.split-arrow::before {
content: "";
display: block;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 7px solid #f2f5f7;
}
.split-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 40;
display: none;
min-width: 150px;
padding-top: 6px;
}
.split-menu button {
width: 100%;
min-width: 150px;
margin: 0;
white-space: nowrap;
}
.split-button:hover .split-menu,
.split-button.open .split-menu {
display: block;
}
button,
.button {
height: 54px;
min-height: 54px;
min-width: 96px;
padding: 0 20px;
@@ -220,8 +299,8 @@ button:focus,
outline-offset: 2px;
}
button:not(.row-action):hover,
.button:hover {
button:not(.row-action):hover:not(:disabled),
.button:hover:not(:disabled) {
background: #303945;
}
@@ -237,31 +316,87 @@ button:disabled,
background: #402326;
}
.primary {
border-color: #4f9f7b;
background: #285943;
}
button.primary:hover:not(:disabled) {
background: #347358;
}
.secondary {
border-color: #59636e;
background: #20262d;
}
.paste-action {
max-width: 480px;
max-width: 360px;
}
.paste-action > * {
flex: 0 0 auto;
margin-right: 8px;
}
.paste-name {
display: inline-block;
max-width: 180px;
flex: 1 1 auto;
min-width: 0;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-action {
min-width: 88px;
border-color: #8c6425;
background: #352b1c;
color: #f8dfae;
}
button.install-action:hover:not(:disabled) {
background: #433521;
}
.paste-count {
white-space: nowrap;
}
.content {
position: relative;
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.content.loading {
pointer-events: none;
}
.content-loading {
position: absolute;
right: 0;
left: 0;
height: 100%;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: rgba(4, 6, 8, .72);
}
.content-loading-text {
color: #aab4be;
font-size: 18px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 1034px;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
@@ -274,6 +409,7 @@ td {
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
line-height: 24px;
}
th {
@@ -285,13 +421,108 @@ th {
font-weight: 600;
}
th.sortable {
cursor: pointer;
}
th.sortable::after {
content: "";
display: inline-block;
width: 12px;
margin-left: 8px;
color: #aeb8c2;
text-align: center;
}
th.sorted-asc::after {
content: "^";
}
th.sorted-desc::after {
content: "v";
}
.name-head {
height: 78px;
min-height: 78px;
display: flex;
align-items: center;
min-width: 0;
}
th.name-col {
padding: 0;
}
th.name-col.sortable::after {
content: none;
}
.name-heading {
min-width: 100px;
padding-left: 18px;
overflow: hidden;
text-overflow: ellipsis;
}
.name-heading::after {
content: "";
display: inline-block;
width: 12px;
margin-left: 8px;
color: #aeb8c2;
text-align: center;
}
th.name-col.sorted-asc .name-heading::after {
content: "^";
}
th.name-col.sorted-desc .name-heading::after {
content: "v";
}
.parent-nav-button {
width: 78px;
min-width: 78px;
height: 78px;
min-height: 78px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
position: relative;
}
.parent-nav-button img {
width: 32px;
height: 32px;
display: block;
}
.parent-nav-button:hover:not(:disabled) {
background: #222932;
}
.parent-nav-button:not(:disabled):focus {
outline-offset: -5px;
}
.parent-nav-button:disabled img {
opacity: .42;
}
.parent-nav-button:disabled {
pointer-events: auto;
}
.select-col {
width: 74px;
padding: 0;
}
.name-col {
min-width: 360px;
min-width: 600px;
}
td.name-col {
@@ -308,6 +539,10 @@ td.name-col {
text-align: right;
}
th.size-col {
text-align: center;
}
.time-col {
width: 225px;
text-align: center;
@@ -328,6 +563,7 @@ tr {
}
thead tr {
height: 78px;
cursor: default;
}
@@ -340,7 +576,12 @@ tr.selected {
background: #1d2d35;
}
tr[data-type="parent"] {
cursor: pointer;
}
.select-hit {
height: 78px;
min-height: 78px;
width: 100%;
display: flex;
@@ -359,19 +600,19 @@ input[type="checkbox"] {
width: 32px;
height: 32px;
flex: 0 0 32px;
display: inline-flex;
align-items: center;
justify-content: center;
display: block;
}
.icon img {
width: 32px;
height: 32px;
display: block;
.special-folder-icon {
-webkit-filter: hue-rotate(65deg);
filter: hue-rotate(65deg);
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
.row-action {
width: 100%;
height: 78px;
min-height: 78px;
padding: 16px 18px;
border: 0;
@@ -402,10 +643,11 @@ input[type="checkbox"] {
.status {
color: #aab4be;
padding: 10px 25px;
}
.status {
flex: 0 0 auto;
height: 46px;
min-height: 46px;
padding: 0 25px;
line-height: 46px;
border-top: 1px solid #2d343d;
background: #15191e;
font-size: 18px;
@@ -419,6 +661,112 @@ input[type="checkbox"] {
color: #7f8a94;
}
.text-editor-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: rgba(4, 6, 8, .9);
}
.text-editor-panel {
width: 1200px;
height: calc(100vh - 96px);
min-width: 0;
max-width: calc(100vw - 96px);
display: flex;
flex-direction: column;
padding: 24px;
border: 1px solid #46515f;
border-radius: 10px;
background: #15191e;
box-shadow: 0 18px 60px rgba(0, 0, 0, .45);
}
.text-editor-path {
flex: 0 0 auto;
margin-bottom: 14px;
overflow: hidden;
color: #c9d3dc;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
white-space: nowrap;
text-overflow: ellipsis;
}
.text-editor {
width: 100%;
min-height: 0;
flex: 1 1 auto;
padding: 18px;
border: 1px solid #46515f;
border-radius: 6px;
outline: none;
resize: none;
background: #0f1215;
color: #edf0f2;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 20px;
line-height: 1.5;
}
.text-editor:focus {
border-color: #6fb1ff;
}
.text-editor-status {
min-height: 28px;
padding-top: 8px;
color: #aab4be;
font-size: 18px;
}
.text-editor-actions {
display: flex;
}
.text-editor-actions button {
flex: 1 1 0;
}
.text-editor-actions button + button {
margin-left: 14px;
}
.image-preview-stage {
width: 100%;
height: calc(100vh - 250px);
min-width: 0;
min-height: 0;
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-preview {
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: calc(100vh - 250px);
pointer-events: none;
-webkit-user-drag: none;
user-select: none;
}
.image-preview-panel > button {
width: 100%;
margin-top: 14px;
}
.task-overlay {
position: fixed;
top: 0;
@@ -467,6 +815,8 @@ input[type="checkbox"] {
display: flex;
align-items: flex-start;
font-size: 24px;
justify-content: space-between;
position: relative;
}
.task-head.has-amount {
@@ -483,13 +833,18 @@ input[type="checkbox"] {
.task-current {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-current-plain {
text-overflow: ellipsis;
}
.task-name {
display: flex;
min-width: 0;
text-overflow: ellipsis;
flex: 1 1 auto;
}
.task-name > * {
@@ -509,6 +864,14 @@ input[type="checkbox"] {
white-space: nowrap;
}
.task-elapsed {
flex: 0 0 auto;
white-space: nowrap;
text-align: right;
margin-left: 16px;
color: #aab4be;
}
.task-meta {
display: flex;
color: #c9d3dc;
@@ -577,18 +940,250 @@ input[type="checkbox"] {
background: #8a929b;
}
@media (max-width: 900px) {
.topbar {
align-items: stretch;
flex-direction: column;
@media (max-width: 815px) {
.top-left {
padding: 0 40px 0 0;
}
th:nth-child(4),
td:nth-child(4),
th:nth-child(5),
td:nth-child(5),
th:nth-child(6),
td:nth-child(6) {
.top-right {
padding: 10px 0;
}
}
@media (max-width: 640px) {
body {
font-size: 16px;
}
.topbar {
min-height: 42px;
padding: 0 10px;
align-items: center;
flex-wrap: nowrap;
}
.top-left {
flex: 1 1 auto;
margin-top: 0;
padding-right: 34px;
}
.top-right {
display: none;
}
.path {
font-size: 16px;
line-height: 42px;
}
#exitBtn {
top: 9px;
right: 10px;
}
.toolbar {
min-height: 0;
padding: 8px 8px 0;
}
.toolbar .tool-left,
.toolbar .tool-right {
min-height: 0;
}
.tool-left,
.tool-right {
flex: 1 1 100%;
}
.tool-right {
justify-content: flex-start;
}
.tool-left > *,
.tool-right > * {
margin: 0 8px 8px 0;
}
.tool-right > * {
margin-left: 0;
}
button,
.button {
height: 40px;
min-height: 40px;
min-width: 76px;
padding: 0 12px;
border-radius: 6px;
font-size: 16px;
}
.split-button {
height: 40px;
min-height: 40px;
margin-bottom: 8px;
}
.split-button .split-main {
min-width: 64px;
}
.split-button .split-arrow {
min-width: 34px;
width: 34px;
}
.split-menu {
min-width: 126px;
padding-top: 4px;
}
.split-menu button {
min-width: 126px;
}
.paste-action {
max-width: 100%;
}
.paste-name {
max-width: 120px;
}
table {
min-width: 960px;
}
.name-col {
width: 600px;
}
th,
td {
padding: 10px 12px;
font-size: 16px;
line-height: 20px;
}
tr,
thead tr,
.name-head,
.parent-nav-button,
.select-hit,
.row-action {
height: 56px;
min-height: 56px;
}
.parent-nav-button {
width: 56px;
min-width: 56px;
}
.name-heading {
padding-left: 12px;
}
.row-action {
padding: 10px 12px;
}
input[type="checkbox"] {
width: 28px;
height: 28px;
}
.icon {
width: 26px;
height: 26px;
flex-basis: 26px;
}
.status {
height: 34px;
min-height: 34px;
padding: 0 12px;
line-height: 34px;
font-size: 14px;
}
.task-overlay {
padding: 18px;
align-items: center;
}
.task-panel {
width: 100%;
max-width: none;
padding: 18px;
border-radius: 8px;
}
.task > * + * {
margin-top: 12px;
}
.task-head {
display: block;
font-size: 20px;
}
.task-name {
display: flex;
max-width: none;
white-space: nowrap;
}
.task-name > * {
margin: 0 8px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-name-subject {
flex: 1 1 auto;
max-width: none;
min-width: 0;
}
.task-elapsed {
margin: 8px 0 0;
text-align: left;
font-size: 18px;
}
.task-current {
font-size: 18px;
white-space: normal;
word-break: break-all;
}
.task-meta {
display: block;
font-size: 18px;
}
.task-meta div,
.task-meta div:nth-child(2),
.task-meta div:nth-child(3) {
display: block;
margin: 0 0 8px;
text-align: left;
white-space: normal;
}
.task-meta div:last-child {
margin-bottom: 0;
}
.progress {
height: 32px;
}
.task .danger {
width: 100%;
}
}
+1243 -156
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+18 -29
View File
@@ -4,12 +4,13 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <ps5/kernel.h>
#include "asset.h"
#include "pkg_installer.h"
#define INCASSET(name, file) \
__asm__(".section .rodata\n" \
".global " #name "\n" \
@@ -28,14 +29,20 @@
INCASSET(param_json, "assets/param.json");
INCASSET(icon0_png, "assets/icon0.png");
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void *);
static int
install_file(const char *path, const uint8_t *data, size_t size) {
struct stat st;
FILE *f;
if(!(f = fopen(path, "w"))) {
if(!stat(path, &st)) {
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(!(f = fopen(path, "wb"))) {
return -1;
}
if(fwrite(data, size, 1, f) != 1) {
@@ -65,32 +72,13 @@ install_app(const char *title_id, const char *dir) {
}
static int
needs_update(const char *path, const uint8_t *expected, size_t expected_size) {
needs_install_file(const char *path) {
struct stat st;
uint8_t *buf;
FILE *f;
int mismatch;
if(stat(path, &st) || st.st_size != (off_t)expected_size) {
if(stat(path, &st)) {
return 1;
}
if(!(f = fopen(path, "r"))) {
return 1;
}
if(!(buf = malloc(expected_size))) {
fclose(f);
return 1;
}
if(fread(buf, 1, expected_size, f) != expected_size) {
free(buf);
fclose(f);
return 1;
}
fclose(f);
mismatch = memcmp(buf, expected, expected_size);
free(buf);
return mismatch != 0;
return 0;
}
int
@@ -104,6 +92,8 @@ app_install_if_needed(void) {
int update_needed = 0;
int err;
asset_register("/icon0.png", icon0_png, icon0_png_size, "image/png", 0);
snprintf(base_dir, sizeof(base_dir), "/user/app/%s", title_id);
snprintf(sce_sys_dir, sizeof(sce_sys_dir), "/user/app/%s/sce_sys", title_id);
snprintf(param_path, sizeof(param_path), "%s/param.json", sce_sys_dir);
@@ -111,8 +101,7 @@ app_install_if_needed(void) {
if(stat(base_dir, &st)) {
update_needed = 1;
} else if(needs_update(param_path, param_json, param_json_size) ||
needs_update(icon_path, icon0_png, icon0_png_size)) {
} else if(needs_install_file(param_path) || needs_install_file(icon_path)) {
update_needed = 1;
}
@@ -122,7 +111,7 @@ app_install_if_needed(void) {
printf("Installing launcher app %s\n", title_id);
if((err = sceAppInstUtilInitialize())) {
if((err = pkg_installer_initialize())) {
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
return -1;
}
+754
View File
@@ -0,0 +1,754 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
#include "websrv.h"
#define DOWNLOAD_ARCHIVE_NAME_LIMIT 20
typedef struct tar_frame {
char path[PATH_MAX];
char name[PATH_MAX];
DIR *dir;
int header_sent;
struct tar_frame *next;
} tar_frame_t;
typedef struct tar_stream {
file_task_t *task;
char **paths;
size_t path_count;
size_t path_index;
size_t stack_depth;
tar_frame_t *stack;
int fd;
char current_file[PATH_MAX];
unsigned long long file_remaining;
size_t file_padding;
size_t pad_remaining;
char *pending;
size_t pending_size;
size_t pending_offset;
int final_blocks;
int done;
int error;
} tar_stream_t;
typedef struct download_file_stream {
file_task_t *task;
FILE *file;
unsigned long long size;
unsigned long long sent;
int done;
int error;
} download_file_stream_t;
static enum MHD_Result
download_task_request_error(struct MHD_Connection *conn, file_task_t *task,
char **paths, size_t count, unsigned int status,
const char *msg) {
free_paths(paths, count);
free_task(task);
return send_json_error(conn, status, msg);
}
static int
tar_checksum(char *header) {
int sum = 0;
int i;
memset(header + 148, ' ', 8);
for(i = 0; i < 512; i++) {
sum += (unsigned char)header[i];
}
snprintf(header + 148, 8, "%06o", sum);
header[154] = 0;
header[155] = ' ';
return 0;
}
static int
tar_split_name(const char *name, char *out_name, size_t out_name_size,
char *prefix, size_t prefix_size) {
size_t len = strlen(name);
const char *slash;
memset(out_name, 0, out_name_size);
memset(prefix, 0, prefix_size);
if(len < out_name_size) {
strcpy(out_name, name);
return 0;
}
for(slash = name + len; slash > name; slash--) {
size_t prefix_len;
size_t name_len;
if(*slash != '/') {
continue;
}
prefix_len = (size_t)(slash - name);
name_len = len - prefix_len - 1;
if(prefix_len < prefix_size && name_len > 0 && name_len < out_name_size) {
memcpy(prefix, name, prefix_len);
prefix[prefix_len] = 0;
memcpy(out_name, slash + 1, name_len + 1);
return 0;
}
}
errno = ENAMETOOLONG;
return -1;
}
static int
tar_queue_header(tar_stream_t *s, const char *name, const struct stat *st,
char type) {
char header[512];
char tar_name[100];
char prefix[155];
unsigned int mode = (unsigned int)(st->st_mode & 07777);
unsigned long long size = type == '0' ? (unsigned long long)st->st_size : 0;
long long mtime = (long long)st->st_mtime;
if(tar_split_name(name, tar_name, sizeof(tar_name), prefix, sizeof(prefix))) {
return -1;
}
memset(header, 0, sizeof(header));
memcpy(header, tar_name, strlen(tar_name));
snprintf(header + 100, 8, "%07o", mode);
snprintf(header + 108, 8, "%07o", 0);
snprintf(header + 116, 8, "%07o", 0);
snprintf(header + 124, 12, "%011llo", size);
snprintf(header + 136, 12, "%011llo", (unsigned long long)mtime);
header[156] = type;
memcpy(header + 257, "ustar", 5);
memcpy(header + 263, "00", 2);
if(prefix[0]) {
memcpy(header + 345, prefix, strlen(prefix));
}
tar_checksum(header);
s->pending = malloc(sizeof(header));
if(!s->pending) {
errno = ENOMEM;
return -1;
}
memcpy(s->pending, header, sizeof(header));
s->pending_size = sizeof(header);
s->pending_offset = 0;
return 0;
}
static int
tar_push_dir(tar_stream_t *s, const char *path, const char *name) {
tar_frame_t *frame = calloc(1, sizeof(*frame));
if(!frame) {
errno = ENOMEM;
return -1;
}
snprintf(frame->path, sizeof(frame->path), "%s", path);
snprintf(frame->name, sizeof(frame->name), "%s", name);
frame->dir = opendir(path);
if(!frame->dir) {
int error = errno;
free(frame);
errno = error;
return -1;
}
frame->next = s->stack;
s->stack = frame;
s->stack_depth++;
return 0;
}
static void
tar_pop_dir(tar_stream_t *s) {
tar_frame_t *frame = s->stack;
if(!frame) {
return;
}
s->stack = frame->next;
if(s->stack_depth) {
s->stack_depth--;
}
if(frame->dir) {
closedir(frame->dir);
}
free(frame);
}
static int
tar_start_path(tar_stream_t *s, const char *path, const char *name) {
struct stat st;
if(lstat(path, &st)) {
return -1;
}
if(S_ISDIR(st.st_mode)) {
char dir_name[PATH_MAX];
snprintf(dir_name, sizeof(dir_name), "%s%s", name,
name[strlen(name) - 1] == '/' ? "" : "/");
return tar_push_dir(s, path, dir_name);
}
if(!S_ISREG(st.st_mode)) {
errno = ENOTSUP;
return -1;
}
if(tar_queue_header(s, name, &st, '0')) {
return -1;
}
s->fd = open(path, O_RDONLY);
if(s->fd < 0) {
return -1;
}
snprintf(s->current_file, sizeof(s->current_file), "%s", path);
s->file_remaining = (unsigned long long)st.st_size;
s->file_padding = (size_t)((512 - ((unsigned long long)st.st_size % 512)) % 512);
return 0;
}
static int
tar_prepare_next(tar_stream_t *s) {
while(!s->pending && s->fd < 0 && !s->done) {
if(s->task && task_cancel_requested(s->task)) {
errno = ECANCELED;
return -1;
}
if(s->pad_remaining) {
size_t size = s->pad_remaining > 512 ? 512 : s->pad_remaining;
s->pending = calloc(1, size);
if(!s->pending) {
errno = ENOMEM;
return -1;
}
s->pending_size = size;
s->pending_offset = 0;
s->pad_remaining -= size;
return 0;
}
if(s->stack) {
tar_frame_t *frame = s->stack;
struct dirent *entry;
struct stat st;
if(!frame->header_sent) {
frame->header_sent = 1;
if(lstat(frame->path, &st) ||
tar_queue_header(s, frame->name, &st, '5')) {
return -1;
}
return 0;
}
while((entry = readdir(frame->dir))) {
char child[PATH_MAX];
char child_name[PATH_MAX];
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
continue;
}
if(path_join(child, sizeof(child), frame->path, entry->d_name) ||
path_join(child_name, sizeof(child_name), frame->name,
entry->d_name) ||
tar_start_path(s, child, child_name)) {
return -1;
}
return 0;
}
tar_pop_dir(s);
continue;
}
if(s->path_index < s->path_count) {
const char *path = s->paths[s->path_index++];
if(tar_start_path(s, path, path_basename(path))) {
return -1;
}
continue;
}
if(s->final_blocks < 2) {
s->pending = calloc(1, 512);
if(!s->pending) {
errno = ENOMEM;
return -1;
}
s->pending_size = 512;
s->pending_offset = 0;
s->final_blocks++;
return 0;
}
s->done = 1;
}
return 0;
}
static ssize_t
tar_read(void *cls, uint64_t pos, char *buf, size_t max) {
tar_stream_t *s = cls;
size_t out = 0;
(void)pos;
while(out < max && !s->done) {
if(s->task && task_cancel_requested(s->task)) {
s->error = ECANCELED;
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
}
if(tar_prepare_next(s)) {
s->error = errno ? errno : EIO;
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
}
if(s->pending) {
size_t left = s->pending_size - s->pending_offset;
size_t take = left < max - out ? left : max - out;
memcpy(buf + out, s->pending + s->pending_offset, take);
s->pending_offset += take;
out += take;
if(s->pending_offset >= s->pending_size) {
free(s->pending);
s->pending = NULL;
s->pending_size = 0;
s->pending_offset = 0;
}
continue;
}
if(s->fd >= 0) {
size_t want = max - out;
ssize_t n;
if((unsigned long long)want > s->file_remaining) {
want = (size_t)s->file_remaining;
}
n = read(s->fd, buf + out, want);
if(n < 0) {
s->error = errno;
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
}
if(!n) {
close(s->fd);
s->fd = -1;
s->current_file[0] = 0;
s->file_remaining = 0;
continue;
}
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);
}
if(!s->file_remaining) {
close(s->fd);
s->fd = -1;
s->current_file[0] = 0;
s->pad_remaining = s->file_padding;
s->file_padding = 0;
}
}
}
return out ? (ssize_t)out : MHD_CONTENT_READER_END_OF_STREAM;
}
static void
tar_close(void *cls) {
tar_stream_t *s = cls;
int canceled;
if(!s) {
return;
}
if(s->fd >= 0) {
close(s->fd);
}
while(s->stack) {
tar_pop_dir(s);
}
canceled = s->task && task_cancel_requested(s->task);
if(s->task) {
char current[PATH_MAX];
snprintf(current, sizeof(current), "%s",
s->task->current[0] ? s->task->current : s->task->src);
if(canceled) {
task_update(s->task, TASK_CANCELED, current, 0, "canceled");
} else if(s->error) {
errno = s->error;
task_update(s->task, TASK_FAILED, current, 0, strerror(errno));
} else if(s->done) {
task_update(s->task, TASK_DONE, current, 0, NULL);
} else {
task_update(s->task, TASK_FAILED, current, 0, "client disconnected");
}
}
free(s->pending);
free_paths(s->paths, s->path_count);
free(s);
}
static int
prepare_download_task(file_task_t *task) {
unsigned long long total = 0;
size_t file_count = 0;
size_t dir_count = 0;
size_t i;
task_update(task, TASK_RUNNING, "preparing", 0, NULL);
for(i = 0; i < task->src_count; i++) {
if(count_task_path_bytes(task, task->srcs[i], task->srcs[i],
&total, &file_count, &dir_count)) {
return -1;
}
}
pthread_mutex_lock(&g_tasks_lock);
task->total = total;
task->file_count = file_count;
task->dir_count = dir_count;
task->updated_at = time(NULL);
pthread_mutex_unlock(&g_tasks_lock);
return 0;
}
static size_t
append_truncated_name(char *out, size_t size, size_t len, const char *name,
size_t limit) {
while(*name && len < limit && len + 1 < size) {
unsigned char c = (unsigned char)*name;
out[len++] = *name++;
if(c >= 0x80) {
while((*name & 0xc0) == 0x80 && len < limit && len + 1 < size) {
out[len++] = *name++;
}
}
}
out[len] = 0;
return len;
}
static void
download_archive_name(char *out, size_t size, char **paths, size_t count) {
char name[PATH_MAX];
size_t len = 0;
size_t i;
out[0] = 0;
if(count == 1) {
path_basename_copy(paths[0], name, sizeof(name));
append_truncated_name(out, size, 0, name, DOWNLOAD_ARCHIVE_NAME_LIMIT);
} else {
for(i = 0; i < count && len < DOWNLOAD_ARCHIVE_NAME_LIMIT; i++) {
path_basename_copy(paths[i], name, sizeof(name));
if(i && len + 1 < DOWNLOAD_ARCHIVE_NAME_LIMIT && len + 1 < size) {
out[len++] = ' ';
out[len] = 0;
}
len = append_truncated_name(out, size, len, name,
DOWNLOAD_ARCHIVE_NAME_LIMIT);
}
}
if(!out[0]) {
snprintf(out, size, "download");
}
snprintf(out + strlen(out), size - strlen(out), ".tar");
}
static void
download_file_name(char *out, size_t size, const char *path) {
path_basename_copy(path, out, size);
if(!out[0]) {
snprintf(out, size, "download");
}
}
static size_t
header_quoted_filename(char *out, size_t size, const char *name) {
size_t len = 0;
while(*name && len + 1 < size) {
unsigned char c = (unsigned char)*name++;
if(c < 0x20 || c == 0x7f || c == '"' || c == '\\') {
c = '_';
}
out[len++] = (char)c;
}
out[len] = 0;
return len;
}
static size_t
header_percent_filename(char *out, size_t size, const char *name) {
static const char hex[] = "0123456789ABCDEF";
size_t len = 0;
while(*name && len + 1 < size) {
unsigned char c = (unsigned char)*name++;
int safe = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') || c == '.' || c == '_' || c == '-';
if(safe) {
out[len++] = (char)c;
} else {
if(len + 3 >= size) {
break;
}
out[len++] = '%';
out[len++] = hex[c >> 4];
out[len++] = hex[c & 15];
}
}
out[len] = 0;
return len;
}
static void
add_download_filename_header(struct MHD_Response *resp, const char *name) {
char quoted[PATH_MAX];
char encoded[PATH_MAX * 3];
char header[PATH_MAX * 4];
header_quoted_filename(quoted, sizeof(quoted), name);
header_percent_filename(encoded, sizeof(encoded), name);
snprintf(header, sizeof(header),
"attachment; filename=\"%s\"; filename*=UTF-8''%s",
quoted, encoded);
MHD_add_response_header(resp, "Content-Disposition", header);
}
static enum MHD_Result
create_download_task_response(struct MHD_Connection *conn, char **paths,
size_t count) {
file_task_t *task = calloc(1, sizeof(file_task_t));
strbuf_t b = {0};
struct stat st;
size_t i;
if(!task) {
free_paths(paths, count);
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
if(!count) {
return download_task_request_error(conn, task, paths, count,
MHD_HTTP_BAD_REQUEST, "no source paths");
}
for(i = 0; i < count; i++) {
if(lstat(paths[i], &st)) {
return download_task_request_error(conn, task, paths, count,
MHD_HTTP_NOT_FOUND, "file not found");
}
if(!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) {
return download_task_request_error(conn, task, paths, count,
MHD_HTTP_BAD_REQUEST, "invalid path");
}
}
task->op = TASK_DOWNLOAD;
task->state = TASK_QUEUED;
task->srcs = paths;
task->src_count = count;
snprintf(task->src, sizeof(task->src), "%s%s", paths[0],
count > 1 ? " ..." : "");
snprintf(task->current, sizeof(task->current), "%s", task->src);
task->created_at = time(NULL);
task->updated_at = task->created_at;
pthread_mutex_lock(&g_tasks_lock);
remove_finished_tasks_locked();
if(has_active_task_locked()) {
pthread_mutex_unlock(&g_tasks_lock);
return download_task_request_error(conn, task, paths, count,
MHD_HTTP_CONFLICT, "another task is running");
}
task->id = g_next_task_id++;
task->next = g_tasks;
g_tasks = task;
pthread_mutex_unlock(&g_tasks_lock);
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
}
enum MHD_Result
api_download_prepare(struct MHD_Connection *conn, const char *body,
size_t body_size) {
char *paths_raw = body_form_value(body, body_size, "paths");
char **paths = NULL;
size_t count = 0;
if(!paths_raw || parse_paths(paths_raw, &paths, &count)) {
free(paths_raw);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
free(paths_raw);
return create_download_task_response(conn, paths, count);
}
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;
if(task_cancel_requested(s->task)) {
s->error = ECANCELED;
return MHD_CONTENT_READER_END_WITH_ERROR;
}
if(fseek(s->file, (long)pos, SEEK_SET)) {
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;
}
s->done = 1;
return MHD_CONTENT_READER_END_OF_STREAM;
}
{
unsigned long long end = (unsigned long long)pos + (unsigned long long)len;
unsigned long long add = end > s->sent ? end - s->sent : 0;
if(end > s->sent) {
s->sent = end;
}
if(s->sent >= s->size) {
s->done = 1;
}
task_update(s->task, TASK_RUNNING, s->task->src, add, NULL);
}
return (ssize_t)len;
}
static void
download_file_close(void *cls) {
download_file_stream_t *s = cls;
int canceled;
if(!s) {
return;
}
if(s->file) {
fclose(s->file);
}
canceled = task_cancel_requested(s->task);
if(canceled) {
task_update(s->task, TASK_CANCELED, s->task->src, 0, "canceled");
} else if(s->error) {
errno = s->error;
task_update(s->task, TASK_FAILED, s->task->src, 0, strerror(errno));
} else if(s->done) {
task_update(s->task, TASK_DONE, s->task->src, 0, NULL);
} else {
task_update(s->task, TASK_FAILED, s->task->src, 0, "client disconnected");
}
free(s);
}
enum MHD_Result
api_download(struct MHD_Connection *conn) {
char *idstr = query_value(conn, "id");
unsigned long id = idstr ? strtoul(idstr, NULL, 10) : 0;
char **paths = NULL;
size_t count = 0;
struct stat st;
tar_stream_t *stream;
struct MHD_Response *resp;
enum MHD_Result ret;
file_task_t *task;
char download_name[PATH_MAX];
free(idstr);
pthread_mutex_lock(&g_tasks_lock);
task = find_task_locked(id);
if(!task || task->op != TASK_DOWNLOAD || task->state != TASK_QUEUED) {
pthread_mutex_unlock(&g_tasks_lock);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "active task not found");
}
task->state = TASK_RUNNING;
task->updated_at = time(NULL);
pthread_mutex_unlock(&g_tasks_lock);
if(prepare_download_task(task)) {
char current[PATH_MAX];
snprintf(current, sizeof(current), "%s",
task->current[0] ? task->current : task->src);
if(errno == ECANCELED || task_cancel_requested(task)) {
task_update(task, TASK_CANCELED, current, 0, "canceled");
} else {
task_update(task, TASK_FAILED, current, 0, strerror(errno));
}
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
}
paths = task->srcs;
count = task->src_count;
if(count == 1 && !stat(paths[0], &st) && S_ISREG(st.st_mode)) {
download_file_stream_t *file_stream = calloc(1, sizeof(*file_stream));
if(!file_stream) {
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
file_stream->task = task;
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) {
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,
download_file_read, file_stream,
download_file_close);
if(!resp) {
download_file_close(file_stream);
return MHD_NO;
}
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"application/octet-stream");
download_file_name(download_name, sizeof(download_name), paths[0]);
add_download_filename_header(resp, download_name);
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
if(!(stream = calloc(1, sizeof(*stream)))) {
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
stream->fd = -1;
stream->task = task;
stream->paths = NULL;
stream->path_count = count;
stream->paths = calloc(count, sizeof(char *));
if(!stream->paths) {
tar_close(stream);
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
for(size_t i = 0; i < count; i++) {
stream->paths[i] = strdup(paths[i]);
if(!stream->paths[i]) {
stream->path_count = i;
tar_close(stream);
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
}
resp = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN, 32 * 0x4000,
tar_read, stream, tar_close);
if(!resp) {
tar_close(stream);
return MHD_NO;
}
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"application/x-tar");
download_archive_name(download_name, sizeof(download_name), paths, count);
add_download_filename_header(resp, download_name);
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
+68
View File
@@ -0,0 +1,68 @@
#include "filemgr.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "filemgr_internal.h"
#include "mime.h"
#include "path_util.h"
#include "websrv.h"
static ssize_t
file_read(void *cls, uint64_t pos, char *buf, size_t max) {
FILE *file = cls;
size_t len;
if(fseek(file, (long)pos, SEEK_SET)) {
return MHD_CONTENT_READER_END_WITH_ERROR;
}
if(!(len = fread(buf, 1, max, file))) {
return ferror(file) ? MHD_CONTENT_READER_END_WITH_ERROR :
MHD_CONTENT_READER_END_OF_STREAM;
}
return (ssize_t)len;
}
static void
file_close(void *cls) {
fclose((FILE *)cls);
}
enum MHD_Result
filemgr_fs_request(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
struct MHD_Response *resp;
enum MHD_Result ret = MHD_NO;
struct stat st;
FILE *file;
if(has_active_task()) {
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
if(!path || stat(path, &st) || !S_ISREG(st.st_mode) ||
!(file = fopen(path, "rb"))) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
if((resp = MHD_create_response_from_callback((uint64_t)st.st_size,
32 * 0x4000, file_read, file,
file_close))) {
const char *mime = mime_get_type(path);
if(mime) {
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE, mime);
}
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
free(path);
return ret;
}
fclose(file);
free(path);
return MHD_NO;
}
+1097 -982
View File
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -3,5 +3,12 @@
#include <microhttpd.h>
enum MHD_Result filemgr_api_request(struct MHD_Connection *conn,
const char *url);
const char *url, const char *method,
const char *body, size_t body_size);
enum MHD_Result filemgr_fs_request(struct MHD_Connection *conn);
int filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx);
int filemgr_upload_data(void *upload_ctx, const char *data, size_t size);
enum MHD_Result filemgr_upload_finish(struct MHD_Connection *conn,
void *upload_ctx);
void filemgr_upload_free(void *upload_ctx);
+120
View File
@@ -0,0 +1,120 @@
#pragma once
#include <limits.h>
#include <pthread.h>
#include <stddef.h>
#include <time.h>
#include <microhttpd.h>
#define ETA_SAMPLE_SLOTS 64
typedef enum task_op {
TASK_COPY,
TASK_MOVE,
TASK_DELETE,
TASK_DOWNLOAD,
TASK_UPLOAD,
} task_op_t;
typedef enum task_state {
TASK_QUEUED,
TASK_RUNNING,
TASK_DONE,
TASK_FAILED,
TASK_CANCELED,
} task_state_t;
typedef struct task_eta_sample {
unsigned long long done;
struct timespec time;
} task_eta_sample_t;
typedef struct file_task {
unsigned long id;
task_op_t op;
task_state_t state;
char src[PATH_MAX];
char dst[PATH_MAX];
char current[PATH_MAX];
char error[160];
char error_code[64];
char error_arg[PATH_MAX + 96];
char **srcs;
size_t src_count;
size_t file_count;
size_t dir_count;
unsigned long long total;
unsigned long long done;
unsigned long long speed;
unsigned long long eta;
unsigned long long speed_sample_done;
struct timespec speed_sample_time;
task_eta_sample_t eta_samples[ETA_SAMPLE_SLOTS];
unsigned int eta_sample_next;
unsigned int eta_sample_count;
int cancel_requested;
time_t created_at;
time_t transfer_started_at;
time_t updated_at;
pthread_t thread;
struct file_task *next;
} file_task_t;
extern pthread_mutex_t g_tasks_lock;
extern file_task_t *g_tasks;
extern unsigned long g_next_task_id;
const char *task_op_name(task_op_t op);
const char *task_state_name(task_state_t state);
int task_is_active(const file_task_t *task);
int has_active_task_locked(void);
int has_active_task(void);
void free_task(file_task_t *task);
void remove_finished_tasks_locked(void);
int task_cancel_requested(file_task_t *task);
file_task_t *find_task_locked(unsigned long id);
void task_update(file_task_t *task, task_state_t state, const char *current,
unsigned long long add_done, const char *error);
void record_task_completion_locked(file_task_t *task, time_t completed_at);
enum MHD_Result send_json_ok(struct MHD_Connection *conn);
enum MHD_Result send_json_error(struct MHD_Connection *conn,
unsigned int status, const char *msg);
enum MHD_Result send_json_error_detail(struct MHD_Connection *conn,
unsigned int status, const char *msg,
const char *code, const char *arg);
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_0777(const char *path);
int fchmod_0777(int fd);
int ignore_chmod_error(int err);
int mode_access(const char *path, int mode);
int check_target_writable(const char *target, char ***checked_dirs,
size_t *checked_dir_count,
char *error, size_t error_size,
char *code, size_t code_size,
char *arg, size_t arg_size);
int check_target_space(const char *target, unsigned long long required,
char *error, size_t error_size,
char *code, size_t code_size,
char *arg, size_t arg_size);
int target_available_space(const char *target, unsigned long long *available);
int count_task_path_bytes(file_task_t *task, const char *path,
const char *display, unsigned long long *total,
size_t *file_count, size_t *dir_count);
enum MHD_Result api_upload_prepare(struct MHD_Connection *conn,
const char *body, size_t body_size);
enum MHD_Result api_upload_finish(struct MHD_Connection *conn);
enum MHD_Result api_download_prepare(struct MHD_Connection *conn,
const char *body, size_t body_size);
enum MHD_Result api_download(struct MHD_Connection *conn);
enum MHD_Result api_list(struct MHD_Connection *conn);
enum MHD_Result api_space(struct MHD_Connection *conn);
enum MHD_Result api_text(struct MHD_Connection *conn);
enum MHD_Result api_text_create(struct MHD_Connection *conn);
enum MHD_Result api_text_save(struct MHD_Connection *conn, const char *body,
size_t body_size);
+356
View File
@@ -0,0 +1,356 @@
#include "filemgr_internal.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#ifdef __linux__
#include <sys/vfs.h>
#else
#include <sys/mount.h>
#endif
#include <time.h>
#include <unistd.h>
#include "path_util.h"
int
ignore_chmod_error(int err) {
return err == ENOTSUP || err == EPERM || err == EINVAL || err == EROFS;
}
#ifndef __linux__
static int
fs_type_has_unix_modes(const char *type) {
return strcmp(type, "exfat") &&
strcmp(type, "exfatfs") &&
strcmp(type, "msdosfs") &&
strcmp(type, "fat") &&
strcmp(type, "vfat");
}
#endif
static int
path_has_unix_modes(const char *path) {
#ifdef __linux__
(void)path;
return 1;
#else
struct statfs fs;
if(statfs(path, &fs)) {
return 1;
}
return fs_type_has_unix_modes(fs.f_fstypename);
#endif
}
static int
fd_has_unix_modes(int fd) {
#ifdef __linux__
(void)fd;
return 1;
#else
struct statfs fs;
if(fstatfs(fd, &fs)) {
return 1;
}
return fs_type_has_unix_modes(fs.f_fstypename);
#endif
}
int
chmod_path_0777(const char *path) {
if(!path_has_unix_modes(path)) {
return 0;
}
if(chmod(path, 0777) && !ignore_chmod_error(errno)) {
return -1;
}
return 0;
}
int
fchmod_0777(int fd) {
if(!fd_has_unix_modes(fd)) {
return 0;
}
if(fchmod(fd, 0777) && !ignore_chmod_error(errno)) {
return -1;
}
return 0;
}
static int
target_statvfs(const char *target, struct statvfs *vfs) {
char parent[PATH_MAX];
if(!statvfs(target, vfs)) {
return 0;
}
if(path_dirname(target, parent, sizeof(parent))) {
return -1;
}
return statvfs(parent, vfs);
}
int
target_available_space(const char *target, unsigned long long *available) {
struct statvfs vfs;
unsigned long long block_size;
if(target_statvfs(target, &vfs)) {
return -1;
}
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
*available = (unsigned long long)vfs.f_bavail * block_size;
return 0;
}
int
check_target_space(const char *target, unsigned long long required,
char *error, size_t error_size,
char *code, size_t code_size,
char *arg, size_t arg_size) {
unsigned long long available;
if(!required) {
return 0;
}
if(target_available_space(target, &available)) {
snprintf(error, error_size, "cannot read target free space");
snprintf(code, code_size, "space_check_failed");
snprintf(arg, arg_size, "%s", target);
return -1;
}
if(available < required) {
snprintf(error, error_size,
"not enough target space, required %llu bytes, available %llu bytes",
required, available);
snprintf(code, code_size, "no_space");
snprintf(arg, arg_size, "%llu,%llu", required, available);
errno = ENOSPC;
return -1;
}
return 0;
}
static void
set_error_detail(char *error, size_t error_size, char *code, size_t code_size,
char *arg, size_t arg_size, const char *error_code,
const char *message, const char *path) {
snprintf(error, error_size, "%s: %s", message, path);
snprintf(code, code_size, "%s", error_code);
snprintf(arg, arg_size, "%s", path);
}
static void
set_permission_error_detail(char *error, size_t error_size,
char *code, size_t code_size,
char *arg, size_t arg_size,
const char *error_code, const char *message,
const char *path) {
struct stat st;
set_error_detail(error, error_size, code, code_size, arg, arg_size,
error_code, message, path);
if(!stat(path, &st)) {
snprintf(arg, arg_size, "%s (mode=%04o, uid=%lu, gid=%lu)", path,
(unsigned int)(st.st_mode & 07777),
(unsigned long)st.st_uid, (unsigned long)st.st_gid);
}
}
static int
mode_access_stat(const struct stat *st, int mode) {
mode_t allowed;
uid_t uid = geteuid();
if(uid == 0) {
if(!(mode & X_OK) || !S_ISREG(st->st_mode) ||
(st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
return 0;
}
} else if(uid == st->st_uid) {
allowed = (st->st_mode >> 6) & 7;
if((allowed & mode) == (mode_t)mode) {
return 0;
}
} else {
gid_t gid = getegid();
int group_match = gid == st->st_gid;
if(!group_match) {
int count = getgroups(0, NULL);
gid_t *groups = count > 0 ? malloc((size_t)count * sizeof(*groups)) : NULL;
if(groups && getgroups(count, groups) == count) {
int i;
for(i = 0; i < count; i++) {
if(groups[i] == st->st_gid) {
group_match = 1;
break;
}
}
}
free(groups);
}
allowed = group_match ? (st->st_mode >> 3) & 7 : st->st_mode & 7;
if((allowed & mode) == (mode_t)mode) {
return 0;
}
}
errno = EACCES;
return -1;
}
int
mode_access(const char *path, int mode) {
struct stat st;
return stat(path, &st) ? -1 : mode_access_stat(&st, mode);
}
static int
probe_dir_writable(const char *path) {
char name[80];
char probe[PATH_MAX];
int attempt;
for(attempt = 0; attempt < 16; attempt++) {
int fd;
int error = 0;
snprintf(name, sizeof(name), ".web-file-mgr-%ld-%lld-%d.tmp",
(long)getpid(), (long long)time(NULL), attempt);
if(path_join(probe, sizeof(probe), path, name)) {
return -1;
}
fd = open(probe, O_WRONLY | O_CREAT | O_EXCL, 0600);
if(fd < 0) {
if(errno == EEXIST) {
continue;
}
return -1;
}
if(close(fd)) {
error = errno;
}
if(unlink(probe) && !error) {
error = errno;
}
if(error) {
errno = error;
return -1;
}
return 0;
}
errno = EEXIST;
return -1;
}
static int
probe_file_writable(const char *path) {
int fd = open(path, O_WRONLY);
return fd < 0 ? -1 : close(fd);
}
static int
path_seen(char **paths, size_t count, const char *path) {
size_t i;
for(i = 0; i < count; i++) {
if(!strcmp(paths[i], path)) {
return 1;
}
}
return 0;
}
static int
remember_path(char ***paths, size_t *count, const char *path) {
char **tmp;
if(path_seen(*paths, *count, path)) {
return 0;
}
tmp = realloc(*paths, sizeof(char *) * (*count + 1));
if(!tmp) {
errno = ENOMEM;
return -1;
}
*paths = tmp;
(*paths)[*count] = strdup(path);
if(!(*paths)[*count]) {
errno = ENOMEM;
return -1;
}
(*count)++;
return 0;
}
static int
probe_dir_once(const char *path, char ***checked, size_t *checked_count) {
if(path_seen(*checked, *checked_count, path)) {
return 0;
}
if(probe_dir_writable(path)) {
return -1;
}
return remember_path(checked, checked_count, path);
}
int
check_target_writable(const char *target, char ***checked_dirs,
size_t *checked_dir_count, char *error, size_t error_size,
char *code, size_t code_size, char *arg, size_t arg_size) {
struct stat st;
char parent[PATH_MAX];
if(!stat(target, &st)) {
if(S_ISDIR(st.st_mode)) {
if(probe_dir_once(target, checked_dirs, checked_dir_count)) {
set_permission_error_detail(error, error_size, code, code_size,
arg, arg_size, "target_dir_not_writable",
"target directory is not writable", target);
return -1;
}
} else {
if(probe_file_writable(target)) {
set_permission_error_detail(error, error_size, code, code_size,
arg, arg_size, "target_file_not_writable",
"target file is not writable", target);
return -1;
}
goto check_parent;
}
return 0;
}
if(errno != ENOENT) {
set_error_detail(error, error_size, code, code_size, arg, arg_size,
"target_check_failed", "cannot check target path", target);
return -1;
}
check_parent:
if(path_dirname(target, parent, sizeof(parent))) {
set_error_detail(error, error_size, code, code_size, arg, arg_size,
"target_check_failed", "cannot check target path", target);
return -1;
}
if(probe_dir_once(parent, checked_dirs, checked_dir_count)) {
set_permission_error_detail(error, error_size, code, code_size,
arg, arg_size, "target_parent_not_writable",
"current directory is not writable", parent);
return -1;
}
return 0;
}
+90
View File
@@ -0,0 +1,90 @@
#include "json_util.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int
strbuf_reserve(strbuf_t *b, size_t extra) {
size_t need = b->len + extra + 1;
char *tmp;
size_t cap;
if(need <= b->cap) {
return 0;
}
cap = b->cap ? b->cap : 4096;
while(cap < need) {
cap *= 2;
}
if(!(tmp = realloc(b->data, cap))) {
return -1;
}
b->data = tmp;
b->cap = cap;
return 0;
}
int
strbuf_append(strbuf_t *b, const char *s) {
size_t n = strlen(s);
if(strbuf_reserve(b, n)) {
return -1;
}
memcpy(b->data + b->len, s, n + 1);
b->len += n;
return 0;
}
int
strbuf_printf(strbuf_t *b, const char *fmt, ...) {
va_list ap;
va_list cp;
int n;
va_start(ap, fmt);
va_copy(cp, ap);
n = vsnprintf(NULL, 0, fmt, cp);
va_end(cp);
if(n < 0 || strbuf_reserve(b, (size_t)n)) {
va_end(ap);
return -1;
}
vsnprintf(b->data + b->len, b->cap - b->len, fmt, ap);
va_end(ap);
b->len += (size_t)n;
return 0;
}
int
json_escape(strbuf_t *b, const char *s) {
if(strbuf_append(b, "\"")) return -1;
while(*s) {
unsigned char c = (unsigned char)*s;
switch(c) {
case '"': if(strbuf_append(b, "\\\"")) return -1; s++; break;
case '\\': if(strbuf_append(b, "\\\\")) return -1; s++; break;
case '\b': if(strbuf_append(b, "\\b")) return -1; s++; break;
case '\f': if(strbuf_append(b, "\\f")) return -1; s++; break;
case '\n': if(strbuf_append(b, "\\n")) return -1; s++; break;
case '\r': if(strbuf_append(b, "\\r")) return -1; s++; break;
case '\t': if(strbuf_append(b, "\\t")) return -1; s++; break;
default:
if(c < 0x20 || c >= 0x80) {
if(strbuf_printf(b, "\\u%04x", c)) return -1;
} else {
if(strbuf_reserve(b, 1)) return -1;
b->data[b->len++] = (char)c;
b->data[b->len] = 0;
}
s++;
break;
}
}
return strbuf_append(b, "\"");
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <stddef.h>
typedef struct strbuf {
char *data;
size_t len;
size_t cap;
} strbuf_t;
int strbuf_reserve(strbuf_t *b, size_t extra);
int strbuf_append(strbuf_t *b, const char *s);
int strbuf_printf(strbuf_t *b, const char *fmt, ...);
int json_escape(strbuf_t *b, const char *s);
+84
View File
@@ -0,0 +1,84 @@
#include "filemgr_internal.h"
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "json_util.h"
#include "path_util.h"
static char
mode_type(const struct stat *st) {
if(S_ISDIR(st->st_mode)) return 'd';
if(S_ISLNK(st->st_mode)) return 'l';
if(S_ISCHR(st->st_mode)) return 'c';
if(S_ISBLK(st->st_mode)) return 'b';
if(S_ISFIFO(st->st_mode)) return 'p';
if(S_ISSOCK(st->st_mode)) return 's';
return '-';
}
enum MHD_Result
api_list(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
DIR *dir;
struct dirent *entry;
struct stat st;
strbuf_t b = {0};
int first = 1;
if(has_active_task()) {
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
if(!path) {
path = strdup("/");
}
if(!(dir = opendir(path))) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, NULL);
}
strbuf_append(&b, "{\"ok\":true,\"path\":");
json_escape(&b, path);
strbuf_append(&b, ",\"parent\":");
char parent[PATH_MAX];
if(path_dirname(path, parent, sizeof(parent))) {
strcpy(parent, "/");
}
json_escape(&b, parent);
strbuf_append(&b, ",\"entries\":[");
while((entry = readdir(dir))) {
char child[PATH_MAX];
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
continue;
}
if(path_join(child, sizeof(child), path, entry->d_name) ||
lstat(child, &st)) {
continue;
}
if(!first) {
strbuf_append(&b, ",");
}
first = 0;
strbuf_append(&b, "{\"name\":");
json_escape(&b, entry->d_name);
strbuf_append(&b, ",\"path\":");
json_escape(&b, child);
strbuf_printf(&b, ",\"type\":\"%c\",\"mode\":%u,\"size\":%lld,\"mtime\":%lld}",
mode_type(&st), (unsigned int)(st.st_mode & 07777),
(long long)st.st_size, (long long)st.st_mtime);
}
closedir(dir);
free(path);
strbuf_append(&b, "]}");
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
}
+3
View File
@@ -150,6 +150,9 @@ main(int argc, char **argv) {
#endif
websrv_listen(port);
if(websrv_stop_requested()) {
break;
}
sleep(3);
}
+3
View File
@@ -8,8 +8,10 @@ typedef struct mime_entry {
} mime_entry_t;
static const mime_entry_t g_mimes[] = {
{"bmp", "image/bmp"},
{"css", "text/css"},
{"elf", "application/octet-stream"},
{"gif", "image/gif"},
{"html", "text/html"},
{"jpeg", "image/jpeg"},
{"jpg", "image/jpeg"},
@@ -20,6 +22,7 @@ static const mime_entry_t g_mimes[] = {
{"png", "image/png"},
{"txt", "text/plain"},
{"xml", "text/xml"},
{"webp", "image/webp"},
{"zip", "application/zip"},
};
+401
View File
@@ -0,0 +1,401 @@
#include "path_util.h"
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *
query_value(struct MHD_Connection *conn, const char *key) {
const char *raw = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, key);
char *value = raw ? strdup(raw) : NULL;
if(value && !strcmp(key, "name")) {
char *start = value;
char *end;
while(isspace((unsigned char)*start)) start++;
end = start + strlen(start);
while(end > start && isspace((unsigned char)end[-1])) end--;
memmove(value, start, (size_t)(end - start));
value[end - start] = 0;
}
return value;
}
char *
header_value(struct MHD_Connection *conn, const char *key) {
const char *raw = MHD_lookup_connection_value(conn, MHD_HEADER_KIND, key);
return raw ? form_decode(raw, strlen(raw)) : NULL;
}
char *
request_value(struct MHD_Connection *conn, const char *header,
const char *query) {
char *value = header_value(conn, header);
return value ? value : query_value(conn, query);
}
static int
hex_value(char c) {
if(c >= '0' && c <= '9') return c - '0';
if(c >= 'a' && c <= 'f') return c - 'a' + 10;
if(c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
char *
form_decode(const char *src, size_t len) {
char *out = malloc(len + 1);
size_t i;
size_t j = 0;
if(!out) {
return NULL;
}
for(i = 0; i < len; i++) {
if(src[i] == '+') {
out[j++] = ' ';
} else if(src[i] == '%' && i + 2 < len) {
int hi = hex_value(src[i + 1]);
int lo = hex_value(src[i + 2]);
if(hi >= 0 && lo >= 0) {
out[j++] = (char)((hi << 4) | lo);
i += 2;
} else {
out[j++] = src[i];
}
} else {
out[j++] = src[i];
}
}
out[j] = 0;
return out;
}
char *
body_form_value(const char *body, size_t body_size, const char *key) {
size_t key_len = strlen(key);
size_t pos = 0;
while(body && pos < body_size) {
size_t start = pos;
size_t end;
size_t eq;
while(pos < body_size && body[pos] != '&') {
pos++;
}
end = pos;
if(pos < body_size && body[pos] == '&') {
pos++;
}
eq = start;
while(eq < end && body[eq] != '=') {
eq++;
}
if(eq - start == key_len && !strncmp(body + start, key, key_len)) {
return form_decode(body + eq + (eq < end), end - eq - (eq < end));
}
}
return NULL;
}
void
free_paths(char **paths, size_t count) {
size_t i;
if(!paths) {
return;
}
for(i = 0; i < count; i++) {
free(paths[i]);
}
free(paths);
}
int
parse_paths(const char *raw, char ***out_paths, size_t *out_count) {
char *copy;
char *line;
char *save;
char **paths = NULL;
size_t count = 0;
size_t capacity = 0;
const char *p;
int in_line = 0;
*out_paths = NULL;
*out_count = 0;
if(!raw || !raw[0]) {
errno = EINVAL;
return -1;
}
for(p = raw; *p; p++) {
if(*p == '\n') {
if(in_line) {
capacity++;
in_line = 0;
}
continue;
}
in_line = 1;
}
if(in_line) {
capacity++;
}
if(!capacity) {
errno = EINVAL;
return -1;
}
if(!(paths = calloc(capacity, sizeof(char *))) ||
!(copy = strdup(raw))) {
free(paths);
errno = ENOMEM;
return -1;
}
for(line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
if(!line[0]) {
continue;
}
if(!(paths[count] = fs_path_value(strdup(line)))) {
free_paths(paths, count);
free(copy);
errno = ENOMEM;
return -1;
}
count++;
}
free(copy);
if(!count) {
errno = EINVAL;
return -1;
}
*out_paths = paths;
*out_count = count;
return 0;
}
const char *
path_basename(const char *path) {
const char *end = path + strlen(path);
const char *base;
while(end > path && end[-1] == '/') {
end--;
}
base = end;
while(base > path && base[-1] != '/') {
base--;
}
return base;
}
void
path_basename_copy(const char *path, char *out, size_t size) {
const char *end = path + strlen(path);
const char *base;
size_t len;
while(end > path && end[-1] == '/') {
end--;
}
base = end;
while(base > path && base[-1] != '/') {
base--;
}
len = (size_t)(end - base);
if(!len) {
snprintf(out, size, "root");
return;
}
if(len >= size) {
len = size - 1;
}
memcpy(out, base, len);
out[len] = 0;
}
int
path_dirname(const char *path, char *out, size_t size) {
const char *base = path_basename(path);
size_t len = (size_t)(base - path);
while(len > 1 && path[len - 1] == '/') {
len--;
}
if(!len) {
len = 1;
}
if(len >= size) {
return -1;
}
memcpy(out, path, len);
out[len] = 0;
return 0;
}
int
path_join(char *out, size_t size, const char *dir, const char *name) {
int n;
if(!dir || !name || !dir[0] || !name[0] || strchr(name, '/')) {
errno = EINVAL;
return -1;
}
n = snprintf(out, size, "%s%s%s", dir,
(strcmp(dir, "/") && dir[strlen(dir) - 1] != '/') ? "/" : "",
name);
if(n < 0 || (size_t)n >= size) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
int
relative_path_safe(const char *path) {
const char *p = path;
if(!path || !path[0] || path[0] == '/') {
errno = EINVAL;
return 0;
}
while(*p) {
const char *start = p;
size_t len;
while(*p && *p != '/') {
if(*p == '\\') {
errno = EINVAL;
return 0;
}
p++;
}
len = (size_t)(p - start);
if(!len || (len == 1 && start[0] == '.') ||
(len == 2 && start[0] == '.' && start[1] == '.')) {
errno = EINVAL;
return 0;
}
if(*p == '/') {
p++;
}
}
return 1;
}
int
path_join_relative(char *out, size_t size, const char *dir, const char *rel) {
int n;
if(!relative_path_safe(rel)) {
return -1;
}
n = snprintf(out, size, "%s%s%s", dir,
(strcmp(dir, "/") && dir[strlen(dir) - 1] != '/') ? "/" : "",
rel);
if(n < 0 || (size_t)n >= size) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
static int
utf8_decode_char(const char **ps, unsigned int *out) {
const unsigned char *s = (const unsigned char *)*ps;
unsigned char c = s[0];
unsigned int cp;
size_t n;
size_t i;
if(c < 0x80) {
*out = c;
*ps += 1;
return 0;
}
if((c & 0xe0) == 0xc0) {
cp = c & 0x1f;
n = 2;
} else if((c & 0xf0) == 0xe0) {
cp = c & 0x0f;
n = 3;
} else if((c & 0xf8) == 0xf0) {
cp = c & 0x07;
n = 4;
} else {
return -1;
}
for(i = 1; i < n; i++) {
unsigned char t = s[i];
if(!t || (t & 0xc0) != 0x80) {
return -1;
}
cp = (cp << 6) | (t & 0x3f);
}
if((n == 2 && cp < 0x80) ||
(n == 3 && cp < 0x800) ||
(n == 4 && (cp < 0x10000 || cp > 0x10ffff)) ||
(cp >= 0xd800 && cp <= 0xdfff)) {
return -1;
}
*out = cp;
*ps += n;
return 0;
}
char *
fs_path_value(char *path) {
const char *p;
char *out;
size_t len;
size_t pos = 0;
int has_byte_token = 0;
if(!path) {
return path;
}
for(p = path; *p;) {
unsigned int cp;
const char *next = p;
if(utf8_decode_char(&next, &cp)) {
return path;
}
if(cp >= 0x80 && cp <= 0xff) {
has_byte_token = 1;
} else if(cp > 0xff) {
return path;
}
p = next;
}
if(!has_byte_token) {
return path;
}
len = strlen(path);
if(!(out = malloc(len + 1))) {
return path;
}
for(p = path; *p;) {
unsigned int cp;
const char *next = p;
if(utf8_decode_char(&next, &cp)) {
free(out);
return path;
}
out[pos++] = (char)(unsigned char)cp;
p = next;
}
out[pos] = 0;
free(path);
return out;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <stddef.h>
#include <limits.h>
#include <microhttpd.h>
char *query_value(struct MHD_Connection *conn, const char *key);
char *header_value(struct MHD_Connection *conn, const char *key);
char *request_value(struct MHD_Connection *conn, const char *header,
const char *query);
char *form_decode(const char *src, size_t len);
char *body_form_value(const char *body, size_t body_size, const char *key);
void free_paths(char **paths, size_t count);
int parse_paths(const char *raw, char ***out_paths, size_t *out_count);
const char *path_basename(const char *path);
void path_basename_copy(const char *path, char *out, size_t size);
int path_dirname(const char *path, char *out, size_t size);
int path_join(char *out, size_t size, const char *dir, const char *name);
int relative_path_safe(const char *path);
int path_join_relative(char *out, size_t size, const char *dir, const char *rel);
char *fs_path_value(char *path);
+108
View File
@@ -0,0 +1,108 @@
#include "pkg_installer.h"
#ifndef __linux__
#include <limits.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
typedef struct pkg_metadata {
const char *uri;
const char *ex_uri;
const char *playgo_scenario_id;
const char *content_id;
const char *content_name;
const char *icon_url;
} pkg_metadata_t;
typedef struct pkg_info {
char content_id[48];
int type;
int platform;
} pkg_info_t;
typedef struct playgo_info {
char lang[8][30];
char scenario_ids[3][64];
char content_ids[64];
long unknown[810];
} playgo_info_t;
int sceAppInstUtilInitialize(void);
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
playgo_info_t *);
static pthread_mutex_t installer_lock = PTHREAD_MUTEX_INITIALIZER;
static int installer_initialized;
static int
initialize_locked(void) {
int result;
if(!installer_initialized) {
result = sceAppInstUtilInitialize();
if(result) return result;
installer_initialized = 1;
}
return 0;
}
int
pkg_installer_initialize(void) {
int result;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
pthread_mutex_unlock(&installer_lock);
return result;
}
int
pkg_installer_install(const char *path) {
char install_path[PATH_MAX + sizeof("/user")];
const char *uri = path;
pkg_metadata_t metadata = {
.uri = NULL,
.ex_uri = "",
.playgo_scenario_id = "",
.content_id = "",
.content_name = "",
.icon_url = ""
};
pkg_info_t pkg_info = {0};
playgo_info_t playgo_info = {0};
int result;
if(!path) return -1;
if(!strncmp(path, "/data/", 6)) {
snprintf(install_path, sizeof(install_path), "/user%s", path);
uri = install_path;
}
metadata.uri = uri;
pthread_mutex_lock(&installer_lock);
result = initialize_locked();
if(result) {
pthread_mutex_unlock(&installer_lock);
return result;
}
result = sceAppInstUtilInstallByPackage(&metadata, &pkg_info, &playgo_info);
pthread_mutex_unlock(&installer_lock);
return result;
}
#else
int
pkg_installer_initialize(void) {
return PKG_INSTALL_UNSUPPORTED;
}
int
pkg_installer_install(const char *path) {
(void)path;
return PKG_INSTALL_UNSUPPORTED;
}
#endif
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
int pkg_installer_initialize(void);
int pkg_installer_install(const char *path);
+128
View File
@@ -0,0 +1,128 @@
#include "filemgr_internal.h"
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#ifdef __SCE__
#include <sys/mount.h>
#endif
#include "json_util.h"
#include "path_util.h"
static unsigned long long
vfs_bytes(fsblkcnt_t blocks, unsigned long block_size) {
return (unsigned long long)blocks * (unsigned long long)block_size;
}
#ifdef __SCE__
static int
path_is_mounted(const char *path, const struct stat *st) {
char parent[PATH_MAX];
struct stat parent_st;
if(!strcmp(path, "/")) {
return 1;
}
if(path_dirname(path, parent, sizeof(parent)) || stat(parent, &parent_st)) {
return 0;
}
return st->st_dev != parent_st.st_dev;
}
#endif
enum MHD_Result
api_space(struct MHD_Connection *conn) {
#ifdef __SCE__
static const struct {
const char *label_key;
const char *path;
} mounts[] = {
{"storageInternal", "/data"},
{"storageUsb", "/mnt/usb0"},
{"storageUsb", "/mnt/usb1"},
{"storageUsb", "/mnt/usb2"},
{"storageUsb", "/mnt/usb3"},
{"storageUsb", "/mnt/usb4"},
{"storageUsb", "/mnt/usb5"},
{"storageUsb", "/mnt/usb6"},
{"storageUsb", "/mnt/usb7"},
{"storageM2", "/mnt/ext1"},
{"storageExtended", "/mnt/ext0"},
};
#endif
char *current = fs_path_value(query_value(conn, "path"));
strbuf_t b = {0};
int first = 1;
strbuf_append(&b, "{\"ok\":true,\"spaces\":[");
#ifdef __SCE__
for(size_t i = 0; i < sizeof(mounts) / sizeof(mounts[0]); i++) {
struct statvfs vfs;
struct stat st;
unsigned long block_size;
unsigned long long free_bytes;
unsigned long long total_bytes;
int is_current = 0;
if(stat(mounts[i].path, &st) || !path_is_mounted(mounts[i].path, &st) ||
statvfs(mounts[i].path, &vfs)) {
continue;
}
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
free_bytes = vfs_bytes(vfs.f_bavail, block_size);
total_bytes = vfs_bytes(vfs.f_blocks, block_size);
if(current) {
if(!strcmp(mounts[i].path, "/")) {
is_current = !strcmp(current, "/");
} else if(!strncmp(current, mounts[i].path, strlen(mounts[i].path)) &&
(current[strlen(mounts[i].path)] == 0 ||
current[strlen(mounts[i].path)] == '/')) {
is_current = 1;
}
}
if(!first) {
strbuf_append(&b, ",");
}
first = 0;
strbuf_append(&b, "{\"label_key\":");
json_escape(&b, mounts[i].label_key);
strbuf_append(&b, ",\"path\":");
json_escape(&b, mounts[i].path);
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
free_bytes, total_bytes, is_current ? "true" : "false");
}
#else
const char *paths[] = {"/", current && strcmp(current, "/") ? current : NULL};
const char *labels[] = {"storageRoot", "storageCurrent"};
for(size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
struct statvfs vfs;
unsigned long block_size;
unsigned long long free_bytes;
unsigned long long total_bytes;
if(!paths[i] || statvfs(paths[i], &vfs)) {
continue;
}
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
free_bytes = vfs_bytes(vfs.f_bavail, block_size);
total_bytes = vfs_bytes(vfs.f_blocks, block_size);
if(!first) {
strbuf_append(&b, ",");
}
first = 0;
strbuf_append(&b, "{\"label_key\":");
json_escape(&b, labels[i]);
strbuf_append(&b, ",\"path\":");
json_escape(&b, paths[i]);
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
free_bytes, total_bytes, i ? "true" : "false");
}
#endif
free(current);
strbuf_append(&b, "]}");
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
}
+217
View File
@@ -0,0 +1,217 @@
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "filemgr_internal.h"
#include "path_util.h"
#define ETA_AVERAGE_WINDOW_SECONDS 30
pthread_mutex_t g_tasks_lock = PTHREAD_MUTEX_INITIALIZER;
file_task_t *g_tasks = NULL;
unsigned long g_next_task_id = 1;
const char *
task_op_name(task_op_t op) {
switch(op) {
case TASK_COPY: return "copy";
case TASK_MOVE: return "move";
case TASK_DELETE: return "delete";
case TASK_DOWNLOAD: return "download";
case TASK_UPLOAD: return "upload";
default: return "unknown";
}
}
const char *
task_state_name(task_state_t state) {
switch(state) {
case TASK_QUEUED: return "queued";
case TASK_RUNNING: return "running";
case TASK_DONE: return "done";
case TASK_FAILED: return "failed";
case TASK_CANCELED: return "canceled";
default: return "unknown";
}
}
int
task_is_active(const file_task_t *task) {
return task->state == TASK_QUEUED || task->state == TASK_RUNNING;
}
int
has_active_task_locked(void) {
file_task_t *task;
for(task = g_tasks; task; task = task->next) {
if(task_is_active(task)) {
return 1;
}
}
return 0;
}
int
has_active_task(void) {
int active;
pthread_mutex_lock(&g_tasks_lock);
active = has_active_task_locked();
pthread_mutex_unlock(&g_tasks_lock);
return active;
}
void
free_task(file_task_t *task) {
if(!task) {
return;
}
free_paths(task->srcs, task->src_count);
free(task);
}
void
remove_finished_tasks_locked(void) {
file_task_t **link = &g_tasks;
while(*link) {
file_task_t *task = *link;
if(task_is_active(task)) {
link = &task->next;
continue;
}
*link = task->next;
free_task(task);
}
}
int
task_cancel_requested(file_task_t *task) {
int cancel;
pthread_mutex_lock(&g_tasks_lock);
cancel = task->cancel_requested;
pthread_mutex_unlock(&g_tasks_lock);
if(cancel) {
errno = ECANCELED;
}
return cancel;
}
file_task_t *
find_task_locked(unsigned long id) {
file_task_t *task;
for(task = g_tasks; task; task = task->next) {
if(task->id == id) {
return task;
}
}
return NULL;
}
static long long
timespec_delta_ns(const struct timespec *end, const struct timespec *start) {
return (long long)(end->tv_sec - start->tv_sec) * 1000000000LL +
(long long)(end->tv_nsec - start->tv_nsec);
}
static void
task_update_eta_locked(file_task_t *task, const struct timespec *now_mono) {
task_eta_sample_t *sample;
task_eta_sample_t *base = NULL;
unsigned int i;
if(!task->total || !task->done || task->done >= task->total) {
task->eta = 0;
return;
}
sample = &task->eta_samples[task->eta_sample_next];
sample->done = task->done;
sample->time = *now_mono;
task->eta_sample_next = (task->eta_sample_next + 1) % ETA_SAMPLE_SLOTS;
if(task->eta_sample_count < ETA_SAMPLE_SLOTS) {
task->eta_sample_count++;
}
for(i = 0; i < task->eta_sample_count; i++) {
task_eta_sample_t *candidate = &task->eta_samples[i];
long long age_ns;
if(!candidate->time.tv_sec || candidate->done >= task->done) {
continue;
}
age_ns = timespec_delta_ns(now_mono, &candidate->time);
if(age_ns <= 0 || age_ns > (long long)ETA_AVERAGE_WINDOW_SECONDS * 1000000000LL) {
continue;
}
if(!base || age_ns > timespec_delta_ns(now_mono, &base->time)) {
base = candidate;
}
}
if(base) {
long long elapsed_ns = timespec_delta_ns(now_mono, &base->time);
unsigned long long delta = task->done - base->done;
unsigned long long remaining = task->total - task->done;
if(delta && elapsed_ns > 0) {
long double seconds = (long double)elapsed_ns / 1000000000.0L;
long double eta = ((long double)remaining / (long double)delta) * seconds;
task->eta = eta > 0 ? (unsigned long long)(eta + 0.999999L) : 0;
return;
}
}
task->eta = task->speed ? (task->total - task->done + task->speed - 1) / task->speed : 0;
}
void
task_update(file_task_t *task, task_state_t state, const char *current,
unsigned long long add_done, const char *error) {
struct timespec now_mono;
time_t now;
clock_gettime(CLOCK_MONOTONIC, &now_mono);
now = time(NULL);
pthread_mutex_lock(&g_tasks_lock);
task->state = state;
if(current) {
snprintf(task->current, sizeof(task->current), "%s", current);
}
if(add_done) {
if(!task->transfer_started_at) {
task->transfer_started_at = now;
}
task->done += add_done;
if(task->total && task->done > task->total) {
task->done = task->total;
}
if(task->speed_sample_time.tv_sec) {
long long elapsed_ns = timespec_delta_ns(&now_mono, &task->speed_sample_time);
if(elapsed_ns >= 250000000LL) {
unsigned long long delta = task->done - task->speed_sample_done;
task->speed = (unsigned long long)((delta * 1000000000ULL) /
(unsigned long long)elapsed_ns);
task->speed_sample_done = task->done;
task->speed_sample_time = now_mono;
}
} else {
task->speed_sample_done = task->done;
task->speed_sample_time = now_mono;
}
task_update_eta_locked(task, &now_mono);
}
if(error) {
snprintf(task->error, sizeof(task->error), "%s", error);
}
task->updated_at = now;
pthread_mutex_unlock(&g_tasks_lock);
}
+445
View File
@@ -0,0 +1,445 @@
#include "filemgr_internal.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "json_util.h"
#include "path_util.h"
#include "websrv.h"
#define TEXT_FILE_MAX_SIZE (1024 * 1024)
typedef enum text_newline {
TEXT_NEWLINE_LF,
TEXT_NEWLINE_CRLF,
TEXT_NEWLINE_CR,
} text_newline_t;
static enum MHD_Result
send_json_version(struct MHD_Connection *conn, unsigned long long version) {
char *data;
if(asprintf(&data, "{\"ok\":true,\"version\":\"%016llx\"}", version) < 0) {
return MHD_NO;
}
return send_buffer(conn, MHD_HTTP_OK, data, "application/json");
}
static int
text_extension_allowed(const char *path) {
static const char *extensions[] = {
".txt", ".json", ".xml", ".ini", ".cfg", ".conf", ".md",
".log", ".lua", ".js", ".css", ".html", ".htm", ".c", ".h",
".cpp", ".hpp", ".sh", ".csv", ".yaml", ".yml", ".shn"
};
const char *extension = strrchr(path, '.');
size_t i;
if(!extension) {
return 0;
}
for(i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) {
if(!strcasecmp(extension, extensions[i])) {
return 1;
}
}
return 0;
}
static int
valid_utf8(const unsigned char *data, size_t size) {
size_t i = 0;
while(i < size) {
unsigned char c = data[i++];
size_t trailing;
unsigned int codepoint;
if(!c) return 0;
if(c < 0x80) continue;
if(c >= 0xc2 && c <= 0xdf) {
trailing = 1;
codepoint = c & 0x1f;
} else if(c >= 0xe0 && c <= 0xef) {
trailing = 2;
codepoint = c & 0x0f;
} else if(c >= 0xf0 && c <= 0xf4) {
trailing = 3;
codepoint = c & 0x07;
} else {
return 0;
}
if(trailing > size - i) return 0;
while(trailing--) {
unsigned char next = data[i++];
if((next & 0xc0) != 0x80) return 0;
codepoint = (codepoint << 6) | (next & 0x3f);
}
if((codepoint >= 0xd800 && codepoint <= 0xdfff) || codepoint > 0x10ffff ||
(codepoint < 0x800 && c >= 0xe0) ||
(codepoint < 0x10000 && c >= 0xf0)) {
return 0;
}
}
return 1;
}
static unsigned long long
text_version(const unsigned char *data, size_t size) {
unsigned long long hash = 1469598103934665603ULL;
size_t i;
for(i = 0; i < size; i++) {
hash ^= data[i];
hash *= 1099511628211ULL;
}
return hash;
}
static text_newline_t
detect_text_newline(const unsigned char *data, size_t size) {
size_t crlf = 0;
size_t lf = 0;
size_t cr = 0;
size_t i;
for(i = 0; i < size; i++) {
if(data[i] == '\r') {
if(i + 1 < size && data[i + 1] == '\n') {
crlf++;
i++;
} else {
cr++;
}
} else if(data[i] == '\n') {
lf++;
}
}
if(crlf > lf && crlf >= cr) return TEXT_NEWLINE_CRLF;
if(cr > lf && cr > crlf) return TEXT_NEWLINE_CR;
return TEXT_NEWLINE_LF;
}
static int
format_text_for_save(const char *body, size_t body_size,
const unsigned char *current, size_t current_size,
char **output, size_t *output_size) {
static const unsigned char bom[] = {0xef, 0xbb, 0xbf};
int keep_bom = current_size >= sizeof(bom) &&
!memcmp(current, bom, sizeof(bom));
text_newline_t newline = detect_text_newline(
current + (keep_bom ? sizeof(bom) : 0),
current_size - (keep_bom ? sizeof(bom) : 0));
const unsigned char *input = (const unsigned char *)(body ? body : "");
size_t input_size = body_size;
size_t capacity = body_size * (newline == TEXT_NEWLINE_CRLF ? 2 : 1) +
sizeof(bom) + 1;
char *formatted;
size_t i;
size_t len = 0;
if(input_size >= sizeof(bom) && !memcmp(input, bom, sizeof(bom))) {
input += sizeof(bom);
input_size -= sizeof(bom);
}
if(!(formatted = malloc(capacity))) {
errno = ENOMEM;
return -1;
}
if(keep_bom) {
memcpy(formatted + len, bom, sizeof(bom));
len += sizeof(bom);
}
for(i = 0; i < input_size; i++) {
unsigned char c = input[i];
if(c != '\r' && c != '\n') {
formatted[len++] = (char)c;
continue;
}
if(c == '\r' && i + 1 < input_size && input[i + 1] == '\n') {
i++;
}
if(newline == TEXT_NEWLINE_CRLF) {
formatted[len++] = '\r';
formatted[len++] = '\n';
} else {
formatted[len++] = newline == TEXT_NEWLINE_CR ? '\r' : '\n';
}
}
if(len > TEXT_FILE_MAX_SIZE) {
free(formatted);
errno = EFBIG;
return -1;
}
formatted[len] = 0;
*output = formatted;
*output_size = len;
return 0;
}
static int
read_text_file(const char *path, char **data, size_t *size,
struct stat *st) {
FILE *file;
size_t read_size;
*data = NULL;
*size = 0;
if(lstat(path, st) || !S_ISREG(st->st_mode)) {
return -1;
}
if(st->st_size < 0 || (unsigned long long)st->st_size > TEXT_FILE_MAX_SIZE) {
errno = EFBIG;
return -1;
}
if(!(file = fopen(path, "rb"))) {
return -1;
}
if(!(*data = malloc((size_t)st->st_size + 1))) {
fclose(file);
errno = ENOMEM;
return -1;
}
read_size = fread(*data, 1, (size_t)st->st_size, file);
if(read_size != (size_t)st->st_size || ferror(file)) {
free(*data);
*data = NULL;
fclose(file);
return -1;
}
fclose(file);
(*data)[read_size] = 0;
*size = read_size;
return 0;
}
static enum MHD_Result
send_text_file(struct MHD_Connection *conn, char *data, size_t size,
unsigned long long version) {
struct MHD_Response *resp;
enum MHD_Result ret;
char version_text[24];
if(!(resp = MHD_create_response_from_buffer(size, data,
MHD_RESPMEM_MUST_FREE))) {
free(data);
return MHD_NO;
}
snprintf(version_text, sizeof(version_text), "%016llx", version);
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"text/plain; charset=utf-8");
MHD_add_response_header(resp, "X-Text-Version", version_text);
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
enum MHD_Result
api_text(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
char *data;
size_t size;
struct stat st;
unsigned long long version;
if(has_active_task()) {
free(path);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
if(!path || !text_extension_allowed(path)) {
free(path);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST,
"file type is not editable");
}
if(read_text_file(path, &data, &size, &st)) {
int error = errno;
free(path);
if(error == EFBIG) {
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
"text file is too large");
}
errno = error;
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
free(path);
if(!valid_utf8((const unsigned char *)data, size)) {
free(data);
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"file is not valid UTF-8");
}
version = text_version((const unsigned char *)data, size);
return send_text_file(conn, data, size, version);
}
enum MHD_Result
api_text_create(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
char *name = fs_path_value(query_value(conn, "name"));
char target[PATH_MAX];
int fd = -1;
int ret = -1;
int error = 0;
int created = 0;
if(has_active_task()) {
free(path); free(name);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
if(!path || !name || path_join(target, sizeof(target), path, name)) {
free(path); free(name);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
if(mode_access(path, W_OK | X_OK)) {
free(path); free(name);
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
"text file is not writable");
}
if((fd = open(target, O_WRONLY | O_CREAT | O_EXCL, 0777)) >= 0) {
created = 1;
ret = fchmod_0777(fd);
if(close(fd) && !ret) ret = -1;
fd = -1;
}
if(ret) {
error = errno;
if(fd >= 0) close(fd);
if(created) unlink(target);
}
free(path); free(name);
if(!ret) {
return send_json_version(conn,
text_version((const unsigned char *)"", 0));
}
errno = error;
return error == EEXIST ?
send_json_error(conn, MHD_HTTP_CONFLICT, "file already exists") :
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
}
static int
write_text_atomic(const char *path, const char *body, size_t body_size,
mode_t mode) {
struct timespec now;
char temp[PATH_MAX];
size_t written = 0;
int fd = -1;
int ret = -1;
int n;
clock_gettime(CLOCK_MONOTONIC, &now);
n = snprintf(temp, sizeof(temp), "%s.wfm-%ld-%ld.tmp", path,
(long)getpid(), now.tv_nsec);
if(n < 0 || (size_t)n >= sizeof(temp)) {
errno = ENAMETOOLONG;
return -1;
}
if((fd = open(temp, O_WRONLY | O_CREAT | O_EXCL, 0600)) < 0) {
return -1;
}
while(written < body_size) {
ssize_t count = write(fd, body + written, body_size - written);
if(count <= 0) {
goto done;
}
written += (size_t)count;
}
if(fchmod(fd, mode & 07777) && !ignore_chmod_error(errno)) {
goto done;
}
if(fsync(fd)) {
goto done;
}
if(close(fd)) {
fd = -1;
goto done;
}
fd = -1;
if(rename(temp, path)) {
goto done;
}
ret = 0;
done:
if(fd >= 0) close(fd);
if(ret) unlink(temp);
return ret;
}
enum MHD_Result
api_text_save(struct MHD_Connection *conn, const char *body,
size_t body_size) {
char *path = fs_path_value(query_value(conn, "path"));
char *expected = query_value(conn, "version");
char *current = NULL;
size_t current_size = 0;
struct stat st;
char version_text[24];
char parent[PATH_MAX];
char *formatted = NULL;
size_t formatted_size = 0;
int ret;
if(has_active_task()) {
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
if(!path || !expected) {
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
if(body_size > TEXT_FILE_MAX_SIZE) {
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
"text file is too large");
}
if(!valid_utf8((const unsigned char *)(body ? body : ""), body_size)) {
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"file is not valid UTF-8");
}
if(read_text_file(path, &current, &current_size, &st)) {
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
snprintf(version_text, sizeof(version_text), "%016llx",
text_version((const unsigned char *)current, current_size));
if(strcmp(expected, version_text)) {
free(current);
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_CONFLICT,
"file changed since it was opened");
}
if(path_dirname(path, parent, sizeof(parent)) ||
mode_access(path, W_OK) || mode_access(parent, W_OK | X_OK)) {
free(current);
free(path); free(expected);
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
"text file is not writable");
}
if(format_text_for_save(body, body_size, (const unsigned char *)current,
current_size, &formatted, &formatted_size)) {
int error = errno;
free(current);
free(path); free(expected);
errno = error;
return error == EFBIG ?
send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
"text file is too large") :
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
}
free(current);
ret = write_text_atomic(path, formatted, formatted_size, st.st_mode);
free(formatted);
free(path); free(expected);
return ret ? send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL)
: send_json_ok(conn);
}
+480
View File
@@ -0,0 +1,480 @@
#include "filemgr.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
typedef struct upload_context {
file_task_t *task;
int fd;
char temp[PATH_MAX];
char target[PATH_MAX];
unsigned long long expected;
unsigned long long written;
int failed;
int error;
int task_done;
} upload_context_t;
static void
finish_upload_task(file_task_t *task, task_state_t state, const char *current,
const char *error) {
if(!task) {
return;
}
pthread_mutex_lock(&g_tasks_lock);
if(task_is_active(task)) {
time_t completed_at = time(NULL);
task->state = state;
if(current) {
snprintf(task->current, sizeof(task->current), "%s", current);
}
if(state == TASK_DONE && task->total) {
task->done = task->total;
}
if(state == TASK_DONE) {
record_task_completion_locked(task, completed_at);
}
if(error) {
snprintf(task->error, sizeof(task->error), "%s", error);
}
task->updated_at = completed_at;
}
pthread_mutex_unlock(&g_tasks_lock);
}
static file_task_t *
upload_task_from_conn(struct MHD_Connection *conn) {
char *idstr = request_value(conn, "X-WFM-Task-ID", "task_id");
unsigned long id = idstr ? strtoul(idstr, NULL, 10) : 0;
file_task_t *task = NULL;
free(idstr);
if(!id) {
return NULL;
}
pthread_mutex_lock(&g_tasks_lock);
task = find_task_locked(id);
if(!task || task->op != TASK_UPLOAD || !task_is_active(task)) {
task = NULL;
}
pthread_mutex_unlock(&g_tasks_lock);
return task;
}
static int
check_upload_manifest_space(const char *base, const char *rels,
const char *sizes, unsigned long long fallback_total,
char *error, size_t error_size,
char *code, size_t code_size,
char *arg, size_t arg_size) {
char *rels_copy = NULL;
char *sizes_copy = NULL;
char *rel;
char *size_text;
char *rel_save;
char *size_save;
unsigned long long available;
int ret = -1;
if(!rels || !sizes) {
return check_target_space(base, fallback_total, error, error_size,
code, code_size, arg, arg_size);
}
if(target_available_space(base, &available)) {
snprintf(error, error_size, "cannot read target free space");
snprintf(code, code_size, "space_check_failed");
snprintf(arg, arg_size, "%s", base);
return -1;
}
if(!(rels_copy = strdup(rels)) || !(sizes_copy = strdup(sizes))) {
errno = ENOMEM;
goto done;
}
rel = strtok_r(rels_copy, "\n", &rel_save);
size_text = strtok_r(sizes_copy, "\n", &size_save);
while(rel || size_text) {
char target[PATH_MAX];
unsigned long long size;
if(!rel || !size_text || path_join_relative(target, sizeof(target), base, rel)) {
snprintf(error, error_size, "invalid path");
snprintf(code, code_size, "invalid_path");
snprintf(arg, arg_size, "%s", rel ? rel : "");
errno = EINVAL;
goto done;
}
size = strtoull(size_text, NULL, 10);
if(available < size) {
snprintf(error, error_size,
"not enough target space, required %llu bytes, available %llu bytes",
size, available);
snprintf(code, code_size, "no_space");
snprintf(arg, arg_size, "%llu,%llu", size, available);
errno = ENOSPC;
goto done;
}
available -= size;
rel = strtok_r(NULL, "\n", &rel_save);
size_text = strtok_r(NULL, "\n", &size_save);
}
ret = 0;
done:
free(rels_copy);
free(sizes_copy);
return ret;
}
enum MHD_Result
api_upload_prepare(struct MHD_Connection *conn, const char *body,
size_t body_size) {
char *path = fs_path_value(body_form_value(body, body_size, "path"));
char *src = body_form_value(body, body_size, "src");
char *total_text = body_form_value(body, body_size, "total");
char *count_text = body_form_value(body, body_size, "count");
char *rels = body_form_value(body, body_size, "rels");
char *sizes = body_form_value(body, body_size, "sizes");
char *overwrite = body_form_value(body, body_size, "overwrite");
file_task_t *task = calloc(1, sizeof(*task));
strbuf_t b = {0};
unsigned long long total = total_text ? strtoull(total_text, NULL, 10) : 0;
size_t count = count_text ? (size_t)strtoull(count_text, NULL, 10) : 0;
char error[128] = {0};
char code[64] = {0};
char arg[PATH_MAX + 96] = {0};
if(!task) {
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
}
if(!path || !src || !count) {
free_task(task);
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
pthread_mutex_lock(&g_tasks_lock);
remove_finished_tasks_locked();
if(has_active_task_locked()) {
pthread_mutex_unlock(&g_tasks_lock);
free_task(task);
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
pthread_mutex_unlock(&g_tasks_lock);
if(check_upload_manifest_space(path, rels, sizes, total, error, sizeof(error),
code, sizeof(code), arg, sizeof(arg))) {
free_task(task);
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
return send_json_error_detail(conn,
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
MHD_HTTP_INTERNAL_SERVER_ERROR,
error[0] ? error : NULL,
code[0] ? code : NULL,
arg[0] ? arg : NULL);
}
task->op = TASK_UPLOAD;
task->state = TASK_RUNNING;
task->src_count = count;
task->file_count = count;
task->total = total;
snprintf(task->src, sizeof(task->src), "%s", src);
snprintf(task->dst, sizeof(task->dst), "%s", path);
snprintf(task->current, sizeof(task->current), "%s", src);
task->created_at = time(NULL);
task->updated_at = task->created_at;
pthread_mutex_lock(&g_tasks_lock);
remove_finished_tasks_locked();
if(has_active_task_locked()) {
pthread_mutex_unlock(&g_tasks_lock);
free_task(task);
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
task->id = g_next_task_id++;
task->next = g_tasks;
g_tasks = task;
pthread_mutex_unlock(&g_tasks_lock);
free(path); free(src); free(total_text); free(count_text);
free(rels); free(sizes); free(overwrite);
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
}
enum MHD_Result
api_upload_finish(struct MHD_Connection *conn) {
file_task_t *task = upload_task_from_conn(conn);
if(!task) {
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "active task not found");
}
if(task_cancel_requested(task)) {
finish_upload_task(task, TASK_CANCELED,
task->current[0] ? task->current : task->src,
"canceled");
return send_json_error(conn, MHD_HTTP_CONFLICT, "canceled");
}
finish_upload_task(task, TASK_DONE,
task->current[0] ? task->current : task->src, NULL);
return send_json_ok(conn);
}
int
filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
upload_context_t *ctx;
char *base = fs_path_value(request_value(conn, "X-WFM-Path", "path"));
char *rel = request_value(conn, "X-WFM-Rel", "rel");
char *overwrite = request_value(conn, "X-WFM-Overwrite", "overwrite");
char *size_text = request_value(conn, "X-WFM-Size", "size");
file_task_t *task = upload_task_from_conn(conn);
char **checked_dirs = NULL;
size_t checked_dir_count = 0;
struct stat st;
char error[128] = {0};
char code[64] = {0};
char arg[PATH_MAX + 96] = {0};
int n;
*upload_ctx = NULL;
if(!(ctx = calloc(1, sizeof(*ctx)))) {
free(base); free(rel); free(overwrite); free(size_text);
errno = ENOMEM;
return -1;
}
ctx->fd = -1;
*upload_ctx = ctx;
if(size_text) {
ctx->expected = strtoull(size_text, NULL, 10);
}
ctx->task = task;
if(task && task_cancel_requested(task)) {
ctx->failed = 1;
ctx->error = ECANCELED;
goto fail;
}
if(!task && has_active_task()) {
ctx->failed = 1;
ctx->error = EBUSY;
goto fail;
}
if(!base || !rel || path_join_relative(ctx->target, sizeof(ctx->target),
base, rel)) {
ctx->failed = 1;
ctx->error = errno ? errno : EINVAL;
goto fail;
}
if(ensure_parent_dirs(base, rel)) {
ctx->failed = 1;
ctx->error = errno ? errno : EACCES;
goto fail;
}
if(!lstat(ctx->target, &st)) {
if(S_ISDIR(st.st_mode) || !overwrite || strcmp(overwrite, "1")) {
ctx->failed = 1;
ctx->error = S_ISDIR(st.st_mode) ? EISDIR : EEXIST;
goto fail;
}
} else if(errno != ENOENT) {
ctx->failed = 1;
ctx->error = errno;
goto fail;
}
if(check_target_writable(ctx->target, &checked_dirs, &checked_dir_count,
error, sizeof(error), code, sizeof(code),
arg, sizeof(arg))) {
ctx->failed = 1;
ctx->error = errno ? errno : EACCES;
goto fail;
}
if(check_target_space(ctx->target, ctx->expected, error, sizeof(error),
code, sizeof(code), arg, sizeof(arg))) {
ctx->failed = 1;
ctx->error = errno ? errno : ENOSPC;
goto fail;
}
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)) {
ctx->failed = 1;
ctx->error = ENAMETOOLONG;
goto fail;
}
ctx->fd = open(ctx->temp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if(ctx->fd < 0) {
ctx->failed = 1;
ctx->error = errno;
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;
}
if(ctx->fd >= 0) {
close(ctx->fd);
ctx->fd = -1;
}
if(ctx->temp[0]) {
unlink(ctx->temp);
}
errno = ctx->error ? ctx->error : EIO;
return -1;
}
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;
}
if(ctx->task && task_cancel_requested(ctx->task)) {
ctx->failed = 1;
ctx->error = ECANCELED;
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;
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;
}
enum MHD_Result
filemgr_upload_finish(struct MHD_Connection *conn, void *upload_ctx) {
upload_context_t *ctx = upload_ctx;
int ret = -1;
if(!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;
}
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);
}
if(ctx->expected && ctx->written != ctx->expected) {
ctx->error = EIO;
goto done;
}
if(fchmod_0777(ctx->fd)) {
ctx->error = errno;
goto done;
}
if(fsync(ctx->fd)) {
ctx->error = errno;
goto done;
}
if(close(ctx->fd)) {
ctx->fd = -1;
ctx->error = errno;
goto done;
}
ctx->fd = -1;
if(rename(ctx->temp, ctx->target)) {
ctx->error = errno;
goto done;
}
ret = 0;
done:
if(ctx->fd >= 0) {
close(ctx->fd);
ctx->fd = -1;
}
if(ret) {
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;
}
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_ok(conn);
}
void
filemgr_upload_free(void *upload_ctx) {
upload_context_t *ctx = upload_ctx;
if(!ctx) {
return;
}
if(ctx->fd >= 0) {
close(ctx->fd);
if(ctx->temp[0]) {
unlink(ctx->temp);
}
if(ctx->task && !ctx->task_done) {
int canceled = task_cancel_requested(ctx->task);
finish_upload_task(ctx->task, canceled ? TASK_CANCELED : TASK_FAILED,
ctx->target[0] ? ctx->target : ctx->task->current,
canceled ? "canceled" : "client disconnected");
ctx->task_done = 1;
}
}
free(ctx);
}
+116 -7
View File
@@ -13,6 +13,39 @@
#include "filemgr.h"
#include "websrv.h"
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
static volatile sig_atomic_t g_stop_requested;
static int g_listen_fd = -1;
typedef struct request_context {
char *body;
size_t size;
int too_large;
int upload_stream;
void *upload_ctx;
} request_context_t;
static enum MHD_Result
websrv_body_too_large(struct MHD_Connection *conn) {
static const char json[] =
"{\"ok\":false,\"error\":\"request body is too large\","
"\"error_code\":\"request_body_too_large\",\"error_arg\":\"\"}";
struct MHD_Response *resp =
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
MHD_RESPMEM_PERSISTENT);
enum MHD_Result ret;
if(!resp) {
return MHD_NO;
}
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"application/json");
ret = websrv_queue_response(conn, MHD_HTTP_CONTENT_TOO_LARGE, resp);
MHD_destroy_response(resp);
return ret;
}
enum MHD_Result
websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
struct MHD_Response *resp) {
@@ -21,15 +54,27 @@ websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
return MHD_queue_response(conn, status, resp);
}
void
websrv_stop(void) {
g_stop_requested = 1;
if(g_listen_fd >= 0) {
shutdown(g_listen_fd, SHUT_RDWR);
}
}
int
websrv_stop_requested(void) {
return g_stop_requested;
}
static enum MHD_Result
websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
const char *method, const char *version,
const char *upload_data, size_t *upload_data_size,
void **con_cls) {
request_context_t *ctx = *con_cls;
(void)cls;
(void)version;
(void)upload_data;
(void)upload_data_size;
if(strcmp(method, MHD_HTTP_METHOD_GET) &&
strcmp(method, MHD_HTTP_METHOD_POST) &&
@@ -37,13 +82,62 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
return MHD_NO;
}
if(!*con_cls) {
*con_cls = (void *)1;
if(!ctx) {
if(!(ctx = calloc(1, sizeof(*ctx)))) {
return MHD_NO;
}
ctx->upload_stream = !strcmp(url, "/api/upload-file") &&
!strcmp(method, MHD_HTTP_METHOD_POST);
*con_cls = ctx;
return MHD_YES;
}
if(*upload_data_size) {
size_t chunk_size = *upload_data_size;
if(ctx->upload_stream) {
if(!ctx->upload_ctx && filemgr_upload_begin(conn, &ctx->upload_ctx)) {
ctx->too_large = 1;
}
if(ctx->upload_ctx && filemgr_upload_data(ctx->upload_ctx, upload_data,
chunk_size)) {
ctx->too_large = 1;
}
*upload_data_size = 0;
return MHD_YES;
}
if(chunk_size > REQUEST_BODY_MAX - ctx->size) {
ctx->too_large = 1;
} else if(!ctx->too_large) {
char *body = realloc(ctx->body, ctx->size + chunk_size + 1);
if(!body) {
return MHD_NO;
}
ctx->body = body;
memcpy(ctx->body + ctx->size, upload_data, chunk_size);
ctx->size += chunk_size;
ctx->body[ctx->size] = 0;
}
*upload_data_size = 0;
return MHD_YES;
}
if(ctx->too_large) {
return ctx->upload_stream ?
filemgr_upload_finish(conn, ctx->upload_ctx) :
websrv_body_too_large(conn);
}
if(ctx->upload_stream) {
if(!ctx->upload_ctx && filemgr_upload_begin(conn, &ctx->upload_ctx)) {
return filemgr_upload_finish(conn, ctx->upload_ctx);
}
return filemgr_upload_finish(conn, ctx->upload_ctx);
}
if(!strncmp(url, "/api/", 5)) {
return filemgr_api_request(conn, url);
return filemgr_api_request(conn, url, method, ctx->body, ctx->size);
}
if(!strcmp(url, "/fs")) {
return filemgr_fs_request(conn);
@@ -51,6 +145,9 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
if(!strcmp(url, "/") || !url[0]) {
return asset_request(conn, "/index.html");
}
if(!strcmp(url, "/favicon.ico")) {
return asset_request(conn, "/icon0.png");
}
return asset_request(conn, url);
}
@@ -60,6 +157,15 @@ websrv_on_completed(void *cls, struct MHD_Connection *connection,
(void)cls;
(void)connection;
(void)toe;
request_context_t *ctx = *con_cls;
if(ctx) {
if(ctx->upload_ctx) {
filemgr_upload_free(ctx->upload_ctx);
}
free(ctx->body);
free(ctx);
}
*con_cls = NULL;
}
@@ -100,6 +206,8 @@ websrv_listen(unsigned short port) {
close(srvfd);
return -1;
}
g_stop_requested = 0;
g_listen_fd = srvfd;
if(!(httpd = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_ITC |
MHD_USE_NO_LISTEN_SOCKET | MHD_USE_DEBUG |
@@ -112,10 +220,10 @@ websrv_listen(unsigned short port) {
return -1;
}
while(1) {
while(!g_stop_requested) {
addr_len = sizeof(client_addr);
if((connfd = accept(srvfd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept");
if(!g_stop_requested) perror("accept");
break;
}
if(MHD_add_connection(httpd, connfd, (struct sockaddr *)&client_addr,
@@ -127,5 +235,6 @@ websrv_listen(unsigned short port) {
}
MHD_stop_daemon(httpd);
g_listen_fd = -1;
return close(srvfd);
}
+2
View File
@@ -23,3 +23,5 @@ enum MHD_Result websrv_queue_response(struct MHD_Connection *conn,
struct MHD_Response *resp);
int websrv_listen(unsigned short port);
void websrv_stop(void);
int websrv_stop_requested(void);