mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 08:00:23 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72341d6710 | ||
|
|
6780599abd | ||
|
|
9e5f323813 | ||
|
|
5c66a65171 | ||
|
|
34de7d2483 | ||
|
|
bd4d1c45a6 | ||
|
|
28230105c4 | ||
|
|
c7fc108e48 | ||
|
|
48d669b848 | ||
|
|
dafbd8dcbf | ||
|
|
939257eda0 | ||
|
|
3efaf24385 | ||
|
|
5302e971f9 | ||
|
|
582d154cdf | ||
|
|
a61ac54e1f | ||
|
|
6a110f95ae | ||
|
|
b463c3f86b | ||
|
|
f9be83061d | ||
|
|
ae23c8fdaf | ||
|
|
2dc1ff2c42 | ||
|
|
9d56cd5039 | ||
|
|
5dba332ad4 | ||
|
|
1a24160b8b | ||
|
|
3696468611 | ||
|
|
777bb4abd4 | ||
|
|
76f8fe21d2 | ||
|
|
5a91067c2e | ||
|
|
ba0704d48c |
No files matched your search
@@ -3,3 +3,5 @@ gen/
|
||||
web-file-mgr-linux
|
||||
*.o
|
||||
*.d
|
||||
tools/
|
||||
test/
|
||||
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v0.4
|
||||
VERSION_TAG := v1.7
|
||||
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 src/pkg_info.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)\"
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
|
||||
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
|
||||
|
||||
.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
|
||||
$(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)
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
|
||||
@@ -21,17 +21,25 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
|
||||
## 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.
|
||||
- Change read/write/execute permissions from the permissions column, using checkboxes or a validated four-digit octal mode.
|
||||
- Copy, move, delete, rename, create files, and create folders.
|
||||
- Edit UTF-8 text files up to 1 MiB using the built-in plain text editor.
|
||||
- Multi-select operations.
|
||||
- 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.
|
||||
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`.
|
||||
- Preview image files ending with `.png,`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
|
||||
- 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/Preview PKG files ends with `.pkg`.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -66,6 +74,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`.
|
||||
@@ -83,25 +100,36 @@ 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+.
|
||||
- **[seregonwar/zftpd](https://github.com/seregonwar/zftpd):** PS5 TCP socket buffer tuning and high-throughput transfer behavior reference. License: MIT.
|
||||
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behavior. License: GPLv3.
|
||||
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. It is licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
|
||||
- **[ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk):** Payload building foundation. License: GPLv3+.
|
||||
- **[etaHEN](https://github.com/etaHEN/etaHEN):** ShellUI URI navigation used to return to the PS5 home screen before exit. License: GPLv3.
|
||||
- **[ezremote](https://github.com/cy33hc/ps5-ezremote-client):** Preview PKG info. License: GPLv2.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 268 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
+110
-9
@@ -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,31 +41,49 @@
|
||||
<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>
|
||||
@@ -75,6 +115,7 @@
|
||||
|
||||
<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>
|
||||
@@ -82,13 +123,73 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="pkgInfoOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel pkg-info-panel" role="dialog" aria-modal="true" aria-labelledby="pkgInfoTitle">
|
||||
<h2 id="pkgInfoTitle" class="pkg-info-title" data-i18n="pkgInfoTitle"></h2>
|
||||
<div class="pkg-info-content">
|
||||
<div class="pkg-info-image-stage">
|
||||
<img id="pkgInfoImage" class="pkg-info-image" src="/icon-pkg.png" alt="">
|
||||
</div>
|
||||
<div id="pkgInfoFields" class="pkg-info-fields"></div>
|
||||
</div>
|
||||
<div class="text-editor-actions">
|
||||
<button id="pkgInfoCloseBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="pkgInfoInstallBtn" class="primary" data-i18n="install"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="permissionOverlay" class="permission-overlay" hidden>
|
||||
<section class="permission-panel" role="dialog" aria-modal="true" aria-labelledby="permissionTitle">
|
||||
<h2 id="permissionTitle" class="permission-title" data-i18n="permissionsTitle"></h2>
|
||||
<div id="permissionPath" class="permission-path"></div>
|
||||
<div class="permission-grid">
|
||||
<div class="permission-row permission-head" aria-hidden="true">
|
||||
<span class="permission-scope"></span>
|
||||
<span>R</span><span>W</span><span>X</span>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOwner"></span>
|
||||
<label><input id="permissionOwnerRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOwnerWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOwnerExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionGroup"></span>
|
||||
<label><input id="permissionGroupRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionGroupWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionGroupExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOther"></span>
|
||||
<label><input id="permissionOtherRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOtherWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOtherExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="permission-octal">
|
||||
<span data-i18n="permissionOctal"></span>
|
||||
<input id="permissionMode" type="text" inputmode="numeric" pattern="0[0-7]{3}"
|
||||
minlength="4" maxlength="4" autocomplete="off" spellcheck="false">
|
||||
</label>
|
||||
<label id="permissionRecursiveOption" class="permission-recursive" hidden>
|
||||
<input id="permissionRecursive" type="checkbox" checked>
|
||||
<span data-i18n="permissionRecursive"></span>
|
||||
</label>
|
||||
<div class="permission-actions">
|
||||
<button id="permissionCancelBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="permissionApplyBtn" class="primary" data-i18n="apply"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true"></div>
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
|
||||
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
+59
-4
@@ -3,17 +3,34 @@ window.WFM_LANG = {
|
||||
copy: "Copy",
|
||||
move: "Move",
|
||||
delete: "Delete",
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
uploadFolder: "Upload Folder",
|
||||
copying: "Copying",
|
||||
moving: "Moving",
|
||||
deleting: "Deleting",
|
||||
changingPermissions: "Changing permissions",
|
||||
rename: "Rename",
|
||||
paste: "Paste",
|
||||
cancel: "Cancel",
|
||||
close: "Close",
|
||||
save: "Save",
|
||||
apply: "Apply",
|
||||
exit: "Exit",
|
||||
exitConfirm: "Exit and stop the file manager process?",
|
||||
exiting: "Exiting...",
|
||||
refresh: "Refresh",
|
||||
mkdir: "New Folder",
|
||||
newText: "New Text",
|
||||
install: "Install",
|
||||
installPackage: "Install package",
|
||||
pkgInfoTitle: "Package Information",
|
||||
pkgInfoLoading: "Reading package information...",
|
||||
pkgInstalling: "Starting package installation: {name}...",
|
||||
pkgInstallStarted: "Package installation started: {name}",
|
||||
file: "File",
|
||||
textFile: "Text",
|
||||
image: "Image",
|
||||
dir: "Folder",
|
||||
parent: "Parent",
|
||||
name: "Name",
|
||||
@@ -21,6 +38,19 @@ window.WFM_LANG = {
|
||||
size: "Size",
|
||||
mtime: "Modified",
|
||||
mode: "Mode",
|
||||
permissionsTitle: "Change permissions",
|
||||
permissionOwner: "Owner",
|
||||
permissionGroup: "Group",
|
||||
permissionOther: "Other",
|
||||
permissionOctal: "Octal mode",
|
||||
permissionRecursive: "Apply to all contents inside this folder",
|
||||
permissionObjectCount: "and {count} objects",
|
||||
permissionChangeTitle: "Change permissions for {name}",
|
||||
permissionInvalid: "Enter exactly four octal digits from 0 to 7, starting with 0. The original value has been restored.",
|
||||
unsavedPermissionConfirm: "The permissions have unsaved changes. Close without applying them?\n\nCancel keeps the permissions dialog open.",
|
||||
permissionChanging: "Changing permissions...",
|
||||
permissionChanged: "Permissions changed: {name} → {mode}",
|
||||
permissionChangeFailed: "Failed to change permissions: {error}",
|
||||
empty: "Folder is empty",
|
||||
ready: "Ready",
|
||||
freeSpace: "Free space",
|
||||
@@ -34,14 +64,21 @@ window.WFM_LANG = {
|
||||
checking: "Checking",
|
||||
pleaseWait: "Please wait",
|
||||
speedLabel: "Speed",
|
||||
itemsPerSecond: "{count} objects/s",
|
||||
progressLabel: "Progress",
|
||||
permissionProgress: "{done} / {total} objects",
|
||||
etaLabel: "ETA",
|
||||
elapsedLabel: "Elapsed",
|
||||
durationHours: "{hours}h {minutes}m",
|
||||
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",
|
||||
@@ -50,25 +87,33 @@ 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\n\nTotal time: {duration}\nTransferred: {size}",
|
||||
transferCompleteFiles: "{label} completed\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
|
||||
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. Save before closing?\n\nCancel discards the changes and closes.",
|
||||
unsavedSaveConfirm: "The text has unsaved changes. Close without saving?\n\nCancel keeps the editor open.",
|
||||
loadingText: "Loading text...",
|
||||
savingText: "Saving...",
|
||||
textSaved: "Text saved",
|
||||
@@ -84,11 +129,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: "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",
|
||||
@@ -96,6 +144,12 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "Source and destination are the same",
|
||||
err_destination_inside_source: "Cannot copy or move a folder inside itself",
|
||||
err_invalid_path: "Invalid path",
|
||||
err_invalid_mode: "Invalid permission mode",
|
||||
err_chmod_failed: "Cannot change permissions: {path}",
|
||||
err_pkg_type_invalid: "Only .pkg files can be installed",
|
||||
err_pkg_info_failed: "Could not read package information: {path}",
|
||||
err_pkg_install_failed: "Package installation failed ({arg})",
|
||||
err_pkg_install_unsupported: "Package installation is only available on PS5",
|
||||
err_file_not_found: "File not found",
|
||||
err_file_already_exists: "A file with the same name already exists",
|
||||
err_invalid_method: "Invalid request method",
|
||||
@@ -104,6 +158,7 @@ window.WFM_LANG = {
|
||||
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",
|
||||
|
||||
+59
-4
@@ -3,17 +3,34 @@ window.WFM_LANG = {
|
||||
copy: "复制",
|
||||
move: "移动",
|
||||
delete: "删除",
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
uploadFolder: "上传文件夹",
|
||||
copying: "复制",
|
||||
moving: "移动",
|
||||
deleting: "删除",
|
||||
changingPermissions: "修改权限",
|
||||
rename: "重命名",
|
||||
paste: "粘贴",
|
||||
cancel: "取消",
|
||||
close: "关闭",
|
||||
save: "保存",
|
||||
apply: "应用",
|
||||
exit: "退出",
|
||||
exitConfirm: "是否退出并关闭文件管理器进程?",
|
||||
exiting: "正在退出...",
|
||||
refresh: "刷新",
|
||||
mkdir: "新建目录",
|
||||
newText: "新建文本",
|
||||
install: "安装",
|
||||
installPackage: "安装 PKG",
|
||||
pkgInfoTitle: "PKG 信息",
|
||||
pkgInfoLoading: "正在读取 PKG 信息...",
|
||||
pkgInstalling: "正在提交安装:{name}...",
|
||||
pkgInstallStarted: "已开始安装:{name}",
|
||||
file: "文件",
|
||||
textFile: "文本",
|
||||
image: "图片",
|
||||
dir: "目录",
|
||||
parent: "上级目录",
|
||||
name: "名称",
|
||||
@@ -21,6 +38,19 @@ window.WFM_LANG = {
|
||||
size: "大小",
|
||||
mtime: "修改时间",
|
||||
mode: "权限",
|
||||
permissionsTitle: "修改权限",
|
||||
permissionOwner: "拥有者",
|
||||
permissionGroup: "组",
|
||||
permissionOther: "其他",
|
||||
permissionOctal: "八进制表示",
|
||||
permissionRecursive: "应用于文件夹内的所有内容",
|
||||
permissionObjectCount: "等 {count} 个对象",
|
||||
permissionChangeTitle: "修改 {name} 的权限",
|
||||
permissionInvalid: "请输入以 0 开头、由 0 到 7 组成的四位八进制权限。已恢复为原权限值。",
|
||||
unsavedPermissionConfirm: "权限有未保存的修改,是否不保存并关闭?\n\n取消将留在权限弹窗中。",
|
||||
permissionChanging: "正在修改权限...",
|
||||
permissionChanged: "权限已修改:{name} → {mode}",
|
||||
permissionChangeFailed: "修改权限失败:{error}",
|
||||
empty: "目录为空",
|
||||
ready: "就绪",
|
||||
freeSpace: "可用空间",
|
||||
@@ -34,14 +64,21 @@ window.WFM_LANG = {
|
||||
checking: "检查中",
|
||||
pleaseWait: "请稍候",
|
||||
speedLabel: "速度",
|
||||
itemsPerSecond: "{count} 个对象/秒",
|
||||
progressLabel: "进度",
|
||||
permissionProgress: "{done} / {total} 个对象",
|
||||
etaLabel: "剩余",
|
||||
elapsedLabel: "耗时",
|
||||
durationHours: "{hours}小时 {minutes}分",
|
||||
durationMinutes: "{minutes}分 {seconds}秒",
|
||||
durationSeconds: "{seconds}秒",
|
||||
preparingTask: "正在准备任务",
|
||||
canceling: "正在取消...",
|
||||
selectedItems: "{name} 等 {count} 项",
|
||||
countItems: "{count} 项",
|
||||
totalItems: "共 {count} 项",
|
||||
readDir: "读取目录中...",
|
||||
processing: "处理中...",
|
||||
actionBusy: "{label}...",
|
||||
taskCreated: "{label}任务已创建",
|
||||
actionDone: "{label}完成",
|
||||
@@ -50,25 +87,33 @@ 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}完成\n\n总耗时:{duration}\n传输大小:{size}",
|
||||
transferCompleteFiles: "{label}完成\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
|
||||
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取消将放弃修改并关闭。",
|
||||
unsavedSaveConfirm: "文本有未保存的修改,是否不保存并关闭?\n\n取消将留在编辑器中。",
|
||||
loadingText: "正在读取文本...",
|
||||
savingText: "正在保存...",
|
||||
textSaved: "文本已保存",
|
||||
@@ -84,11 +129,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_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: "有任务正在执行",
|
||||
@@ -96,6 +144,12 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "源和目标相同",
|
||||
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
|
||||
err_invalid_path: "路径无效",
|
||||
err_invalid_mode: "权限格式无效",
|
||||
err_chmod_failed: "无法修改权限:{path}",
|
||||
err_pkg_type_invalid: "只能安装 .pkg 文件",
|
||||
err_pkg_info_failed: "无法读取 PKG 信息:{path}",
|
||||
err_pkg_install_failed: "PKG 安装失败({arg})",
|
||||
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
|
||||
err_file_not_found: "文件不存在",
|
||||
err_file_already_exists: "同名文件已存在",
|
||||
err_invalid_method: "请求方式无效",
|
||||
@@ -104,6 +158,7 @@ window.WFM_LANG = {
|
||||
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: "没有源路径",
|
||||
|
||||
+845
-37
File diff suppressed because it is too large.
Load diff
+1377
-131
File diff suppressed because it is too large.
Load diff
+18
-29
@@ -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;
|
||||
}
|
||||
|
||||
+761
@@ -0,0 +1,761 @@
|
||||
#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
|
||||
#define DOWNLOAD_BUFFER_SIZE (2 * 1024 * 1024)
|
||||
|
||||
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;
|
||||
int fd;
|
||||
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;
|
||||
unsigned long long progress = 0;
|
||||
char progress_current[PATH_MAX] = {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;
|
||||
progress += (unsigned long long)n;
|
||||
if(s->current_file[0]) {
|
||||
snprintf(progress_current, sizeof(progress_current), "%s",
|
||||
s->current_file);
|
||||
}
|
||||
if(!s->file_remaining) {
|
||||
close(s->fd);
|
||||
s->fd = -1;
|
||||
s->current_file[0] = 0;
|
||||
s->pad_remaining = s->file_padding;
|
||||
s->file_padding = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(s->task && progress) {
|
||||
task_update(s->task, TASK_RUNNING,
|
||||
progress_current[0] ? progress_current : NULL,
|
||||
progress, NULL);
|
||||
}
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
|
||||
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;
|
||||
ssize_t len;
|
||||
|
||||
if(task_cancel_requested(s->task)) {
|
||||
s->error = ECANCELED;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
len = pread(s->fd, buf, max, (off_t)pos);
|
||||
if(len < 0) {
|
||||
s->error = errno ? errno : EIO;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!len) {
|
||||
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 len;
|
||||
}
|
||||
|
||||
static void
|
||||
download_file_close(void *cls) {
|
||||
download_file_stream_t *s = cls;
|
||||
int canceled;
|
||||
|
||||
if(!s) {
|
||||
return;
|
||||
}
|
||||
if(s->fd >= 0) {
|
||||
close(s->fd);
|
||||
}
|
||||
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->fd = -1;
|
||||
file_stream->size = (unsigned long long)st.st_size;
|
||||
file_stream->done = file_stream->size == 0;
|
||||
file_stream->fd = open(paths[0], O_RDONLY);
|
||||
if(file_stream->fd < 0) {
|
||||
free(file_stream);
|
||||
task_update(task, TASK_FAILED, task->src, 0, strerror(errno));
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
resp = MHD_create_response_from_callback((uint64_t)st.st_size,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
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,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+1365
-1358
File diff suppressed because it is too large.
Load diff
@@ -6,3 +6,9 @@ enum MHD_Result filemgr_api_request(struct MHD_Connection *conn,
|
||||
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);
|
||||
@@ -0,0 +1,128 @@
|
||||
#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_CHMOD,
|
||||
TASK_DOWNLOAD,
|
||||
TASK_UPLOAD,
|
||||
TASK_PKG_INSTALL,
|
||||
} 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;
|
||||
size_t upload_completed;
|
||||
unsigned int chmod_mode;
|
||||
int recursive;
|
||||
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;
|
||||
int reported; /* Terminal state has been included in /api/tasks. */
|
||||
unsigned int active_streams;
|
||||
time_t created_at;
|
||||
time_t transfer_started_at;
|
||||
time_t updated_at;
|
||||
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_mode(const char *path, unsigned int mode);
|
||||
int chmod_path_0777(const char *path);
|
||||
int fchmod_0777(int fd);
|
||||
int ignore_chmod_error(int err);
|
||||
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);
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
#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_mode(const char *path, unsigned int mode) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
return 0;
|
||||
}
|
||||
if(chmod(path, (mode_t)(mode & 0777))) {
|
||||
/* A filesystem that does not implement Unix modes is compatible with the
|
||||
paste behavior. Real permission and read-only errors must reach the UI. */
|
||||
if(errno != ENOTSUP && errno != EINVAL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
chmod_path_0777(const char *path) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
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;
|
||||
}
|
||||
@@ -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, "\"");
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -150,6 +150,9 @@ main(int argc, char **argv) {
|
||||
#endif
|
||||
|
||||
websrv_listen(port);
|
||||
if(websrv_stop_requested()) {
|
||||
break;
|
||||
}
|
||||
sleep(3);
|
||||
}
|
||||
|
||||
|
||||
+401
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
#include "pkg_info.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define PKG_CNT_MAGIC 0x7f434e54u
|
||||
#define PKG_FIH_MAGIC 0x7f464948u
|
||||
#define PKG_LIH_MAGIC 0x7f4c4948u
|
||||
#define PKG_HEADER_SIZE 0xa0
|
||||
#define PKG_ENTRY_SIZE 0x20
|
||||
#define PKG_ENTRY_PARAM_SFO 0x1000u
|
||||
#define PKG_ENTRY_ICON0_PNG 0x1200u
|
||||
#define PKG_ENTRY_ICON0_LOCALIZED_LAST 0x121fu
|
||||
#define PKG_ENTRY_PARAM_JSON 0x2000u
|
||||
#define PKG_ENTRY_FLAG_ENCRYPTED 0x80000000u
|
||||
#define PKG_ENTRY_MAX 65536u
|
||||
#define PKG_PARAM_MAX (4u * 1024u * 1024u)
|
||||
#define PKG_ICON_MAX (32u * 1024u * 1024u)
|
||||
#define SFO_MAGIC 0x46535000u
|
||||
#define SFO_ENTRY_SIZE 0x10
|
||||
#define SFO_ENTRY_MAX 256u
|
||||
#define JSON_TOKEN_MAX 4096u
|
||||
|
||||
typedef enum pkg_param_type {
|
||||
PKG_PARAM_SFO,
|
||||
PKG_PARAM_JSON,
|
||||
} pkg_param_type_t;
|
||||
|
||||
typedef struct pkg_source {
|
||||
int fd;
|
||||
uint64_t size;
|
||||
uint32_t content_type;
|
||||
uint32_t content_flags;
|
||||
char content_id[37];
|
||||
uint64_t param_offset;
|
||||
uint32_t param_size;
|
||||
pkg_param_type_t param_type;
|
||||
uint64_t icon_offset;
|
||||
uint32_t icon_size;
|
||||
} pkg_source_t;
|
||||
|
||||
typedef enum json_token_type {
|
||||
JSON_OBJECT,
|
||||
JSON_ARRAY,
|
||||
JSON_STRING,
|
||||
JSON_PRIMITIVE,
|
||||
} json_token_type_t;
|
||||
|
||||
typedef struct json_token {
|
||||
json_token_type_t type;
|
||||
size_t start;
|
||||
size_t end;
|
||||
int parent;
|
||||
} json_token_t;
|
||||
|
||||
static uint16_t
|
||||
read_le16(const unsigned char *p) {
|
||||
return (uint16_t)p[0] | (uint16_t)p[1] << 8;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_le32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] | (uint32_t)p[1] << 8 |
|
||||
(uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
read_le64(const unsigned char *p) {
|
||||
return (uint64_t)read_le32(p) | (uint64_t)read_le32(p + 4) << 32;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_be32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 |
|
||||
(uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static int
|
||||
range_valid(uint64_t offset, uint64_t size, uint64_t file_size) {
|
||||
return offset <= file_size && size <= file_size - offset;
|
||||
}
|
||||
|
||||
static int
|
||||
read_at(int fd, void *buffer, size_t size, uint64_t offset) {
|
||||
unsigned char *p = buffer;
|
||||
size_t done = 0;
|
||||
|
||||
while(done < size) {
|
||||
ssize_t n = pread(fd, p + done, size - done, (off_t)(offset + done));
|
||||
if(n <= 0) {
|
||||
if(n < 0 && errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
done += (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
png_signature_valid(const unsigned char *data, size_t size) {
|
||||
static const unsigned char signature[] = "\x89PNG\r\n\x1a\n";
|
||||
|
||||
return size >= sizeof(signature) - 1 &&
|
||||
!memcmp(data, signature, sizeof(signature) - 1);
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_icon_is_png(const pkg_source_t *pkg, uint64_t offset, uint32_t size) {
|
||||
unsigned char signature[8];
|
||||
|
||||
return size >= sizeof(signature) &&
|
||||
!read_at(pkg->fd, signature, sizeof(signature), offset) &&
|
||||
png_signature_valid(signature, sizeof(signature));
|
||||
}
|
||||
|
||||
static void
|
||||
pkg_source_close(pkg_source_t *pkg) {
|
||||
if(pkg->fd >= 0) close(pkg->fd);
|
||||
pkg->fd = -1;
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_source_open(const char *path, pkg_source_t *pkg) {
|
||||
unsigned char header[PKG_HEADER_SIZE];
|
||||
unsigned char *table = NULL;
|
||||
struct stat st;
|
||||
uint64_t container_offset = 0;
|
||||
uint64_t container_size;
|
||||
uint32_t magic;
|
||||
uint32_t entry_count;
|
||||
uint32_t table_offset;
|
||||
size_t table_size;
|
||||
int ret = -1;
|
||||
|
||||
memset(pkg, 0, sizeof(*pkg));
|
||||
pkg->fd = -1;
|
||||
if((pkg->fd = open(path, O_RDONLY)) < 0 || fstat(pkg->fd, &st) ||
|
||||
!S_ISREG(st.st_mode) || st.st_size < PKG_HEADER_SIZE ||
|
||||
read_at(pkg->fd, header, sizeof(header), 0)) goto done;
|
||||
|
||||
pkg->size = (uint64_t)st.st_size;
|
||||
magic = read_be32(header);
|
||||
if(magic == PKG_FIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x58);
|
||||
} else if(magic == PKG_LIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x30);
|
||||
} else if(magic != PKG_CNT_MAGIC) {
|
||||
goto done;
|
||||
}
|
||||
if(container_offset) {
|
||||
if(!range_valid(container_offset, sizeof(header), pkg->size) ||
|
||||
read_at(pkg->fd, header, sizeof(header), container_offset) ||
|
||||
read_be32(header) != PKG_CNT_MAGIC) goto done;
|
||||
}
|
||||
container_size = pkg->size - container_offset;
|
||||
|
||||
memcpy(pkg->content_id, header + 0x40, 36);
|
||||
pkg->content_id[36] = 0;
|
||||
pkg->content_type = read_be32(header + 0x74);
|
||||
pkg->content_flags = read_be32(header + 0x78);
|
||||
entry_count = read_be32(header + 0x10);
|
||||
table_offset = read_be32(header + 0x18);
|
||||
if(!entry_count || entry_count > PKG_ENTRY_MAX) goto done;
|
||||
table_size = (size_t)entry_count * PKG_ENTRY_SIZE;
|
||||
if(!range_valid(table_offset, table_size, container_size) ||
|
||||
!(table = malloc(table_size)) ||
|
||||
read_at(pkg->fd, table, table_size, container_offset + table_offset)) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
for(uint32_t i = 0; i < entry_count; i++) {
|
||||
const unsigned char *entry = table + (size_t)i * PKG_ENTRY_SIZE;
|
||||
uint32_t id = read_be32(entry);
|
||||
uint32_t flags1 = read_be32(entry + 0x08);
|
||||
uint32_t offset = read_be32(entry + 0x10);
|
||||
uint32_t size = read_be32(entry + 0x14);
|
||||
int encrypted = (flags1 & PKG_ENTRY_FLAG_ENCRYPTED) != 0;
|
||||
|
||||
if(!range_valid(offset, size, container_size)) goto done;
|
||||
if(encrypted || !size) continue;
|
||||
if(id == PKG_ENTRY_PARAM_SFO && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_SFO;
|
||||
} else if(id == PKG_ENTRY_PARAM_JSON && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_JSON;
|
||||
} else if(id >= PKG_ENTRY_ICON0_PNG &&
|
||||
id <= PKG_ENTRY_ICON0_LOCALIZED_LAST &&
|
||||
size <= PKG_ICON_MAX &&
|
||||
(id == PKG_ENTRY_ICON0_PNG || !pkg->icon_size) &&
|
||||
pkg_icon_is_png(pkg, container_offset + offset, size)) {
|
||||
/* Prefer icon0.png; otherwise keep the first valid localized icon. */
|
||||
pkg->icon_offset = container_offset + offset;
|
||||
pkg->icon_size = size;
|
||||
}
|
||||
}
|
||||
if(!pkg->param_size) goto done;
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
free(table);
|
||||
if(ret) {
|
||||
errno = EINVAL;
|
||||
pkg_source_close(pkg);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
append_sfo_fields(strbuf_t *json, const unsigned char *sfo, size_t size) {
|
||||
uint32_t key_offset;
|
||||
uint32_t value_offset;
|
||||
uint32_t count;
|
||||
uint64_t index_end;
|
||||
int first = 1;
|
||||
|
||||
if(size < 20 || read_le32(sfo) != SFO_MAGIC) return -1;
|
||||
key_offset = read_le32(sfo + 8);
|
||||
value_offset = read_le32(sfo + 12);
|
||||
count = read_le32(sfo + 16);
|
||||
index_end = 20 + (uint64_t)count * SFO_ENTRY_SIZE;
|
||||
if(count > SFO_ENTRY_MAX || index_end > size || key_offset < index_end ||
|
||||
value_offset < key_offset || value_offset > size) return -1;
|
||||
|
||||
strbuf_append(json, "[");
|
||||
for(uint32_t i = 0; i < count; i++) {
|
||||
const unsigned char *entry = sfo + 20 + (size_t)i * SFO_ENTRY_SIZE;
|
||||
uint16_t name_offset = read_le16(entry);
|
||||
uint16_t format = read_le16(entry + 2);
|
||||
uint32_t value_size = read_le32(entry + 4);
|
||||
uint32_t value_max = read_le32(entry + 8);
|
||||
uint32_t data_offset = read_le32(entry + 12);
|
||||
uint64_t key_pos = (uint64_t)key_offset + name_offset;
|
||||
uint64_t value_pos = (uint64_t)value_offset + data_offset;
|
||||
const unsigned char *key_end;
|
||||
char number[32];
|
||||
char *value = NULL;
|
||||
|
||||
if(key_pos >= value_offset || value_pos > size ||
|
||||
(value_max && value_size > value_max)) continue;
|
||||
key_end = memchr(sfo + key_pos, 0, value_offset - (size_t)key_pos);
|
||||
if(!key_end) continue;
|
||||
if(format == 0x0204) {
|
||||
size_t length;
|
||||
if(!value_size || !range_valid(value_pos, value_size, size)) continue;
|
||||
length = strnlen((const char *)sfo + value_pos, value_size);
|
||||
if(!(value = malloc(length + 1))) continue;
|
||||
memcpy(value, sfo + value_pos, length);
|
||||
value[length] = 0;
|
||||
} else if(format == 0x0404 && range_valid(value_pos, 4, size)) {
|
||||
snprintf(number, sizeof(number), "%u", read_le32(sfo + value_pos));
|
||||
value = strdup(number);
|
||||
}
|
||||
if(!value) continue;
|
||||
if(!first) strbuf_append(json, ",");
|
||||
first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, (const char *)sfo + key_pos);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
json_escape(json, value);
|
||||
strbuf_append(json, "}");
|
||||
free(value);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_add_token(json_token_t *tokens, size_t *count, json_token_type_t type,
|
||||
size_t start, int parent) {
|
||||
if(*count >= JSON_TOKEN_MAX) return -1;
|
||||
tokens[*count] = (json_token_t){
|
||||
.type = type, .start = start, .end = 0, .parent = parent
|
||||
};
|
||||
return (int)(*count)++;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_json_tokens(const unsigned char *data, size_t size, json_token_t *tokens,
|
||||
size_t *token_count) {
|
||||
int parent = -1;
|
||||
size_t count = 0;
|
||||
|
||||
for(size_t i = 0; i < size; i++) {
|
||||
unsigned char c = data[i];
|
||||
if(c == '{' || c == '[') {
|
||||
int index = json_add_token(tokens, &count,
|
||||
c == '{' ? JSON_OBJECT : JSON_ARRAY,
|
||||
i, parent);
|
||||
if(index < 0) return -1;
|
||||
parent = index;
|
||||
} else if(c == '}' || c == ']') {
|
||||
json_token_type_t type = c == '}' ? JSON_OBJECT : JSON_ARRAY;
|
||||
if(parent < 0 || tokens[parent].type != type) return -1;
|
||||
tokens[parent].end = i + 1;
|
||||
parent = tokens[parent].parent;
|
||||
} else if(c == '"') {
|
||||
int index = json_add_token(tokens, &count, JSON_STRING, i + 1, parent);
|
||||
if(index < 0) return -1;
|
||||
for(i++; i < size && data[i] != '"'; i++) {
|
||||
if(data[i] < 0x20) return -1;
|
||||
if(data[i] == '\\') {
|
||||
if(++i >= size || !strchr("\"\\/bfnrtu", data[i])) return -1;
|
||||
if(data[i] == 'u') {
|
||||
for(unsigned int n = 0; n < 4; n++) {
|
||||
if(++i >= size || !((data[i] >= '0' && data[i] <= '9') ||
|
||||
(data[i] >= 'a' && data[i] <= 'f') ||
|
||||
(data[i] >= 'A' && data[i] <= 'F'))) return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(i >= size) return -1;
|
||||
tokens[index].end = i;
|
||||
} else if(c == ':' || c == ',' || c == ' ' || c == '\t' ||
|
||||
c == '\r' || c == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
int index = json_add_token(tokens, &count, JSON_PRIMITIVE, i, parent);
|
||||
if(index < 0) return -1;
|
||||
while(i < size && data[i] != ',' && data[i] != ']' && data[i] != '}' &&
|
||||
data[i] != ' ' && data[i] != '\t' && data[i] != '\r' &&
|
||||
data[i] != '\n') i++;
|
||||
if(tokens[index].start == i) return -1;
|
||||
tokens[index].end = i;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if(parent >= 0 || !count || tokens[0].type != JSON_OBJECT ||
|
||||
!tokens[0].end) return -1;
|
||||
*token_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_token_equals(const unsigned char *data, const json_token_t *token,
|
||||
const char *text) {
|
||||
size_t length = strlen(text);
|
||||
return token->type == JSON_STRING && token->end - token->start == length &&
|
||||
!memcmp(data + token->start, text, length);
|
||||
}
|
||||
|
||||
static int
|
||||
json_next_child(const json_token_t *tokens, size_t count, int parent,
|
||||
size_t start) {
|
||||
for(size_t i = start; i < count; i++) {
|
||||
if(tokens[i].parent == parent) return (int)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const char *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(json_token_equals(data, &tokens[item], key)) return value;
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value_token(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const json_token_t *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
size_t key_size = key->end - key->start;
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
tokens[item].end - tokens[item].start == key_size &&
|
||||
!memcmp(data + tokens[item].start, data + key->start, key_size)) {
|
||||
return value;
|
||||
}
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_token_string(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *token) {
|
||||
strbuf_append(json, "\"");
|
||||
strbuf_printf(json, "%.*s", (int)(token->end - token->start),
|
||||
data + token->start);
|
||||
strbuf_append(json, "\"");
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_field(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *key, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
append_json_token_string(json, data, key);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static void
|
||||
append_named_json_field(strbuf_t *json, const char *name,
|
||||
const unsigned char *data, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, name);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static int
|
||||
append_param_json_fields(strbuf_t *json, const unsigned char *data,
|
||||
size_t size) {
|
||||
json_token_t *tokens = calloc(JSON_TOKEN_MAX, sizeof(*tokens));
|
||||
size_t count = 0;
|
||||
int first = 1;
|
||||
int localized;
|
||||
int title = -1;
|
||||
|
||||
if(!tokens || parse_json_tokens(data, size, tokens, &count)) {
|
||||
free(tokens);
|
||||
return -1;
|
||||
}
|
||||
strbuf_append(json, "[");
|
||||
|
||||
localized = json_object_value(data, tokens, count, 0, "localizedParameters");
|
||||
if(localized >= 0 && tokens[localized].type == JSON_OBJECT) {
|
||||
int language = json_object_value(data, tokens, count, localized,
|
||||
"defaultLanguage");
|
||||
int language_data = language >= 0 && tokens[language].type == JSON_STRING ?
|
||||
json_object_value_token(data, tokens, count, localized,
|
||||
&tokens[language]) : -1;
|
||||
if(language_data >= 0 && tokens[language_data].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, language_data,
|
||||
"titleName");
|
||||
}
|
||||
if(title < 0) {
|
||||
int english = json_object_value(data, tokens, count, localized, "en-US");
|
||||
if(english >= 0 && tokens[english].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, english, "titleName");
|
||||
}
|
||||
}
|
||||
if(title < 0) {
|
||||
int item = json_next_child(tokens, count, localized,
|
||||
(size_t)localized + 1);
|
||||
while(item >= 0 && title < 0) {
|
||||
int value = json_next_child(tokens, count, localized,
|
||||
(size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[value].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, value, "titleName");
|
||||
}
|
||||
item = json_next_child(tokens, count, localized, (size_t)value + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(title >= 0 && tokens[title].type == JSON_STRING) {
|
||||
append_named_json_field(json, "titleName", data, &tokens[title], &first);
|
||||
}
|
||||
|
||||
for(int item = json_next_child(tokens, count, 0, 1); item >= 0;) {
|
||||
int value = json_next_child(tokens, count, 0, (size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
(tokens[value].type == JSON_STRING ||
|
||||
tokens[value].type == JSON_PRIMITIVE)) {
|
||||
append_json_field(json, data, &tokens[item], &tokens[value], &first);
|
||||
}
|
||||
item = json_next_child(tokens, count, 0, (size_t)value + 1);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
free(tokens);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
pkg_error(struct MHD_Connection *conn, const char *path) {
|
||||
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"could not read package information",
|
||||
"pkg_info_failed", path);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_info(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *param;
|
||||
strbuf_t json = {0};
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
if(!(param = malloc(pkg.param_size)) ||
|
||||
read_at(pkg.fd, param, pkg.param_size, pkg.param_offset)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
strbuf_printf(&json,
|
||||
"{\"ok\":true,\"size\":%llu,\"content_id\":",
|
||||
(unsigned long long)pkg.size);
|
||||
json_escape(&json, pkg.content_id);
|
||||
strbuf_printf(&json,
|
||||
",\"platform\":\"%s\",\"content_type\":%u,"
|
||||
"\"content_flags\":%u,\"has_icon\":%s,\"fields\":",
|
||||
pkg.param_type == PKG_PARAM_JSON ? "PS5" : "PS4",
|
||||
pkg.content_type, pkg.content_flags,
|
||||
pkg.icon_size ? "true" : "false");
|
||||
if((pkg.param_type == PKG_PARAM_JSON &&
|
||||
append_param_json_fields(&json, param, pkg.param_size)) ||
|
||||
(pkg.param_type == PKG_PARAM_SFO &&
|
||||
append_sfo_fields(&json, param, pkg.param_size))) {
|
||||
enum MHD_Result ret;
|
||||
free(json.data);
|
||||
ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
strbuf_append(&json, "}");
|
||||
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_icon(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *icon;
|
||||
struct MHD_Response *response;
|
||||
enum MHD_Result ret;
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg) || !pkg.icon_size ||
|
||||
!(icon = malloc(pkg.icon_size)) ||
|
||||
read_at(pkg.fd, icon, pkg.icon_size, pkg.icon_offset)) {
|
||||
if(path && pkg.fd >= 0) pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "package icon not found");
|
||||
}
|
||||
if(!png_signature_valid(icon, pkg.icon_size)) {
|
||||
free(icon);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND,
|
||||
"package icon not found");
|
||||
}
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
|
||||
response = MHD_create_response_from_buffer(pkg.icon_size, icon,
|
||||
MHD_RESPMEM_MUST_FREE);
|
||||
if(!response) {
|
||||
free(icon);
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, "image/png");
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, response);
|
||||
MHD_destroy_response(response);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
enum MHD_Result api_pkg_info(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_pkg_icon(struct MHD_Connection *conn);
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "pkg_installer.h"
|
||||
|
||||
#ifndef __linux__
|
||||
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.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;
|
||||
uint32_t slot;
|
||||
uint32_t is_playgo_enabled;
|
||||
} pkg_metadata_t;
|
||||
|
||||
_Static_assert(sizeof(pkg_metadata_t) == 0x38,
|
||||
"sceAppInstUtil metadata ABI mismatch");
|
||||
|
||||
typedef struct pkg_info {
|
||||
char content_id[48];
|
||||
int type;
|
||||
int platform;
|
||||
} pkg_info_t;
|
||||
|
||||
typedef struct playgo_info {
|
||||
char languages[30][8];
|
||||
char scenario_ids[64][3];
|
||||
char content_ids[64][48];
|
||||
long unknown[810];
|
||||
} playgo_info_t;
|
||||
|
||||
_Static_assert(sizeof(playgo_info_t) == 0x2700,
|
||||
"sceAppInstUtil PlayGoInfo ABI mismatch");
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
|
||||
playgo_info_t *);
|
||||
|
||||
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 = "",
|
||||
.slot = 0,
|
||||
.is_playgo_enabled = 0
|
||||
};
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
|
||||
|
||||
int pkg_installer_initialize(void);
|
||||
int pkg_installer_install(const char *path);
|
||||
+128
@@ -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");
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
#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_CHMOD: return "chmod";
|
||||
case TASK_DOWNLOAD: return "download";
|
||||
case TASK_UPLOAD: return "upload";
|
||||
case TASK_PKG_INSTALL: return "pkg_install";
|
||||
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->op != TASK_PKG_INSTALL && 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) || task->active_streams ||
|
||||
(task->op == TASK_PKG_INSTALL && !task->reported)) {
|
||||
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
@@ -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, ¤t, ¤t_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);
|
||||
}
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
#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"
|
||||
|
||||
#define UPLOAD_BUFFER_SIZE (1024 * 1024)
|
||||
|
||||
typedef struct upload_context {
|
||||
file_task_t *task;
|
||||
int fd;
|
||||
char *buffer;
|
||||
size_t buffered;
|
||||
char temp[PATH_MAX];
|
||||
char target[PATH_MAX];
|
||||
unsigned long long expected;
|
||||
unsigned long long written;
|
||||
int failed;
|
||||
int error;
|
||||
int task_done;
|
||||
const char *stage;
|
||||
char error_message[256];
|
||||
} upload_context_t;
|
||||
|
||||
static const char *
|
||||
upload_error_message(upload_context_t *ctx) {
|
||||
if(!ctx->error_message[0]) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s failed%s%s: %s",
|
||||
ctx->stage ? ctx->stage : "upload",
|
||||
ctx->target[0] ? " for " : "",
|
||||
ctx->target[0] ? ctx->target : "",
|
||||
strerror(ctx->error ? ctx->error : EIO));
|
||||
}
|
||||
return ctx->error_message;
|
||||
}
|
||||
|
||||
static int
|
||||
upload_flush(upload_context_t *ctx) {
|
||||
size_t written = 0;
|
||||
|
||||
while(written < ctx->buffered) {
|
||||
ssize_t n;
|
||||
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
n = write(ctx->fd, ctx->buffer + written, ctx->buffered - written);
|
||||
if(n <= 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EIO;
|
||||
return -1;
|
||||
}
|
||||
written += (size_t)n;
|
||||
ctx->written += (unsigned long long)n;
|
||||
}
|
||||
if(ctx->task && written) {
|
||||
task_update(ctx->task, TASK_RUNNING, ctx->target,
|
||||
(unsigned long long)written, NULL);
|
||||
}
|
||||
ctx->buffered = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_task(file_task_t *task, task_state_t state, const char *current,
|
||||
const char *error) {
|
||||
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);
|
||||
}
|
||||
if(state == TASK_FAILED) {
|
||||
snprintf(task->error_code, sizeof(task->error_code), "upload_failed");
|
||||
snprintf(task->error_arg, sizeof(task->error_arg), "%s",
|
||||
current ? current : task->src);
|
||||
}
|
||||
task->updated_at = completed_at;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_context_error(upload_context_t *ctx) {
|
||||
if(!ctx->task || ctx->task_done) {
|
||||
return;
|
||||
}
|
||||
upload_error_message(ctx);
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message);
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
|
||||
static file_task_t *
|
||||
upload_task_from_conn(struct MHD_Connection *conn) {
|
||||
char *idstr = request_value(conn, "X-WFM-Task-ID", "task_id");
|
||||
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;
|
||||
ctx->stage = "preparing upload";
|
||||
*upload_ctx = ctx;
|
||||
|
||||
if(size_text) {
|
||||
ctx->expected = strtoull(size_text, NULL, 10);
|
||||
}
|
||||
ctx->task = task;
|
||||
if(task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->active_streams++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
if(task && task_cancel_requested(task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
goto fail;
|
||||
}
|
||||
if(!task && has_active_task()) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = EBUSY;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "validating target path";
|
||||
if(!base || !rel || path_join_relative(ctx->target, sizeof(ctx->target),
|
||||
base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EINVAL;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating target folders";
|
||||
if(ensure_parent_dirs(base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target path";
|
||||
if(!lstat(ctx->target, &st)) {
|
||||
if(S_ISDIR(st.st_mode) || !overwrite || strcmp(overwrite, "1")) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = S_ISDIR(st.st_mode) ? EISDIR : EEXIST;
|
||||
goto fail;
|
||||
}
|
||||
} else if(errno != ENOENT) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target permissions";
|
||||
if(check_target_writable(ctx->target, &checked_dirs, &checked_dir_count,
|
||||
error, sizeof(error), code, sizeof(code),
|
||||
arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "target is not writable");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target space";
|
||||
if(check_target_space(ctx->target, ctx->expected, error, sizeof(error),
|
||||
code, sizeof(code), arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "not enough target space");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : ENOSPC;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating temporary path";
|
||||
n = snprintf(ctx->temp, sizeof(ctx->temp), "%s.wfm-upload-%ld-%lld.tmp",
|
||||
ctx->target, (long)getpid(), (long long)time(NULL));
|
||||
if(n < 0 || (size_t)n >= sizeof(ctx->temp)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ENAMETOOLONG;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "opening temporary file";
|
||||
ctx->fd = open(ctx->temp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if(ctx->fd < 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "allocating upload buffer";
|
||||
if(!(ctx->buffer = malloc(UPLOAD_BUFFER_SIZE))) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
fail:
|
||||
free_paths(checked_dirs, checked_dir_count);
|
||||
free(base); free(rel); free(overwrite); free(size_text);
|
||||
if(ctx->failed) {
|
||||
finish_upload_context_error(ctx);
|
||||
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;
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
filemgr_upload_data(void *upload_ctx, const char *data, size_t size) {
|
||||
upload_context_t *ctx = upload_ctx;
|
||||
|
||||
if(!ctx || ctx->failed) {
|
||||
return -1;
|
||||
}
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
while(size) {
|
||||
size_t space = UPLOAD_BUFFER_SIZE - ctx->buffered;
|
||||
size_t take = size < space ? size : space;
|
||||
|
||||
memcpy(ctx->buffer + ctx->buffered, data, take);
|
||||
ctx->buffered += take;
|
||||
data += take;
|
||||
size -= take;
|
||||
if(ctx->buffered == UPLOAD_BUFFER_SIZE && upload_flush(ctx)) {
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error_detail(conn,
|
||||
errno == EEXIST ? MHD_HTTP_CONFLICT :
|
||||
errno == EBUSY ? MHD_HTTP_CONFLICT :
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
if(ctx->buffered && upload_flush(ctx)) {
|
||||
ctx->error = ctx->error ? ctx->error : EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "verifying uploaded size";
|
||||
if(ctx->expected && ctx->written != ctx->expected) {
|
||||
ctx->error = EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "setting file permissions";
|
||||
if(fchmod_0777(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "syncing file data";
|
||||
if(fsync(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "closing temporary file";
|
||||
if(close(ctx->fd)) {
|
||||
ctx->fd = -1;
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->fd = -1;
|
||||
ctx->stage = "moving temporary file into place";
|
||||
if(rename(ctx->temp, ctx->target)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
ctx->task->upload_completed++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
if(ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
ctx->fd = -1;
|
||||
}
|
||||
if(ret) {
|
||||
upload_error_message(ctx);
|
||||
if(ctx->temp[0]) {
|
||||
unlink(ctx->temp);
|
||||
}
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error_detail(conn,
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
return send_json_ok(conn);
|
||||
}
|
||||
|
||||
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->buffer);
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
if(ctx->task->active_streams) {
|
||||
ctx->task->active_streams--;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
+82
-6
@@ -6,6 +6,7 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <microhttpd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -13,19 +14,38 @@
|
||||
#include "filemgr.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define REQUEST_BODY_MAX (1024 * 1024)
|
||||
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_LIMIT (8 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_INCREMENT (2 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_RCVBUF_SIZE (4 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_SNDBUF_SIZE (4 * 1024 * 1024)
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested;
|
||||
static int g_listen_fd = -1;
|
||||
|
||||
static void
|
||||
websrv_tune_connection_socket(int fd) {
|
||||
const int sndbuf = HTTP_SOCKET_SNDBUF_SIZE;
|
||||
const int nodelay = 1;
|
||||
|
||||
/* OrbisOS HTTP sockets need an explicit send buffer to fill a GbE link. */
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
|
||||
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
}
|
||||
|
||||
typedef struct request_context {
|
||||
char *body;
|
||||
size_t size;
|
||||
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\":\"text_file_too_large\",\"error_arg\":\"\"}";
|
||||
"\"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);
|
||||
@@ -49,6 +69,19 @@ 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,
|
||||
@@ -68,6 +101,8 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
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;
|
||||
}
|
||||
@@ -75,6 +110,18 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
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) {
|
||||
@@ -92,7 +139,16 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
}
|
||||
|
||||
if(ctx->too_large) {
|
||||
return websrv_body_too_large(conn);
|
||||
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)) {
|
||||
@@ -104,6 +160,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);
|
||||
}
|
||||
|
||||
@@ -116,6 +175,9 @@ websrv_on_completed(void *cls, struct MHD_Connection *connection,
|
||||
request_context_t *ctx = *con_cls;
|
||||
|
||||
if(ctx) {
|
||||
if(ctx->upload_ctx) {
|
||||
filemgr_upload_free(ctx->upload_ctx);
|
||||
}
|
||||
free(ctx->body);
|
||||
free(ctx);
|
||||
}
|
||||
@@ -143,6 +205,12 @@ websrv_listen(unsigned short port) {
|
||||
close(srvfd);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
const int rcvbuf = HTTP_SOCKET_RCVBUF_SIZE;
|
||||
|
||||
/* Set before listen so accepted PS5 sockets inherit the larger window. */
|
||||
(void)setsockopt(srvfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
|
||||
}
|
||||
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
@@ -159,11 +227,17 @@ 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 |
|
||||
MHD_USE_INTERNAL_POLLING_THREAD,
|
||||
MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_TURBO,
|
||||
0, NULL, NULL, &websrv_on_request, NULL,
|
||||
MHD_OPTION_CONNECTION_MEMORY_LIMIT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_LIMIT,
|
||||
MHD_OPTION_CONNECTION_MEMORY_INCREMENT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_INCREMENT,
|
||||
MHD_OPTION_NOTIFY_COMPLETED,
|
||||
&websrv_on_completed, NULL, MHD_OPTION_END))) {
|
||||
perror("MHD_start_daemon");
|
||||
@@ -171,12 +245,13 @@ 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;
|
||||
}
|
||||
websrv_tune_connection_socket(connfd);
|
||||
if(MHD_add_connection(httpd, connfd, (struct sockaddr *)&client_addr,
|
||||
addr_len) != MHD_YES) {
|
||||
perror("MHD_add_connection");
|
||||
@@ -186,5 +261,6 @@ websrv_listen(unsigned short port) {
|
||||
}
|
||||
|
||||
MHD_stop_daemon(httpd);
|
||||
g_listen_fd = -1;
|
||||
return close(srvfd);
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user