Коммит 6b2b7968 создал по автору OMP Education's avatar OMP Education Зафиксировано автором Elizaveta Vaysberg
Просмотр файлов

[Project] Implement Web App Server Application

владелец 5a997c2e
<div>
<div align="center">
<a href="https://static-web-server.net" title="static-web-server website">
<img src="https://static-web-server.net/assets/sws.svg" height="100" width="100"
/></a>
</div>
<h1 align="center">Static Web Server</h1>
<h4 align="center">
A cross-platform, high-performance and asynchronous web server for static files-serving ⚡
</h4>
<div align="center">
<a href="https://github.com/static-web-server/static-web-server/actions/workflows/devel.yml" title="devel ci"><img src="https://github.com/static-web-server/static-web-server/actions/workflows/devel.yml/badge.svg?branch=master"></a>
<a href="https://hub.docker.com/r/joseluisq/static-web-server/" title="Docker Image Version (tag latest semver)"><img src="https://img.shields.io/docker/v/joseluisq/static-web-server/2"></a>
<a href="https://hub.docker.com/r/joseluisq/static-web-server/tags" title="Docker Image Size (tag)"><img src="https://img.shields.io/docker/image-size/joseluisq/static-web-server/2"></a>
<a href="https://hub.docker.com/r/joseluisq/static-web-server/" title="Docker Image"><img src="https://img.shields.io/docker/pulls/joseluisq/static-web-server.svg"></a>
<a href="https://crates.io/crates/static-web-server" title="static-web-server crate"><img src="https://img.shields.io/crates/v/static-web-server.svg"></a>
<a href="https://docs.rs/static-web-server" title="static-web-server crate docs"><img src="https://img.shields.io/docsrs/static-web-server/latest?label=docs.rs"></a>
<a href="https://github.com/static-web-server/static-web-server/blob/master/LICENSE-APACHE" title="static-web-server license"><img src="https://img.shields.io/crates/l/static-web-server"></a>
<a href="https://discord.gg/VWvtZeWAA7" title="Static Web Server Community on Discord">
<img src="https://img.shields.io/discord/1086203405225164842?logo=discord&label=discord">
</a>
</div>
</div>
## Overview
**Static Web Server** (or **`SWS`** abbreviated) is a tiny and fast production-ready web server suitable to serve static web files or assets.
It is focused on **lightness and easy-to-use** principles while keeping [high performance and safety](https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html) powered by [The Rust Programming Language](https://rust-lang.org).
Written on top of [Hyper](https://github.com/hyperium/hyper) and [Tokio](https://github.com/tokio-rs/tokio) runtime, it provides [concurrent and asynchronous networking abilities](https://rust-lang.github.io/async-book/01_getting_started/02_why_async.html) and the latest HTTP/1 - HTTP/2 implementations.
Cross-platform and available for `Linux`, `macOS`, `Windows`, `FreeBSD`, `NetBSD`, `Android`, `Docker` and `Wasm` (via [Wasmer](https://wasmer.io/wasmer/static-web-server)).
![static-web-server running](https://github.com/static-web-server/static-web-server/assets/1700322/102bef12-1f30-4054-a1bc-30c650d4ffa7)
## Features
- Built with [Rust](https://rust-lang.org), which focuses on [safety, speed and concurrency](https://kornel.ski/rust-c-speed).
- Memory-safe and significantly reduced CPU and RAM overhead.
- Blazing fast static files-serving and asynchronous powered by the latest [Hyper](https://github.com/hyperium/hyper/), [Tokio](https://github.com/tokio-rs/tokio) and a set of [awesome crates](https://github.com/static-web-server/static-web-server/blob/master/Cargo.toml).
- Single __4MB__ (uncompressed) and fully static binary with no dependencies ([Musl libc](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/musl-support-for-fully-static-binaries.html)). Suitable for running on [any Linux distro](https://en.wikipedia.org/wiki/Linux_distribution) or [Docker container](https://hub.docker.com/r/joseluisq/static-web-server/tags).
- Optional GZip, Deflate, Brotli or Zstandard (zstd) compression for text-based web files only.
- Compression on-demand via [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header.
- [Partial Content Delivery](https://en.wikipedia.org/wiki/Byte_serving) support for byte-serving of large files.
- Optional [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) headers for assets.
- [Termination signal](https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html) handling with [graceful shutdown](https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-terminating-with-grace) ability and grace period.
- [HTTP/2](https://tools.ietf.org/html/rfc7540) and TLS support.
- [Security headers](https://web.dev/security-headers/) for HTTP/2 by default.
- [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) and [OPTIONS](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.7) responses.
- Lightweight and configurable logging via [tracing](https://github.com/tokio-rs/tracing) crate.
- Customizable number of blocking and worker threads.
- Optional directory listing with sorting and JSON output format support.
- [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with preflight requests support.
- Basic HTTP Authentication.
- Customizable HTTP response headers for specific file requests via glob patterns.
- Fallback pages for 404 errors, useful for Single-page applications.
- Run the server as a [Windows Service](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783643(v=ws.10)).
- Configurable using CLI arguments, environment variables or a TOML file.
- Default and custom error pages.
- Built-in HTTP to HTTPS redirect.
- GET/HEAD Health check endpoint.
- Support for serving pre-compressed (Gzip/Brotli/Zstd) files directly from disk.
- Custom URL rewrites and redirects via glob patterns with replacements.
- Virtual hosting support.
- Multiple index files.
- Maintenance Mode functionality.
- Available as a library crate with opt-in features.
- First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch), latest [Alpine Linux](https://hub.docker.com/_/alpine) and [Debian](https://hub.docker.com/_/alpine) Docker images.
- Ability to accept a socket listener as a file descriptor for sandboxing and on-demand applications (e.g. [systemd](http://0pointer.de/blog/projects/socket-activation.html)).
- Cross-platform. Pre-compiled binaries for Linux, macOS, Windows, FreeBSD, NetBSD, Android (`x86/x86_64`, `ARM/ARM64`) and WebAssembly (via [Wasmer](https://wasmer.io/wasmer/static-web-server)).
## Documentation
Please refer to [The Documentation Website](https://static-web-server.net/) for more details about the API, usage and examples.
## Releases
- [Docker Images](https://hub.docker.com/r/joseluisq/static-web-server/)
- [Release Binaries](https://github.com/static-web-server/static-web-server/releases)
- [Platforms/Architectures Supported](https://static-web-server.net/platforms-architectures/)
## Benchmarks
<img title="SWS - Benchmarks April 2023" src="https://raw.githubusercontent.com/static-web-server/benchmarks/master/data/2023-04/benchmark-2023-04.png" width="860">
For more details see [benchmarks repository](https://github.com/static-web-server/benchmarks)
## Notes
- If you're looking for `v1` please go to [1.x](https://github.com/static-web-server/static-web-server/tree/1.x) branch.
- If you want to migrate from `v1` to `v2` please view [Migrating from `v1` to `v2`](https://static-web-server.net/migration/) release.
## Contributions
Unless you explicitly state otherwise, any contribution you intentionally submitted for inclusion in current work, as defined in the Apache-2.0 license, shall be dual licensed as described below, without any additional terms or conditions.
Feel free to submit a [pull request](https://github.com/static-web-server/static-web-server/pulls) or file an [issue](https://github.com/static-web-server/static-web-server/issues).
## Community
[SWS Community on Discord](https://discord.gg/VWvtZeWAA7)
## License
This work is primarily distributed under the terms of both the [MIT license](LICENSE-MIT) and the [Apache License (Version 2.0)](LICENSE-APACHE).
© 2019-present [Jose Quintana](https://joseluisq.net)
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
import QtQuick 2.6
import Sailfish.Silica 1.0
import ru.auroraos.WebView 1.0
import Sailfish.Pickers 1.0
Item {
id: root
// original request for accept()
property var downloadRequests: ({})
// latest active download items for pause/resume/cancel()
property var activeDownloads: ({})
property int pendingDownloadId: -1
property string suggestedDownloadName: ""
function handleDownloadRequested(downloadItem, suggestedName) {
root.activeDownloads[downloadItem.id] = downloadItem;
root.downloadRequests[downloadItem.id] = downloadItem;
root.suggestedDownloadName = suggestedName;
root.pendingDownloadId = downloadItem.id;
downloadConfirmationDialog.open();
}
function handleDownloadUpdated(downloadItem) {
root.activeDownloads[downloadItem.id] = downloadItem;
if (root.pendingDownloadId !== -1 && root.pendingDownloadId === downloadItem.id) {
downloadItem.pause();
}
}
function handleDownloadCompleted(downloadItem) {
delete root.activeDownloads[downloadItem.id];
delete root.downloadRequests[downloadItem.id];
if (root.pendingDownloadId === downloadItem.id) {
root.pendingDownloadId = -1;
root.suggestedDownloadName = "";
}
}
function handleDownloadInterrupted(downloadItem) {
delete root.activeDownloads[downloadItem.id];
delete root.downloadRequests[downloadItem.id];
if (root.pendingDownloadId === downloadItem.id) {
root.pendingDownloadId = -1;
root.suggestedDownloadName = "";
}
}
Dialog {
id: downloadConfirmationDialog
property int activeDownloadId: -1
property string path: ""
states: [
State {
name: "confirmation"
PropertyChanges {
target: title
title: "Confirm download"
}
PropertyChanges {
target: progressLabel
visible: false
}
PropertyChanges {
target: speedLabel
visible: false
}
PropertyChanges {
target: progressBar
visible: false
}
PropertyChanges {
target: messageLabel
visible: false
}
PropertyChanges {
target: header
cancelText: "Cancel"
}
PropertyChanges {
target: header
acceptText: "Accept"
}
},
State {
name: "downloading"
PropertyChanges {
target: title
title: "Downloading..."
}
PropertyChanges {
target: progressLabel
visible: true
}
PropertyChanges {
target: speedLabel
visible: true
}
PropertyChanges {
target: progressBar
visible: true
}
PropertyChanges {
target: messageLabel
visible: true
}
PropertyChanges {
target: header
cancelText: "Close"
}
PropertyChanges {
target: header
acceptText: ""
}
},
State {
name: "complete"
PropertyChanges {
target: title
title: "Download complete"
}
PropertyChanges {
target: progressLabel
visible: true
}
PropertyChanges {
target: speedLabel
visible: false
}
PropertyChanges {
target: progressBar
visible: true
}
PropertyChanges {
target: messageLabel
visible: true
}
PropertyChanges {
target: header
cancelText: "Close"
}
PropertyChanges {
target: header
acceptText: ""
}
},
State {
name: "interrupted"
PropertyChanges {
target: title
title: "Download interrupted"
}
PropertyChanges {
target: progressLabel
visible: false
}
PropertyChanges {
target: speedLabel
visible: false
}
PropertyChanges {
target: progressBar
visible: false
}
PropertyChanges {
target: messageLabel
visible: true
}
PropertyChanges {
target: header
cancelText: "Close"
}
PropertyChanges {
target: header
acceptText: ""
}
}
]
state: "confirmation"
canAccept: state == "confirmation" && root.pendingDownloadId !== -1 && root.pendingDownloadId == activeDownloadId
Component.onCompleted: {
DownloadHandler.downloadUpdated.connect(dialogHandleDownloadUpdated);
DownloadHandler.downloadCompleted.connect(dialogHandleDownloadCompleted);
DownloadHandler.downloadInterrupted.connect(dialogHandleDownloadInterrupted);
}
function dialogHandleDownloadUpdated(downloadItem) {
if (activeDownloadId === downloadItem.id) {
progressLabel.percentValue = downloadItem.percentComplete;
speedLabel.speedValue = formatUnit(downloadItem.currentSpeed);
progressBar.value = downloadItem.percentComplete / 100;
messageLabel.text = "Will be saved at:\n" + path;
filetypeLabel.mimeValue = downloadItem.mimeType;
fileSizeLabel.sizeValue = formatUnit(downloadItem.totalBytes);
}
}
function dialogHandleDownloadCompleted(downloadItem) {
if (activeDownloadId === downloadItem.id) {
state = "complete";
progressLabel.percentValue = 100;
progressBar.value = 1;
messageLabel.text = "The file was saved at:\n" + path;
filetypeLabel.mimeValue = downloadItem.mimeType;
fileSizeLabel.sizeValue = formatUnit(downloadItem.totalBytes);
}
}
function dialogHandleDownloadInterrupted(downloadItem) {
if (activeDownloadId === downloadItem.id) {
state = "interrupted";
messageLabel.text = "The download was interrupted:\n" + interruptReason(downloadItem.interruptCode);
filetypeLabel.mimeValue = downloadItem.mimeType;
fileSizeLabel.sizeValue = formatUnit(downloadItem.totalBytes);
}
}
onOpened: {
activeDownloadId = root.pendingDownloadId;
}
onAccepted: {
folderPickerDialog.activeDownloadId = activeDownloadId;
folderPickerDialog.open();
}
onRejected: {
if (state === "confirmation" && activeDownloadId != -1) {
root.activeDownloads[activeDownloadId].cancel();
root.pendingDownloadId = -1;
root.suggestedDownloadName = "";
}
activeDownloadId = -1;
state = "confirmation";
}
Column {
width: parent.width
spacing: Theme.paddingLarge
DialogHeader {
id: title
}
Label {
id: filenameLabel
text: root.suggestedDownloadName
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeMedium
}
Label {
id: filetypeLabel
property string mimeValue: ""
text: "File type: " + mimeValue
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
Label {
id: fileSizeLabel
property string sizeValue: ""
text: "File size: " + sizeValue
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
Label {
id: progressLabel
property int percentValue: 0
text: percentValue + "%"
visible: false
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeMedium
}
Label {
id: speedLabel
property string speedValue: ""
text: speedValue + "/s"
visible: false
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
ProgressBar {
id: progressBar
visible: false
width: parent.width
value: 0
}
Label {
id: messageLabel
visible: false
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
}
DialogHeader {
id: header
cancelText: "Cancel"
acceptText: "Accept"
}
}
FolderPickerDialog {
id: folderPickerDialog
property int activeDownloadId: -1
title: "Save download to"
path: StandardPaths.home
onAccepted: {
if (activeDownloadId !== -1) {
var pendingDownload = root.activeDownloads[activeDownloadId];
if (pendingDownload) {
root.pendingDownloadId = -1;
var selectedDir = selectedPath;
var lastChar = selectedDir.charAt(selectedDir.length - 1);
var separator = (lastChar === "/") ? "" : "/";
var fullPath = selectedDir + separator + root.suggestedDownloadName;
root.downloadRequests[activeDownloadId].accept(fullPath);
pendingDownload.resume();
downloadConfirmationDialog.state = "downloading";
downloadConfirmationDialog.path = fullPath;
activeDownloadId = -1;
} else {
console.log("[DD] ERROR: Download item not found with ID:", folderPickerDialog.activeDownloadId);
}
}
}
}
function formatUnit(bytes) {
if (bytes < 1024)
return bytes + " B";
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
function interruptReason(code) {
const enumMap = {
"0": 'CEF_DOWNLOAD_INTERRUPT_REASON_NONE',
"1": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_FAILED',
"2": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED',
"3": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE',
"5": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG',
"6": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE',
"7": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED',
"10": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR',
"11": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED',
"12": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED',
"13": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT',
"14": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_HASH_MISMATCH',
"15": 'CEF_DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE',
"20": 'CEF_DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED',
"21": 'CEF_DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT',
"22": 'CEF_DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED',
"23": 'CEF_DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN',
"24": 'CEF_DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST',
"30": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED',
"31": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE',
"33": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT',
"34": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED',
"35": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_CERT_PROBLEM',
"36": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN',
"37": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_UNREACHABLE',
"38": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH',
"39": 'CEF_DOWNLOAD_INTERRUPT_REASON_SERVER_CROSS_ORIGIN_REDIRECT',
"40": 'CEF_DOWNLOAD_INTERRUPT_REASON_USER_CANCELED',
"41": 'CEF_DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN',
"50": 'CEF_DOWNLOAD_INTERRUPT_REASON_CRASH'
};
if (enumMap[code])
return enumMap[code];
else
return 'UNKNOWN_CODE_${code}';
}
}
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
import QtQuick 2.0
Item {
property var history: []
property int currentIndex: -1
property bool isCurrentlyNavigatingBack: false
property bool canGoBack: this.currentIndex > 0
function initialize(url) {
if (!url || url === "about:blank")
return false;
this.history = [url];
this.currentIndex = 0;
return true;
}
function addUrl(url) {
if (!url || url === "about:blank")
return false;
if (this.history.length === 0)
return this.initialize(url);
if (this.history[this.currentIndex] === url)
return false;
if (this.currentIndex < this.history.length - 1)
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(url);
this.currentIndex = this.history.length - 1;
return true;
}
function navigateBack() {
if (this.currentIndex <= 0)
return null;
this.isCurrentlyNavigatingBack = true;
const removedUrl = this.history[this.currentIndex];
this.history.splice(this.currentIndex, 1);
this.currentIndex--;
const urlToNavigate = this.history[this.currentIndex];
return urlToNavigate;
}
function finishBackNavigation() {
if (this.isCurrentlyNavigatingBack)
this.isCurrentlyNavigatingBack = false;
}
function getCurrentUrl() {
if (this.currentIndex >= 0 && this.currentIndex < this.history.length)
return this.history[this.currentIndex];
return null;
}
}
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
import QtQuick 2.6
import Sailfish.Silica 1.0
Dialog {
id: root
allowedOrientations: Orientation.All
property var acceptedRegularPermissions: []
property var acceptedMediaPermissions: []
property var currentRequest
property bool isMediaRequest: false
function areAllRegularPermissionsAccepted(request) {
for (var i = 0; i < request.requestedPermissions.length; i++) {
var permissionType = request.requestedPermissions[i];
if (permissions.indexOf(permissionType) === -1 && root.acceptedRegularPermissions.indexOf(permissionType) === -1) {
return false;
}
}
return true;
}
function areAllMediaPermissionsAccepted(request) {
for (var i = 0; i < request.requestedPermissions.length; i++) {
var permissionType = request.requestedPermissions[i];
if (mediaAccess.indexOf(permissionType) === -1 && root.acceptedMediaPermissions.indexOf(permissionType) === -1) {
return false;
}
}
return true;
}
function handleRegularPermissionRequest(request) {
if (areAllRegularPermissionsAccepted(request)) {
request.accept();
return;
}
root.currentRequest = request;
root.isMediaRequest = false;
root.open();
}
function handleMediaRequest(request) {
if (areAllMediaPermissionsAccepted(request)) {
request.accept();
return;
}
root.currentRequest = request;
root.isMediaRequest = true;
root.open();
}
onAccepted: {
currentRequest.accept();
if (currentRequest && currentRequest.requestedPermissions) {
for (var j = 0; j < currentRequest.requestedPermissions.length; j++) {
var perm = currentRequest.requestedPermissions[j];
if (isMediaRequest) {
if (root.acceptedMediaPermissions.indexOf(perm) === -1) {
root.acceptedMediaPermissions.push(perm);
}
} else {
if (root.acceptedRegularPermissions.indexOf(perm) === -1) {
root.acceptedRegularPermissions.push(perm);
}
}
}
}
}
onRejected: currentRequest.deny()
Column {
spacing: Theme.paddingMedium
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.paddingMedium
rightMargin: Theme.paddingMedium
}
DialogHeader {
title: "Permission Request"
}
Label {
text: "Origin: " + (currentRequest ? currentRequest.requestingOrigin : "Unknown")
width: parent.width
wrapMode: Text.Wrap
}
Label {
text: "Type: " + (isMediaRequest ? "Media" : "Regular")
width: parent.width
wrapMode: Text.Wrap
}
Label {
text: "Requested Permissions"
color: Theme.highlightColor
horizontalAlignment: Text.AlignHCenter
}
TextArea {
width: parent.width
text: getRequestedPermissionsText()
readOnly: true
wrapMode: TextEdit.Wrap
horizontalAlignment: Text.AlignHCenter
}
Label {
text: "Allow this site to use the specified features?"
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryColor
}
}
function getPermissionName(permissionType) {
if (isMediaRequest) {
switch (permissionType) {
case 1 << 0:
return "Device Audio Capture";
case 1 << 1:
return "Device Video Capture";
case 1 << 2:
return "Desktop Audio Capture";
case 1 << 3:
return "Desktop Video Capture";
default:
return "Unknown Media Permission (" + permissionType + ")";
}
} else {
switch (permissionType) {
case 1 << 0:
return "Accessibility Events";
case 1 << 1:
return "AR Session";
case 1 << 2:
return "Camera Pan/Tilt/Zoom";
case 1 << 3:
return "Camera Stream";
case 1 << 4:
return "Captured Surface Control";
case 1 << 5:
return "Clipboard";
case 1 << 6:
return "Top-level Storage Access";
case 1 << 7:
return "Disk Quota";
case 1 << 8:
return "Local Fonts";
case 1 << 9:
return "Geolocation";
case 1 << 10:
return "Identity Provider";
case 1 << 11:
return "Idle Detection";
case 1 << 12:
return "Microphone Stream";
case 1 << 13:
return "MIDI SysEx";
case 1 << 14:
return "Multiple Downloads";
case 1 << 15:
return "Notifications";
case 1 << 16:
return "Keyboard Lock";
case 1 << 17:
return "Pointer Lock";
case 1 << 18:
return "Protected Media Identifier";
case 1 << 19:
return "Register Protocol Handler";
case 1 << 20:
return "Storage Access";
case 1 << 21:
return "VR Session";
case 1 << 22:
return "Window Management";
case 1 << 23:
return "File System Access";
default:
return "Unknown Permission (" + permissionType + ")";
}
}
}
function getRequestedPermissionsText() {
if (!currentRequest || !currentRequest.requestedPermissions) {
return "No requested permissions";
}
var permissionNames = [];
for (var i = 0; i < currentRequest.requestedPermissions.length; i++) {
var permissionType = currentRequest.requestedPermissions[i];
permissionNames.push(getPermissionName(permissionType));
}
return permissionNames.join("\n");
}
}
Нет предварительного просмотра для этого типа файлов
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
import QtQuick 2.0
import QtQuick 2.6
import Sailfish.Silica 1.0
import ru.auroraos.WebView 1.0
import "../components"
Page {
objectName: "mainPage"
id: page
property bool viewSuspended: false
allowedOrientations: Orientation.All
backNavigation: false
Connections {
target: staticWebServer
onSiteFilesChanged: {
console.log("Reload page...")
webView.reload();
}
}
WebViewItem {
id: webView
objectName: "webView"
url: targetUrl
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: pageFooter.top
}
LoadRequestExtension {
enabled: true
function beforeUrlLoad(url, userGesture, isRedirect) {
return true;
}
}
TouchInput {
enabled: true
}
KeyboardInput {
enabled: true
}
PageHeader {
objectName: "pageHeader"
title: qsTr("WebAppServer")
extraContent.children: [
IconButton {
objectName: "aboutButton"
icon.source: "image://theme/icon-m-about"
anchors.verticalCenter: parent.verticalCenter
onUrlChanged: {
if (historyManager.isCurrentlyNavigatingBack) {
historyManager.finishBackNavigation();
urlField.text = url;
return;
}
historyManager.addUrl(url);
urlField.text = url;
}
Component.onCompleted: {
PermissionHandler.onPermissionsRequested.connect(permissionManager.handleRegularPermissionRequest);
PermissionHandler.onMediaAccessRequested.connect(permissionManager.handleMediaRequest);
DownloadHandler.downloadRequested.connect(downloadManager.handleDownloadRequested);
DownloadHandler.downloadUpdated.connect(downloadManager.handleDownloadUpdated);
DownloadHandler.downloadCompleted.connect(downloadManager.handleDownloadCompleted);
DownloadHandler.downloadInterrupted.connect(downloadManager.handleDownloadInterrupted);
}
}
Item {
id: pageFooter
objectName: "pageFooter"
height: Math.max(backButton.height, aboutButton.height, urlField.height) + Theme.paddingMedium * 2
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml"))
IconButton {
id: backButton
objectName: "backButton"
icon.source: "image://theme/icon-m-back"
enabled: historyManager.canGoBack
anchors {
left: parent.left
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
]
onClicked: {
const urlToNavigate = historyManager.navigateBack();
if (urlToNavigate)
webView.url = urlToNavigate;
}
}
IconButton {
id: aboutButton
objectName: "aboutButton"
anchors {
left: backButton.right
leftMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
icon {
source: "image://theme/icon-m-about"
sourceSize {
width: Theme.iconSizeMedium
height: Theme.iconSizeMedium
}
}
onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml"))
}
TextField {
id: urlField
objectName: "urlField"
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhUrlCharactersOnly
focusOutBehavior: FocusBehavior.ClearPageFocus
labelVisible: false
placeholderText: qsTr("URL")
textLeftPadding: 0
textLeftMargin: Theme.paddingMedium
anchors {
left: aboutButton.right
right: parent.right
verticalCenter: parent.verticalCenter
}
font {
pixelSize: Theme.fontSizeLarge
family: Theme.fontFamilyHeading
}
EnterKey.iconSource: "image://theme/icon-m-search"
EnterKey.onClicked: {
webView.url = text;
webView.focus = true;
}
Component.onCompleted: urlField.text = targetUrl
}
}
HistoryManager {
id: historyManager
}
PermissionManager {
id: permissionManager
}
DownloadManager {
id: downloadManager
}
}
......@@ -7,6 +7,11 @@ URL: https://auroraos.ru
Source0: %{name}-%{version}.tar.bz2
Requires: sailfishsilica-qt5 >= 0.10.9
Requires: sailfish-components-webview-qt5
Requires: sailfish-components-webview-qt5-popups
Requires: sailfish-components-webview-qt5-pickers
BuildRequires: pkgconfig(aurorawebview)
BuildRequires: pkgconfig(auroraapp)
BuildRequires: pkgconfig(Qt5Core)
BuildRequires: pkgconfig(Qt5Qml)
......@@ -32,3 +37,5 @@ Aurora OS application to demonstate how to start up local static web server and
%{_datadir}/%{name}
%{_datadir}/applications/%{name}.desktop
%{_datadir}/icons/hicolor/*/apps/%{name}.png
%defattr(755,root,root,-)
%{_libexecdir}/%{name}
......@@ -2,12 +2,11 @@
Type=Application
X-Nemo-Application-Type=silica-qt5
Icon=ru.auroraos.WebAppServer
Exec=/usr/bin/ru.auroraos.WebAppServer
Exec=/usr/bin/ru.auroraos.WebAppServer %u
Name=WebAppServer
Name[ru]=WebAppServer
[X-Application]
Permissions=
Permissions=Internet;UserDirs;DeviceInfo
OrganizationName=ru.auroraos
ApplicationName=WebAppServer
ExecDBus=/usr/bin/ru.auroraos.WebAppServer
......@@ -3,15 +3,37 @@
#include <auroraapp.h>
#include <QtQuick>
#include <QObject>
#include <webenginecontext.h>
#include "staticwebserverservice.h"
int main(int argc, char *argv[])
{
Aurora::WebView::WebEngineContext::StartProcess(argc, argv);
QGuiApplication::instance()->setAttribute(Qt::AA_ShareOpenGLContexts);
QScopedPointer<QGuiApplication> application(Aurora::Application::application(argc, argv));
application->setOrganizationName(QStringLiteral("ru.auroraos"));
application->setApplicationName(QStringLiteral("WebAppServer"));
QScopedPointer<QQuickView> view(Aurora::Application::createView());
view->setSource(Aurora::Application::pathTo(QStringLiteral("qml/WebAppServer.qml")));
StaticWebServerService service;
service.initialize();
QObject::connect(application.data(), &QCoreApplication::aboutToQuit, &service, &StaticWebServerService::stopStaticWebServer);
auto view = Aurora::Application::createView();
view->rootContext()->setContextProperty("targetUrl", service.localhostUrl());
view->rootContext()->setContextProperty("staticWebServer", &service);
QVariantList permissions;
QVariantList mediaAccess;
view->rootContext()->setContextProperty("permissions", permissions);
view->rootContext()->setContextProperty("mediaAccess", mediaAccess);
Aurora::WebView::WebEngineContext::InitBrowser();
view->setSource(
Aurora::Application::pathTo(QStringLiteral("qml/ru.auroraos.WebAppServer.qml")));
view->show();
return application->exec();
......
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
#include <QDebug>
#include <auroraapp.h>
#include <signal.h>
#include "staticwebserverservice.h"
const static QString s_port = QStringLiteral("8844");
const static QString s_siteFilesPath = Aurora::Application::getPath(PathType::DocumentsLocation)
+ "/WebAppServer/";
/*!
* \brief Default constructor.
* Sets up the file watcher to observe the site folder paths and sets up the signal-slot connections.
* \param parent QObject parent instance.
*/
StaticWebServerService::StaticWebServerService(QObject *parent) : QObject(parent)
{
m_siteFilesWatcher.addPaths(filesAndSubFolders(s_siteFilesPath));
connect(&m_siteFilesWatcher, &QFileSystemWatcher::directoryChanged,
this, &StaticWebServerService::onSitePathChanged);
connect(&m_siteFilesWatcher, &QFileSystemWatcher::fileChanged,
this, &StaticWebServerService::onSitePathChanged);
}
/*!
* \brief Destructor stops the static web server.
*/
StaticWebServerService::~StaticWebServerService()
{
stopStaticWebServer();
}
/*!
* \brief Initializes the static web server.
* The method copies the initial site data into ~/Documents/WebAppServer/ if there is no the folder,
* and starts the static web servere
*/
void StaticWebServerService::initialize()
{
copyInitialDataIfEmpty();
startStaticWebServer();
}
/*!
* \brief Starts the static web server process with port and ~/Documents/WebAppServer/
* as a root folder.
*/
void StaticWebServerService::startStaticWebServer()
{
qInfo() << "Start static web server...";
connect(&m_serverProcess, &QProcess::stateChanged, this,
&StaticWebServerService::onStaticWebServerProcessStateChange);
QString serverAppFilePath = QStringLiteral("/usr/libexec/%1.%2/static-web-server")
.arg(Aurora::Application::organizationName(), Aurora::Application::applicationName());
m_serverProcess.startDetached(serverAppFilePath, { "--port", s_port, "--root", s_siteFilesPath, "--threads-multiplier", "1",
"--max-blocking-threads", "8", "--cache-control-headers", "false"}, s_siteFilesPath, &m_processPid);
}
/*!
* \brief Stops the static web server process and disconnects the stateChanged() signal.
*/
void StaticWebServerService::stopStaticWebServer()
{
qInfo() << "Stop static web server...";
disconnect(&m_serverProcess, &QProcess::stateChanged, this,
&StaticWebServerService::onStaticWebServerProcessStateChange);
kill(m_processPid, SIGTERM);
}
/*!
* \return The localhost address with port.
*/
QString StaticWebServerService::localhostUrl() const
{
return QStringLiteral("http://localhost:%1").arg(s_port);
}
/*!
* \brief Handles changing the static web serve process state.
* The method logs the new process state and reads its data.
* \param newState New process state as QProcess::ProcessState.
*/
void StaticWebServerService::onStaticWebServerProcessStateChange(QProcess::ProcessState newState)
{
qInfo() << "New file server state: " << newState;
qInfo() << m_serverProcess.readAll();
}
void StaticWebServerService::onSitePathChanged(const QString &path)
{
qInfo() << path << "is changed";
m_siteFilesWatcher.addPath(path);
emit siteFilesChanged();
}
/*!
* \brief Copies the initial site data into the ~/Documents/WebAppServer/ folder if it isn't exists.
*/
void StaticWebServerService::copyInitialDataIfEmpty()
{
const QDir dataDir(s_siteFilesPath);
if (dataDir.exists())
return;
QString sourceDir = QStringLiteral("/usr/share/%1.%2/initial-site")
.arg(Aurora::Application::organizationName(), Aurora::Application::applicationName());
copyDir(sourceDir, s_siteFilesPath);
}
/*!
* \brief Copies all dirs and files recursively in the source path to the given destination.
* \param srcPath Source path to copy.
* \param dstPath Destination path.
* \return True if copying is finished successfully, false — if with an error.
*/
bool StaticWebServerService::copyDir(const QString &srcPath, const QString &dstPath)
{
QDir srcDir(srcPath), dstDir(dstPath);
if (!dstDir.mkpath("."))
return false;
foreach (QFileInfo info, srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) {
QString newSourcePath = info.absoluteFilePath();
QString newDestinationPath = dstDir.filePath(info.fileName());
if (info.isDir()) {
if (!copyDir(newSourcePath, newDestinationPath))
return false;
} else if (info.isFile()) {
if (!QFile::copy(newSourcePath, newDestinationPath))
return false;
} else {
qWarning() << "Unhandled item" << info.filePath() << "in copyDir";
}
}
return true;
}
/*!
* \brief Searches and returns a list of all files and sub-folders of the fiven path.
* \param path Path to the folder to watch.
* \return QStringList with paths.
*/
QStringList StaticWebServerService::filesAndSubFolders(const QString &path)
{
QStringList list { path };
QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
while (it.hasNext()) {
list << it.next();
}
return list;
}
// SPDX-FileCopyrightText: 2025 Open Mobile Platform LLC <community@omp.ru>
// SPDX-License-Identifier: BSD-3-Clause
#ifndef STATICWEBSERVERSERVICE_H
#define STATICWEBSERVERSERVICE_H
#include <QObject>
#include <QProcess>
#include <QFileSystemWatcher>
/*!
* \brief StaticWebServerService class is a class to set up and run the static wev server.
*/
class StaticWebServerService : public QObject
{
Q_OBJECT
public:
explicit StaticWebServerService(QObject *parent = 0);
~StaticWebServerService();
void initialize();
QString localhostUrl() const;
public slots:
void stopStaticWebServer();
private:
void startStaticWebServer();
void copyInitialDataIfEmpty();
bool copyDir(const QString &srcPath, const QString &dstPath);
QStringList filesAndSubFolders(const QString &path);
private slots:
void onStaticWebServerProcessStateChange(QProcess::ProcessState newState);
void onSitePathChanged(const QString &path);
private:
QProcess m_serverProcess;
QFileSystemWatcher m_siteFilesWatcher;
qint64 m_processPid;
signals:
void siteFilesChanged();
};
#endif // STATICWEBSERVERSERVICE_H
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать