mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Compare commits
2 Commits
v0.4.0-bet
...
feat/gener
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8401b753fb | ||
|
|
f8efd6e6be |
67
.github/workflows/startos-iso.yaml
vendored
67
.github/workflows/startos-iso.yaml
vendored
@@ -29,7 +29,7 @@ on:
|
|||||||
- aarch64
|
- aarch64
|
||||||
- aarch64-nonfree
|
- aarch64-nonfree
|
||||||
- aarch64-nvidia
|
- aarch64-nvidia
|
||||||
- raspberrypi
|
# - raspberrypi
|
||||||
- riscv64
|
- riscv64
|
||||||
- riscv64-nonfree
|
- riscv64-nonfree
|
||||||
deploy:
|
deploy:
|
||||||
@@ -89,9 +89,9 @@ jobs:
|
|||||||
"riscv64": "ubuntu-latest"
|
"riscv64": "ubuntu-latest"
|
||||||
}')[matrix.arch],
|
}')[matrix.arch],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "amd64-fast",
|
"x86_64": "ubuntu-24.04-32-cores",
|
||||||
"aarch64": "aarch64-fast",
|
"aarch64": "ubuntu-24.04-arm-32-cores",
|
||||||
"riscv64": "amd64-fast"
|
"riscv64": "ubuntu-24.04-32-cores"
|
||||||
}')[matrix.arch]
|
}')[matrix.arch]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -153,15 +153,15 @@ jobs:
|
|||||||
"riscv64-nonfree": "ubuntu-24.04-arm",
|
"riscv64-nonfree": "ubuntu-24.04-arm",
|
||||||
}')[matrix.platform],
|
}')[matrix.platform],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "amd64-fast",
|
"x86_64": "ubuntu-24.04-8-cores",
|
||||||
"x86_64-nonfree": "amd64-fast",
|
"x86_64-nonfree": "ubuntu-24.04-8-cores",
|
||||||
"x86_64-nvidia": "amd64-fast",
|
"x86_64-nvidia": "ubuntu-24.04-8-cores",
|
||||||
"aarch64": "aarch64-fast",
|
"aarch64": "ubuntu-24.04-arm-8-cores",
|
||||||
"aarch64-nonfree": "aarch64-fast",
|
"aarch64-nonfree": "ubuntu-24.04-arm-8-cores",
|
||||||
"aarch64-nvidia": "aarch64-fast",
|
"aarch64-nvidia": "ubuntu-24.04-arm-8-cores",
|
||||||
"raspberrypi": "aarch64-fast",
|
"raspberrypi": "ubuntu-24.04-arm-8-cores",
|
||||||
"riscv64": "amd64-fast",
|
"riscv64": "ubuntu-24.04-8-cores",
|
||||||
"riscv64-nonfree": "amd64-fast",
|
"riscv64-nonfree": "ubuntu-24.04-8-cores",
|
||||||
}')[matrix.platform]
|
}')[matrix.platform]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -296,18 +296,6 @@ jobs:
|
|||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
echo "Version: $VERSION"
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
- name: Determine platforms
|
|
||||||
id: platforms
|
|
||||||
run: |
|
|
||||||
INPUT="${{ github.event.inputs.platform }}"
|
|
||||||
if [ "$INPUT" = "ALL" ]; then
|
|
||||||
PLATFORMS="x86_64 x86_64-nonfree x86_64-nvidia aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree"
|
|
||||||
else
|
|
||||||
PLATFORMS="$INPUT"
|
|
||||||
fi
|
|
||||||
echo "list=$PLATFORMS" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Platforms: $PLATFORMS"
|
|
||||||
|
|
||||||
- name: Download squashfs artifacts
|
- name: Download squashfs artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
@@ -359,12 +347,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
cd artifacts
|
cd artifacts
|
||||||
for PLATFORM in ${{ steps.platforms.outputs.list }}; do
|
for file in *.iso *.squashfs; do
|
||||||
for file in *_${PLATFORM}.squashfs *_${PLATFORM}.iso; do
|
[ -f "$file" ] || continue
|
||||||
[ -f "$file" ] || continue
|
echo "Uploading $file..."
|
||||||
echo "Uploading $file..."
|
s3cmd put -P "$file" "${{ env.S3_BUCKET }}/v${VERSION}/$file"
|
||||||
s3cmd put -P "$file" "${{ env.S3_BUCKET }}/v${VERSION}/$file"
|
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Register OS version
|
- name: Register OS version
|
||||||
@@ -377,14 +363,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
cd artifacts
|
cd artifacts
|
||||||
for PLATFORM in ${{ steps.platforms.outputs.list }}; do
|
for file in *.squashfs *.iso; do
|
||||||
for file in *_${PLATFORM}.squashfs *_${PLATFORM}.iso; do
|
[ -f "$file" ] || continue
|
||||||
[ -f "$file" ] || continue
|
PLATFORM=$(echo "$file" | sed 's/.*_\([^.]*\)\.\(squashfs\|iso\)$/\1/')
|
||||||
echo "Indexing $file for platform $PLATFORM..."
|
echo "Indexing $file for platform $PLATFORM..."
|
||||||
start-cli --registry="${{ env.REGISTRY }}" registry os asset add \
|
start-cli --registry="${{ env.REGISTRY }}" registry os asset add \
|
||||||
--platform="$PLATFORM" \
|
--platform="$PLATFORM" \
|
||||||
--version="$VERSION" \
|
--version="$VERSION" \
|
||||||
"$file" \
|
"$file" \
|
||||||
"${{ env.S3_CDN }}/v${VERSION}/$file"
|
"${{ env.S3_CDN }}/v${VERSION}/$file"
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -83,16 +83,11 @@ if [ ${#DEB_FILES[@]} -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy each deb to the pool, removing old versions of the same package+arch
|
# Copy each deb to the pool, renaming to standard format
|
||||||
for deb in "${DEB_FILES[@]}"; do
|
for deb in "${DEB_FILES[@]}"; do
|
||||||
PKG_NAME="$(dpkg-deb --field "$deb" Package)"
|
PKG_NAME="$(dpkg-deb --field "$deb" Package)"
|
||||||
PKG_ARCH="$(dpkg-deb --field "$deb" Architecture)"
|
|
||||||
POOL_DIR="$REPO_DIR/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}"
|
POOL_DIR="$REPO_DIR/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}"
|
||||||
mkdir -p "$POOL_DIR"
|
mkdir -p "$POOL_DIR"
|
||||||
# Remove old versions for the same architecture
|
|
||||||
for old in "$POOL_DIR"/${PKG_NAME}_*_${PKG_ARCH}.deb; do
|
|
||||||
[ -f "$old" ] && rm -v "$old"
|
|
||||||
done
|
|
||||||
cp "$deb" "$POOL_DIR/"
|
cp "$deb" "$POOL_DIR/"
|
||||||
dpkg-name -o "$POOL_DIR/$(basename "$deb")" 2>/dev/null || true
|
dpkg-name -o "$POOL_DIR/$(basename "$deb")" 2>/dev/null || true
|
||||||
echo "Added: $(basename "$deb") -> pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/"
|
echo "Added: $(basename "$deb") -> pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/"
|
||||||
|
|||||||
@@ -58,18 +58,15 @@ iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to
|
|||||||
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
|
|
||||||
# NAT hairpin: masquerade so replies route back through this host for proper
|
# NAT hairpin: masquerade traffic from the bridge subnet or host to the DNAT
|
||||||
# NAT reversal instead of taking a direct path that bypasses conntrack.
|
# target, so replies route back through the host for proper NAT reversal.
|
||||||
# Host-to-target hairpin: locally-originated packets whose original destination
|
# Container-to-container hairpin (source is on the bridge subnet)
|
||||||
# was sip (before OUTPUT DNAT rewrote it to dip). Using --ctorigdst ties the
|
if [ -n "$bridge_subnet" ]; then
|
||||||
# rule to this specific sip, so multiple WAN IPs forwarding the same port to
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
# different targets each get their own masquerade.
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
fi
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
# Host-to-container hairpin (host connects to its own gateway IP, source is sip)
|
||||||
# Same-subnet hairpin: when traffic originates from the same subnet as the DNAT
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
# target (e.g. a container reaching another container, or a WireGuard peer
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
# connecting to itself via the tunnel's public IP).
|
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
|
||||||
|
|
||||||
exit $err
|
exit $err
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ S3_BUCKET="s3://startos-images"
|
|||||||
S3_CDN="https://startos-images.nyc3.cdn.digitaloceanspaces.com"
|
S3_CDN="https://startos-images.nyc3.cdn.digitaloceanspaces.com"
|
||||||
START9_GPG_KEY="2D63C217"
|
START9_GPG_KEY="2D63C217"
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ARCHES="aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia"
|
ARCHES="aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia"
|
||||||
CLI_ARCHES="aarch64 riscv64 x86_64"
|
CLI_ARCHES="aarch64 riscv64 x86_64"
|
||||||
|
|
||||||
@@ -84,21 +83,16 @@ resolve_gh_user() {
|
|||||||
GH_GPG_KEY=$(git config user.signingkey 2>/dev/null || true)
|
GH_GPG_KEY=$(git config user.signingkey 2>/dev/null || true)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fetch the URL for an OS asset from the registry index.
|
|
||||||
# Usage: registry_url <iso|squashfs|img> <platform>
|
|
||||||
registry_url() {
|
|
||||||
local ext=$1 platform=$2
|
|
||||||
if [ -z "${_REGISTRY_INDEX:-}" ]; then
|
|
||||||
_REGISTRY_INDEX=$(start-cli --registry=$REGISTRY registry os index)
|
|
||||||
fi
|
|
||||||
echo "$_REGISTRY_INDEX" | jq -r ".versions[\"$VERSION\"].$ext[\"$platform\"].urls[0]"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Subcommands ---
|
# --- Subcommands ---
|
||||||
|
|
||||||
cmd_download() {
|
cmd_download() {
|
||||||
require_version
|
require_version
|
||||||
|
|
||||||
|
if [ -z "${RUN_ID:-}" ]; then
|
||||||
|
read -rp "RUN_ID (OS images, leave blank to skip): " RUN_ID
|
||||||
|
fi
|
||||||
|
RUN_ID=$(parse_run_id "${RUN_ID:-}")
|
||||||
|
|
||||||
if [ -z "${ST_RUN_ID:-}" ]; then
|
if [ -z "${ST_RUN_ID:-}" ]; then
|
||||||
read -rp "ST_RUN_ID (start-tunnel, leave blank to skip): " ST_RUN_ID
|
read -rp "ST_RUN_ID (start-tunnel, leave blank to skip): " ST_RUN_ID
|
||||||
fi
|
fi
|
||||||
@@ -111,14 +105,14 @@ cmd_download() {
|
|||||||
|
|
||||||
ensure_release_dir
|
ensure_release_dir
|
||||||
|
|
||||||
# Download OS images from registry (deployed by GitHub workflow)
|
if [ -n "$RUN_ID" ]; then
|
||||||
echo "Downloading OS images from registry..."
|
for arch in $ARCHES; do
|
||||||
for arch in $ARCHES; do
|
while ! gh run download -R $REPO "$RUN_ID" -n "$arch.squashfs" -D "$(pwd)"; do sleep 1; done
|
||||||
for ext in squashfs iso; do
|
|
||||||
echo " $ext $arch"
|
|
||||||
start-cli --registry=$REGISTRY registry os asset get "$ext" "$VERSION" "$arch" -d "$(pwd)"
|
|
||||||
done
|
done
|
||||||
done
|
for arch in $ARCHES; do
|
||||||
|
while ! gh run download -R $REPO "$RUN_ID" -n "$arch.iso" -D "$(pwd)"; do sleep 1; done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$ST_RUN_ID" ]; then
|
if [ -n "$ST_RUN_ID" ]; then
|
||||||
for arch in $CLI_ARCHES; do
|
for arch in $CLI_ARCHES; do
|
||||||
@@ -149,12 +143,19 @@ cmd_pull() {
|
|||||||
gh release download -R $REPO "v$VERSION" -p "$file" -D "$(pwd)" --clobber
|
gh release download -R $REPO "v$VERSION" -p "$file" -D "$(pwd)" --clobber
|
||||||
done
|
done
|
||||||
|
|
||||||
# Download ISOs and squashfs from registry
|
# Download ISOs and squashfs from S3 CDN
|
||||||
echo "Downloading OS images from registry..."
|
|
||||||
for arch in $ARCHES; do
|
for arch in $ARCHES; do
|
||||||
for ext in squashfs iso; do
|
for ext in squashfs iso; do
|
||||||
echo " $ext $arch"
|
# Get the actual filename from the GH release asset list or body
|
||||||
start-cli --registry=$REGISTRY registry os asset get "$ext" "$VERSION" "$arch" -d "$(pwd)"
|
local filename
|
||||||
|
filename=$(gh release view -R $REPO "v$VERSION" --json assets -q ".assets[].name" | grep "_${arch}\\.${ext}$" || true)
|
||||||
|
if [ -z "$filename" ]; then
|
||||||
|
filename=$(gh release view -R $REPO "v$VERSION" --json body -q .body | grep -oP "[^ ]*_${arch}\\.${ext}" | head -1 || true)
|
||||||
|
fi
|
||||||
|
if [ -n "$filename" ]; then
|
||||||
|
echo "Downloading $filename from S3..."
|
||||||
|
curl -fSL -o "$filename" "$S3_CDN/v$VERSION/$filename"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -169,12 +170,14 @@ cmd_upload() {
|
|||||||
require_version
|
require_version
|
||||||
enter_release_dir
|
enter_release_dir
|
||||||
|
|
||||||
# OS images (iso/squashfs) are already on S3 via the GitHub workflow.
|
|
||||||
# Upload only debs and CLI binaries to the GitHub Release.
|
|
||||||
for file in $(release_files); do
|
for file in $(release_files); do
|
||||||
case "$file" in
|
case "$file" in
|
||||||
*.iso|*.squashfs) ;;
|
*.iso|*.squashfs)
|
||||||
*) gh release upload -R $REPO "v$VERSION" "$file" ;;
|
s3cmd put -P "$file" "$S3_BUCKET/v$VERSION/$file"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
gh release upload -R $REPO "v$VERSION" "$file"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -245,24 +248,6 @@ cmd_cosign() {
|
|||||||
echo "Done. Personal signatures for $GH_USER added to v$VERSION."
|
echo "Done. Personal signatures for $GH_USER added to v$VERSION."
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_publish_tunnel() {
|
|
||||||
require_version
|
|
||||||
enter_release_dir
|
|
||||||
|
|
||||||
local tunnel_debs=()
|
|
||||||
for file in start-tunnel*.deb; do
|
|
||||||
[ -f "$file" ] && tunnel_debs+=("$file")
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#tunnel_debs[@]} -eq 0 ]; then
|
|
||||||
>&2 echo "No start-tunnel .deb files found in release directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Publishing start-tunnel debs to apt repository..."
|
|
||||||
"$SCRIPT_DIR/apt/publish-deb.sh" "${tunnel_debs[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_notes() {
|
cmd_notes() {
|
||||||
require_version
|
require_version
|
||||||
enter_release_dir
|
enter_release_dir
|
||||||
@@ -270,14 +255,14 @@ cmd_notes() {
|
|||||||
cat << EOF
|
cat << EOF
|
||||||
# ISO Downloads
|
# ISO Downloads
|
||||||
|
|
||||||
- [x86_64/AMD64]($(registry_url iso x86_64-nonfree))
|
- [x86_64/AMD64]($S3_CDN/v$VERSION/$(ls *_x86_64-nonfree.iso))
|
||||||
- [x86_64/AMD64 + NVIDIA]($(registry_url iso x86_64-nvidia))
|
- [x86_64/AMD64 + NVIDIA]($S3_CDN/v$VERSION/$(ls *_x86_64-nvidia.iso))
|
||||||
- [x86_64/AMD64-slim (FOSS-only)]($(registry_url iso x86_64) "Without proprietary software or drivers")
|
- [x86_64/AMD64-slim (FOSS-only)]($S3_CDN/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers")
|
||||||
- [aarch64/ARM64]($(registry_url iso aarch64-nonfree))
|
- [aarch64/ARM64]($S3_CDN/v$VERSION/$(ls *_aarch64-nonfree.iso))
|
||||||
- [aarch64/ARM64 + NVIDIA]($(registry_url iso aarch64-nvidia))
|
- [aarch64/ARM64 + NVIDIA]($S3_CDN/v$VERSION/$(ls *_aarch64-nvidia.iso))
|
||||||
- [aarch64/ARM64-slim (FOSS-Only)]($(registry_url iso aarch64) "Without proprietary software or drivers")
|
- [aarch64/ARM64-slim (FOSS-Only)]($S3_CDN/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
|
||||||
- [RISCV64 (RVA23)]($(registry_url iso riscv64-nonfree))
|
- [RISCV64 (RVA23)]($S3_CDN/v$VERSION/$(ls *_riscv64-nonfree.iso))
|
||||||
- [RISCV64 (RVA23)-slim (FOSS-only)]($(registry_url iso riscv64) "Without proprietary software or drivers")
|
- [RISCV64 (RVA23)-slim (FOSS-only)]($S3_CDN/v$VERSION/$(ls *_riscv64.iso) "Without proprietary software or drivers")
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
@@ -333,8 +318,9 @@ EOF
|
|||||||
|
|
||||||
cmd_full_release() {
|
cmd_full_release() {
|
||||||
cmd_download
|
cmd_download
|
||||||
|
cmd_register
|
||||||
cmd_upload
|
cmd_upload
|
||||||
cmd_publish_tunnel
|
cmd_index
|
||||||
cmd_sign
|
cmd_sign
|
||||||
cmd_notes
|
cmd_notes
|
||||||
}
|
}
|
||||||
@@ -344,23 +330,22 @@ usage() {
|
|||||||
Usage: manage-release.sh <subcommand>
|
Usage: manage-release.sh <subcommand>
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
download Download OS images from registry + other artifacts from GH Actions
|
download Download artifacts from GitHub Actions runs
|
||||||
OS images are pulled via start-cli from the registry (deployed by GH workflow)
|
Requires: RUN_ID, ST_RUN_ID, CLI_RUN_ID (any combination)
|
||||||
Requires: ST_RUN_ID, CLI_RUN_ID (any combination)
|
pull Download an existing release from the GH tag and S3
|
||||||
pull Download an existing release from the GH tag and S3
|
register Register the version in the Start9 registry
|
||||||
register Register the version in the Start9 registry
|
upload Upload artifacts to GitHub Releases and S3
|
||||||
upload Upload artifacts to GitHub Releases and S3
|
index Add assets to the registry index
|
||||||
index Add assets to the registry index
|
sign Sign all artifacts with Start9 org key (+ personal key if available)
|
||||||
publish-tunnel Publish start-tunnel .deb files to the apt repository
|
and upload signatures.tar.gz
|
||||||
sign Sign all artifacts with Start9 org key (+ personal key if available)
|
cosign Add personal GPG signature to an existing release's signatures
|
||||||
and upload signatures.tar.gz
|
(requires 'pull' first so you can verify assets before signing)
|
||||||
cosign Add personal GPG signature to an existing release's signatures
|
notes Print release notes with download links and checksums
|
||||||
(requires 'pull' first so you can verify assets before signing)
|
full-release Run: download → register → upload → index → sign → notes
|
||||||
notes Print release notes with download links and checksums
|
|
||||||
full-release Run: download → register → upload → publish-tunnel → sign → notes
|
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
VERSION (required) Release version
|
VERSION (required) Release version
|
||||||
|
RUN_ID GitHub Actions run ID for OS images (download subcommand)
|
||||||
ST_RUN_ID GitHub Actions run ID for start-tunnel (download subcommand)
|
ST_RUN_ID GitHub Actions run ID for start-tunnel (download subcommand)
|
||||||
CLI_RUN_ID GitHub Actions run ID for start-cli (download subcommand)
|
CLI_RUN_ID GitHub Actions run ID for start-cli (download subcommand)
|
||||||
GH_USER Override GitHub username (default: autodetected via gh cli)
|
GH_USER Override GitHub username (default: autodetected via gh cli)
|
||||||
@@ -369,15 +354,14 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
download) cmd_download ;;
|
download) cmd_download ;;
|
||||||
pull) cmd_pull ;;
|
pull) cmd_pull ;;
|
||||||
register) cmd_register ;;
|
register) cmd_register ;;
|
||||||
upload) cmd_upload ;;
|
upload) cmd_upload ;;
|
||||||
index) cmd_index ;;
|
index) cmd_index ;;
|
||||||
publish-tunnel) cmd_publish_tunnel ;;
|
sign) cmd_sign ;;
|
||||||
sign) cmd_sign ;;
|
cosign) cmd_cosign ;;
|
||||||
cosign) cmd_cosign ;;
|
notes) cmd_notes ;;
|
||||||
notes) cmd_notes ;;
|
full-release) cmd_full_release ;;
|
||||||
full-release) cmd_full_release ;;
|
*) usage; exit 1 ;;
|
||||||
*) usage; exit 1 ;;
|
|
||||||
esac
|
esac
|
||||||
|
|||||||
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"../sdk/dist": {
|
"../sdk/dist": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "1.0.0",
|
"version": "0.4.0-beta.66",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -396,12 +396,6 @@ export class SystemForEmbassy implements System {
|
|||||||
if (this.manifest.id === "nostr") {
|
if (this.manifest.id === "nostr") {
|
||||||
this.manifest.id = "nostr-rs-relay"
|
this.manifest.id = "nostr-rs-relay"
|
||||||
}
|
}
|
||||||
if (this.manifest.id === "ghost") {
|
|
||||||
this.manifest.id = "ghost-legacy"
|
|
||||||
}
|
|
||||||
if (this.manifest.id === "synapse") {
|
|
||||||
this.manifest.id = "synapse-legacy"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(
|
async init(
|
||||||
@@ -500,7 +494,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const host = new MultiHost({ effects, id })
|
const host = new MultiHost({ effects, id })
|
||||||
const internalPorts = new Set(
|
const internalPorts = new Set(
|
||||||
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
||||||
.map((v) => parseInt(v))
|
.map(Number.parseInt)
|
||||||
.concat(
|
.concat(
|
||||||
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
||||||
(c) => c.internal,
|
(c) => c.internal,
|
||||||
|
|||||||
40
core/Cargo.lock
generated
40
core/Cargo.lock
generated
@@ -1967,18 +1967,6 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fallible-iterator"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fallible-streaming-iterator"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -3574,17 +3562,6 @@ dependencies = [
|
|||||||
"redox_syscall 0.7.3",
|
"redox_syscall 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libsqlite3-sys"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libyml"
|
name = "libyml"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
@@ -5467,20 +5444,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rusqlite"
|
|
||||||
version = "0.34.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.0",
|
|
||||||
"fallible-iterator",
|
|
||||||
"fallible-streaming-iterator",
|
|
||||||
"hashlink",
|
|
||||||
"libsqlite3-sys",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-argon2"
|
name = "rust-argon2"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@@ -6476,7 +6439,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "start-os"
|
name = "start-os"
|
||||||
version = "0.4.0-beta.0"
|
version = "0.4.0-alpha.23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"async-acme",
|
"async-acme",
|
||||||
@@ -6573,7 +6536,6 @@ dependencies = [
|
|||||||
"reqwest_cookie_store",
|
"reqwest_cookie_store",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rpc-toolkit",
|
"rpc-toolkit",
|
||||||
"rusqlite",
|
|
||||||
"rust-argon2",
|
"rust-argon2",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"semver",
|
"semver",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-beta.0" # VERSION_BUMP
|
version = "0.4.0-alpha.23" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
name = "startos"
|
||||||
@@ -182,7 +182,6 @@ qrcode = "0.14.1"
|
|||||||
r3bl_tui = "0.7.6"
|
r3bl_tui = "0.7.6"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
rusqlite = { version = "0.34", features = ["bundled"] }
|
|
||||||
reqwest = { version = "0.12.25", features = [
|
reqwest = { version = "0.12.25", features = [
|
||||||
"http2",
|
"http2",
|
||||||
"json",
|
"json",
|
||||||
@@ -251,3 +250,5 @@ opt-level = 3
|
|||||||
[profile.dev.package.backtrace]
|
[profile.dev.package.backtrace]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev.package.sqlx-macros]
|
||||||
|
opt-level = 3
|
||||||
|
|||||||
@@ -1826,21 +1826,6 @@ registry.os.version.signer-not-authorized:
|
|||||||
fr_FR: "Le signataire %{signer} n'est pas autorisé à signer pour v%{version}"
|
fr_FR: "Le signataire %{signer} n'est pas autorisé à signer pour v%{version}"
|
||||||
pl_PL: "Sygnatariusz %{signer} nie jest autoryzowany do podpisywania v%{version}"
|
pl_PL: "Sygnatariusz %{signer} nie jest autoryzowany do podpisywania v%{version}"
|
||||||
|
|
||||||
# registry/os/promote.rs
|
|
||||||
registry.os.promote.need-from-or-to:
|
|
||||||
en_US: "At least one of --from or --to must be specified"
|
|
||||||
de_DE: "Mindestens --from oder --to muss angegeben werden"
|
|
||||||
es_ES: "Se debe especificar al menos --from o --to"
|
|
||||||
fr_FR: "Au moins --from ou --to doit être spécifié"
|
|
||||||
pl_PL: "Należy podać przynajmniej --from lub --to"
|
|
||||||
|
|
||||||
registry.os.promote.version-not-found:
|
|
||||||
en_US: "OS version %{version} not found on source registry"
|
|
||||||
de_DE: "OS-Version %{version} nicht in der Quell-Registry gefunden"
|
|
||||||
es_ES: "Versión del SO %{version} no encontrada en el registro de origen"
|
|
||||||
fr_FR: "Version OS %{version} introuvable dans le registre source"
|
|
||||||
pl_PL: "Wersja OS %{version} nie znaleziona w rejestrze źródłowym"
|
|
||||||
|
|
||||||
# registry/package/mod.rs
|
# registry/package/mod.rs
|
||||||
registry.package.remove-not-exist:
|
registry.package.remove-not-exist:
|
||||||
en_US: "%{id}@%{version}%{sighash} does not exist, so not removed"
|
en_US: "%{id}@%{version}%{sighash} does not exist, so not removed"
|
||||||
@@ -1885,20 +1870,6 @@ registry.package.add-mirror.unauthorized:
|
|||||||
fr_FR: "Non autorisé"
|
fr_FR: "Non autorisé"
|
||||||
pl_PL: "Brak autoryzacji"
|
pl_PL: "Brak autoryzacji"
|
||||||
|
|
||||||
registry.package.promote.need-from-or-to:
|
|
||||||
en_US: "At least one of --from or --to must be specified"
|
|
||||||
de_DE: "Mindestens --from oder --to muss angegeben werden"
|
|
||||||
es_ES: "Se debe especificar al menos --from o --to"
|
|
||||||
fr_FR: "Au moins --from ou --to doit être spécifié"
|
|
||||||
pl_PL: "Należy podać przynajmniej --from lub --to"
|
|
||||||
|
|
||||||
registry.package.promote.version-not-found:
|
|
||||||
en_US: "Version %{version} of %{id} not found on source registry"
|
|
||||||
de_DE: "Version %{version} von %{id} nicht in der Quell-Registry gefunden"
|
|
||||||
es_ES: "Versión %{version} de %{id} no encontrada en el registro de origen"
|
|
||||||
fr_FR: "Version %{version} de %{id} introuvable dans le registre source"
|
|
||||||
pl_PL: "Wersja %{version} pakietu %{id} nie znaleziona w rejestrze źródłowym"
|
|
||||||
|
|
||||||
registry.package.cannot-remove-last-mirror:
|
registry.package.cannot-remove-last-mirror:
|
||||||
en_US: "Cannot remove last mirror from an s9pk"
|
en_US: "Cannot remove last mirror from an s9pk"
|
||||||
de_DE: "Letzter Spiegel kann nicht aus einem s9pk entfernt werden"
|
de_DE: "Letzter Spiegel kann nicht aus einem s9pk entfernt werden"
|
||||||
@@ -2931,13 +2902,6 @@ help.arg.force-uninstall:
|
|||||||
fr_FR: "Ignorer les erreurs dans le script de désinitialisation du service"
|
fr_FR: "Ignorer les erreurs dans le script de désinitialisation du service"
|
||||||
pl_PL: "Ignoruj błędy w skrypcie deinicjalizacji usługi"
|
pl_PL: "Ignoruj błędy w skrypcie deinicjalizacji usługi"
|
||||||
|
|
||||||
help.arg.from-registry-url:
|
|
||||||
en_US: "Source registry URL to promote from"
|
|
||||||
de_DE: "Quell-Registry-URL zum Heraufstufen"
|
|
||||||
es_ES: "URL del registro de origen para promover"
|
|
||||||
fr_FR: "URL du registre source pour la promotion"
|
|
||||||
pl_PL: "URL rejestru źródłowego do promowania"
|
|
||||||
|
|
||||||
help.arg.host-url:
|
help.arg.host-url:
|
||||||
en_US: "URL of the StartOS server"
|
en_US: "URL of the StartOS server"
|
||||||
de_DE: "URL des StartOS-Servers"
|
de_DE: "URL des StartOS-Servers"
|
||||||
@@ -2959,13 +2923,6 @@ help.arg.icon-path:
|
|||||||
fr_FR: "Chemin vers le fichier d'icône du service"
|
fr_FR: "Chemin vers le fichier d'icône du service"
|
||||||
pl_PL: "Ścieżka do pliku ikony usługi"
|
pl_PL: "Ścieżka do pliku ikony usługi"
|
||||||
|
|
||||||
help.arg.icon-source:
|
|
||||||
en_US: "Icon source: file path, file:// URL, http(s):// URL, or data: URL"
|
|
||||||
de_DE: "Icon-Quelle: Dateipfad, file://-URL, http(s)://-URL oder data:-URL"
|
|
||||||
es_ES: "Fuente del icono: ruta de archivo, URL file://, URL http(s):// o URL data:"
|
|
||||||
fr_FR: "Source de l'icône : chemin de fichier, URL file://, URL http(s):// ou URL data:"
|
|
||||||
pl_PL: "Źródło ikony: ścieżka pliku, URL file://, URL http(s):// lub URL data:"
|
|
||||||
|
|
||||||
help.arg.image-id:
|
help.arg.image-id:
|
||||||
en_US: "Docker image identifier"
|
en_US: "Docker image identifier"
|
||||||
de_DE: "Docker-Image-Kennung"
|
de_DE: "Docker-Image-Kennung"
|
||||||
@@ -3050,34 +3007,6 @@ help.arg.merge:
|
|||||||
fr_FR: "Fusionner avec la plage de versions existante au lieu de remplacer"
|
fr_FR: "Fusionner avec la plage de versions existante au lieu de remplacer"
|
||||||
pl_PL: "Połącz z istniejącym zakresem wersji zamiast zastępować"
|
pl_PL: "Połącz z istniejącym zakresem wersji zamiast zastępować"
|
||||||
|
|
||||||
help.arg.metrics-after:
|
|
||||||
en_US: "Start of time range (RFC 3339 timestamp)"
|
|
||||||
de_DE: "Beginn des Zeitraums (RFC 3339 Zeitstempel)"
|
|
||||||
es_ES: "Inicio del rango de tiempo (marca de tiempo RFC 3339)"
|
|
||||||
fr_FR: "Début de la plage temporelle (horodatage RFC 3339)"
|
|
||||||
pl_PL: "Początek zakresu czasu (znacznik czasu RFC 3339)"
|
|
||||||
|
|
||||||
help.arg.metrics-before:
|
|
||||||
en_US: "End of time range (RFC 3339 timestamp)"
|
|
||||||
de_DE: "Ende des Zeitraums (RFC 3339 Zeitstempel)"
|
|
||||||
es_ES: "Fin del rango de tiempo (marca de tiempo RFC 3339)"
|
|
||||||
fr_FR: "Fin de la plage temporelle (horodatage RFC 3339)"
|
|
||||||
pl_PL: "Koniec zakresu czasu (znacznik czasu RFC 3339)"
|
|
||||||
|
|
||||||
help.arg.metrics-pkg-id:
|
|
||||||
en_US: "Filter by package ID"
|
|
||||||
de_DE: "Nach Paket-ID filtern"
|
|
||||||
es_ES: "Filtrar por ID de paquete"
|
|
||||||
fr_FR: "Filtrer par identifiant de paquet"
|
|
||||||
pl_PL: "Filtruj według identyfikatora pakietu"
|
|
||||||
|
|
||||||
help.arg.metrics-version:
|
|
||||||
en_US: "Filter by version"
|
|
||||||
de_DE: "Nach Version filtern"
|
|
||||||
es_ES: "Filtrar por versión"
|
|
||||||
fr_FR: "Filtrer par version"
|
|
||||||
pl_PL: "Filtruj według wersji"
|
|
||||||
|
|
||||||
help.arg.mirror-url:
|
help.arg.mirror-url:
|
||||||
en_US: "URL of the mirror"
|
en_US: "URL of the mirror"
|
||||||
de_DE: "URL des Spiegels"
|
de_DE: "URL des Spiegels"
|
||||||
@@ -3197,6 +3126,13 @@ help.arg.port:
|
|||||||
fr_FR: "Numéro de port"
|
fr_FR: "Numéro de port"
|
||||||
pl_PL: "Numer portu"
|
pl_PL: "Numer portu"
|
||||||
|
|
||||||
|
help.arg.postgres-connection-url:
|
||||||
|
en_US: "PostgreSQL connection URL"
|
||||||
|
de_DE: "PostgreSQL-Verbindungs-URL"
|
||||||
|
es_ES: "URL de conexión PostgreSQL"
|
||||||
|
fr_FR: "URL de connexion PostgreSQL"
|
||||||
|
pl_PL: "URL połączenia PostgreSQL"
|
||||||
|
|
||||||
help.arg.proxy-url:
|
help.arg.proxy-url:
|
||||||
en_US: "HTTP/SOCKS proxy URL"
|
en_US: "HTTP/SOCKS proxy URL"
|
||||||
de_DE: "HTTP/SOCKS-Proxy-URL"
|
de_DE: "HTTP/SOCKS-Proxy-URL"
|
||||||
@@ -3421,13 +3357,6 @@ help.arg.target-version-range:
|
|||||||
fr_FR: "Contrainte de plage de version cible"
|
fr_FR: "Contrainte de plage de version cible"
|
||||||
pl_PL: "Ograniczenie zakresu wersji docelowej"
|
pl_PL: "Ograniczenie zakresu wersji docelowej"
|
||||||
|
|
||||||
help.arg.to-registry-url:
|
|
||||||
en_US: "Destination registry URL to promote to"
|
|
||||||
de_DE: "Ziel-Registry-URL zum Heraufstufen"
|
|
||||||
es_ES: "URL del registro de destino para promover"
|
|
||||||
fr_FR: "URL du registre de destination pour la promotion"
|
|
||||||
pl_PL: "URL rejestru docelowego do promowania"
|
|
||||||
|
|
||||||
help.arg.tor-proxy-url:
|
help.arg.tor-proxy-url:
|
||||||
en_US: "Tor SOCKS proxy URL"
|
en_US: "Tor SOCKS proxy URL"
|
||||||
de_DE: "Tor-SOCKS-Proxy-URL"
|
de_DE: "Tor-SOCKS-Proxy-URL"
|
||||||
@@ -4528,13 +4457,6 @@ about.commands-registry-db:
|
|||||||
fr_FR: "Commandes pour interagir avec la base de données, comme dump et apply"
|
fr_FR: "Commandes pour interagir avec la base de données, comme dump et apply"
|
||||||
pl_PL: "Polecenia interakcji z bazą danych, np. dump i apply"
|
pl_PL: "Polecenia interakcji z bazą danych, np. dump i apply"
|
||||||
|
|
||||||
about.commands-registry-metrics:
|
|
||||||
en_US: "Query registry usage metrics"
|
|
||||||
de_DE: "Registry-Nutzungsmetriken abfragen"
|
|
||||||
es_ES: "Consultar métricas de uso del registro"
|
|
||||||
fr_FR: "Consulter les métriques d'utilisation du registre"
|
|
||||||
pl_PL: "Zapytaj o metryki użycia rejestru"
|
|
||||||
|
|
||||||
about.commands-registry-info:
|
about.commands-registry-info:
|
||||||
en_US: "View or edit registry information"
|
en_US: "View or edit registry information"
|
||||||
de_DE: "Registry-Informationen anzeigen oder bearbeiten"
|
de_DE: "Registry-Informationen anzeigen oder bearbeiten"
|
||||||
@@ -4976,27 +4898,6 @@ about.get-listen-address-for-webserver:
|
|||||||
fr_FR: "Obtenir l'adresse d'écoute du serveur web"
|
fr_FR: "Obtenir l'adresse d'écoute du serveur web"
|
||||||
pl_PL: "Pobierz adres nasłuchiwania serwera internetowego"
|
pl_PL: "Pobierz adres nasłuchiwania serwera internetowego"
|
||||||
|
|
||||||
about.get-metrics-downloads:
|
|
||||||
en_US: "Count package download requests with optional filters"
|
|
||||||
de_DE: "Paket-Download-Anfragen mit optionalen Filtern zählen"
|
|
||||||
es_ES: "Contar solicitudes de descarga de paquetes con filtros opcionales"
|
|
||||||
fr_FR: "Compter les demandes de téléchargement de paquets avec filtres optionnels"
|
|
||||||
pl_PL: "Zlicz żądania pobrania pakietów z opcjonalnymi filtrami"
|
|
||||||
|
|
||||||
about.get-metrics-summary:
|
|
||||||
en_US: "Get a summary of registry usage metrics"
|
|
||||||
de_DE: "Zusammenfassung der Registry-Nutzungsmetriken abrufen"
|
|
||||||
es_ES: "Obtener un resumen de las métricas de uso del registro"
|
|
||||||
fr_FR: "Obtenir un résumé des métriques d'utilisation du registre"
|
|
||||||
pl_PL: "Pobierz podsumowanie metryk użycia rejestru"
|
|
||||||
|
|
||||||
about.get-metrics-users:
|
|
||||||
en_US: "Count unique active users within a time range"
|
|
||||||
de_DE: "Eindeutige aktive Benutzer in einem Zeitraum zählen"
|
|
||||||
es_ES: "Contar usuarios activos únicos dentro de un rango de tiempo"
|
|
||||||
fr_FR: "Compter les utilisateurs actifs uniques dans un intervalle de temps"
|
|
||||||
pl_PL: "Zlicz unikalnych aktywnych użytkowników w zakresie czasu"
|
|
||||||
|
|
||||||
about.get-os-versions-info:
|
about.get-os-versions-info:
|
||||||
en_US: "Get OS versions info"
|
en_US: "Get OS versions info"
|
||||||
de_DE: "Informationen zu Betriebssystemversionen abrufen"
|
de_DE: "Informationen zu Betriebssystemversionen abrufen"
|
||||||
@@ -5333,20 +5234,6 @@ about.persist-new-notification:
|
|||||||
fr_FR: "Persister une nouvelle notification"
|
fr_FR: "Persister une nouvelle notification"
|
||||||
pl_PL: "Utrwal nowe powiadomienie"
|
pl_PL: "Utrwal nowe powiadomienie"
|
||||||
|
|
||||||
about.promote-os-registry:
|
|
||||||
en_US: "Promote an OS version from one registry to another"
|
|
||||||
de_DE: "Eine OS-Version von einer Registry in eine andere heraufstufen"
|
|
||||||
es_ES: "Promover una versión del SO de un registro a otro"
|
|
||||||
fr_FR: "Promouvoir une version OS d'un registre à un autre"
|
|
||||||
pl_PL: "Promuj wersję OS z jednego rejestru do drugiego"
|
|
||||||
|
|
||||||
about.promote-package-registry:
|
|
||||||
en_US: "Promote a package from one registry to another"
|
|
||||||
de_DE: "Ein Paket von einer Registry in eine andere heraufstufen"
|
|
||||||
es_ES: "Promover un paquete de un registro a otro"
|
|
||||||
fr_FR: "Promouvoir un paquet d'un registre à un autre"
|
|
||||||
pl_PL: "Promuj pakiet z jednego rejestru do drugiego"
|
|
||||||
|
|
||||||
about.publish-s9pk:
|
about.publish-s9pk:
|
||||||
en_US: "Publish s9pk to S3 bucket and index on registry"
|
en_US: "Publish s9pk to S3 bucket and index on registry"
|
||||||
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ impl Public {
|
|||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
server_info: ServerInfo {
|
server_info: ServerInfo {
|
||||||
|
arch: get_arch(),
|
||||||
|
platform: get_platform(),
|
||||||
id: account.server_id.clone(),
|
id: account.server_id.clone(),
|
||||||
version: Current::default().semver(),
|
version: Current::default().semver(),
|
||||||
name: account.hostname.name.clone(),
|
name: account.hostname.name.clone(),
|
||||||
@@ -123,10 +125,10 @@ impl Public {
|
|||||||
},
|
},
|
||||||
status_info: ServerStatus {
|
status_info: ServerStatus {
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
|
updated: false,
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
restart: None,
|
|
||||||
},
|
},
|
||||||
unread_notification_count: 0,
|
unread_notification_count: 0,
|
||||||
password_hash: account.password.clone(),
|
password_hash: account.password.clone(),
|
||||||
@@ -158,6 +160,14 @@ impl Public {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_arch() -> InternedString {
|
||||||
|
(*ARCH).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_platform() -> InternedString {
|
||||||
|
(&*PLATFORM).into()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_echoip_urls() -> Vec<Url> {
|
pub fn default_echoip_urls() -> Vec<Url> {
|
||||||
vec![
|
vec![
|
||||||
"https://ipconfig.io".parse().unwrap(),
|
"https://ipconfig.io".parse().unwrap(),
|
||||||
@@ -170,6 +180,10 @@ pub fn default_echoip_urls() -> Vec<Url> {
|
|||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ServerInfo {
|
pub struct ServerInfo {
|
||||||
|
#[serde(default = "get_arch")]
|
||||||
|
pub arch: InternedString,
|
||||||
|
#[serde(default = "get_platform")]
|
||||||
|
pub platform: InternedString,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: InternedString,
|
pub name: InternedString,
|
||||||
pub hostname: InternedString,
|
pub hostname: InternedString,
|
||||||
@@ -206,16 +220,6 @@ pub struct ServerInfo {
|
|||||||
pub keyboard: Option<KeyboardOptions>,
|
pub keyboard: Option<KeyboardOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub enum RestartReason {
|
|
||||||
Mdns,
|
|
||||||
Language,
|
|
||||||
Kiosk,
|
|
||||||
Update,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
@@ -360,13 +364,12 @@ pub struct BackupProgress {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ServerStatus {
|
pub struct ServerStatus {
|
||||||
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
||||||
|
pub updated: bool,
|
||||||
pub update_progress: Option<FullProgress>,
|
pub update_progress: Option<FullProgress>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub shutting_down: bool,
|
pub shutting_down: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub restarting: bool,
|
pub restarting: bool,
|
||||||
#[serde(default)]
|
|
||||||
pub restart: Option<RestartReason>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -2,11 +2,24 @@ use std::io::Cursor;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::disk::fsck::RequiresReboot;
|
use crate::disk::fsck::RequiresReboot;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn btrfs_check_readonly(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
||||||
|
Command::new("btrfs")
|
||||||
|
.arg("check")
|
||||||
|
.arg("--readonly")
|
||||||
|
.arg(logicalname.as_ref())
|
||||||
|
.invoke(crate::ErrorKind::DiskManagement)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(RequiresReboot(false))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn btrfs_check_repair(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
pub async fn btrfs_check_repair(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
||||||
Command::new("btrfs")
|
Command::new("btrfs")
|
||||||
.arg("check")
|
.arg("check")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use color_eyre::eyre::eyre;
|
|||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::disk::fsck::btrfs::btrfs_check_repair;
|
use crate::disk::fsck::btrfs::{btrfs_check_readonly, btrfs_check_repair};
|
||||||
use crate::disk::fsck::ext4::{e2fsck_aggressive, e2fsck_preen};
|
use crate::disk::fsck::ext4::{e2fsck_aggressive, e2fsck_preen};
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ impl RepairStrategy {
|
|||||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||||
) -> Result<RequiresReboot, Error> {
|
) -> Result<RequiresReboot, Error> {
|
||||||
match self {
|
match self {
|
||||||
RepairStrategy::Preen => Ok(RequiresReboot(false)),
|
RepairStrategy::Preen => btrfs_check_readonly(logicalname).await,
|
||||||
RepairStrategy::Aggressive => btrfs_check_repair(logicalname).await,
|
RepairStrategy::Aggressive => btrfs_check_repair(logicalname).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ impl OsPartitionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6E6F-744E-656564454649";
|
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548";
|
||||||
|
|
||||||
/// Find the BIOS boot partition on the same disk as `known_part`.
|
/// Find the BIOS boot partition on the same disk as `known_part`.
|
||||||
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tracing::instrument;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::db::model::public::{RestartReason, ServerInfo};
|
use crate::db::model::public::ServerInfo;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
@@ -272,7 +272,6 @@ pub async fn set_hostname_rpc(
|
|||||||
}
|
}
|
||||||
if let Some(hostname) = &hostname {
|
if let Some(hostname) = &hostname {
|
||||||
hostname.save(server_info)?;
|
hostname.save(server_info)?;
|
||||||
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Mdns))?;
|
|
||||||
}
|
}
|
||||||
ServerHostnameInfo::load(server_info)
|
ServerHostnameInfo::load(server_info)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -371,11 +371,11 @@ pub async fn init(
|
|||||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||||
let devices = lshw().await?;
|
let devices = lshw().await?;
|
||||||
let status_info = ServerStatus {
|
let status_info = ServerStatus {
|
||||||
|
updated: false,
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
restart: None,
|
|
||||||
};
|
};
|
||||||
db.mutate(|v| {
|
db.mutate(|v| {
|
||||||
let server_info = v.as_public_mut().as_server_info_mut();
|
let server_info = v.as_public_mut().as_server_info_mut();
|
||||||
|
|||||||
@@ -241,19 +241,11 @@ pub async fn check_port(
|
|||||||
.await
|
.await
|
||||||
.map_or(false, |r| r.is_ok());
|
.map_or(false, |r| r.is_ok());
|
||||||
|
|
||||||
let local_ipv4 = ip_info
|
|
||||||
.subnets
|
|
||||||
.iter()
|
|
||||||
.find_map(|s| match s.addr() {
|
|
||||||
IpAddr::V4(v4) => Some(v4),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.unwrap_or(Ipv4Addr::UNSPECIFIED);
|
|
||||||
let client = reqwest::Client::builder();
|
let client = reqwest::Client::builder();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let client = client
|
let client = client
|
||||||
.interface(gateway.as_str())
|
.interface(gateway.as_str())
|
||||||
.local_address(IpAddr::V4(local_ipv4));
|
.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||||
let client = client.build()?;
|
let client = client.build()?;
|
||||||
|
|
||||||
let mut res = None;
|
let mut res = None;
|
||||||
@@ -290,7 +282,12 @@ pub async fn check_port(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let hairpinning = check_hairpin(gateway, local_ipv4, ip, port).await;
|
let hairpinning = tokio::time::timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
tokio::net::TcpStream::connect(SocketAddr::new(ip.into(), port)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_or(false, |r| r.is_ok());
|
||||||
|
|
||||||
Ok(CheckPortRes {
|
Ok(CheckPortRes {
|
||||||
ip,
|
ip,
|
||||||
@@ -301,30 +298,6 @@ pub async fn check_port(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn check_hairpin(gateway: GatewayId, local_ipv4: Ipv4Addr, ip: Ipv4Addr, port: u16) -> bool {
|
|
||||||
let hairpinning = tokio::time::timeout(Duration::from_secs(5), async {
|
|
||||||
let dest = SocketAddr::new(ip.into(), port);
|
|
||||||
let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)?;
|
|
||||||
socket.bind_device(Some(gateway.as_str().as_bytes()))?;
|
|
||||||
socket.bind(&SocketAddr::new(IpAddr::V4(local_ipv4), 0).into())?;
|
|
||||||
socket.set_nonblocking(true)?;
|
|
||||||
let socket = unsafe {
|
|
||||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
|
||||||
tokio::net::TcpSocket::from_raw_fd(socket.into_raw_fd())
|
|
||||||
};
|
|
||||||
socket.connect(dest).await.map(|_| ())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_or(false, |r| r.is_ok());
|
|
||||||
hairpinning
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
async fn check_hairpin(_: GatewayId, _: Ipv4Addr, _: Ipv4Addr, _: u16) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
#[group(skip)]
|
#[group(skip)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -792,7 +765,6 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
});
|
});
|
||||||
gc_policy_routing(&ifaces).await;
|
|
||||||
for result in futures::future::join_all(jobs).await {
|
for result in futures::future::join_all(jobs).await {
|
||||||
result.log_err();
|
result.log_err();
|
||||||
}
|
}
|
||||||
@@ -811,16 +783,12 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_wan_ipv4(
|
async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, Error> {
|
||||||
iface: &str,
|
|
||||||
base_url: &Url,
|
|
||||||
local_ipv4: Ipv4Addr,
|
|
||||||
) -> Result<Option<Ipv4Addr>, Error> {
|
|
||||||
let client = reqwest::Client::builder();
|
let client = reqwest::Client::builder();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let client = client
|
let client = client
|
||||||
.interface(iface)
|
.interface(iface)
|
||||||
.local_address(IpAddr::V4(local_ipv4));
|
.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||||
let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?;
|
let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?;
|
||||||
let text = client
|
let text = client
|
||||||
.build()?
|
.build()?
|
||||||
@@ -838,43 +806,15 @@ async fn get_wan_ipv4(
|
|||||||
Ok(Some(trimmed.parse()?))
|
Ok(Some(trimmed.parse()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PolicyRoutingGuard {
|
struct PolicyRoutingCleanup {
|
||||||
table_id: u32,
|
table_id: u32,
|
||||||
|
iface: String,
|
||||||
}
|
}
|
||||||
|
impl Drop for PolicyRoutingCleanup {
|
||||||
/// Remove stale per-interface policy-routing state (fwmark rules, routing
|
fn drop(&mut self) {
|
||||||
/// tables, iptables CONNMARK rules) for interfaces that no longer exist.
|
let table_str = self.table_id.to_string();
|
||||||
async fn gc_policy_routing(active_ifaces: &BTreeSet<GatewayId>) {
|
let iface = std::mem::take(&mut self.iface);
|
||||||
let active_tables: BTreeSet<u32> = active_ifaces
|
tokio::spawn(async move {
|
||||||
.iter()
|
|
||||||
.filter_map(|iface| if_nametoindex(iface.as_str()).ok().map(|idx| 1000 + idx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// GC fwmark ip rules at priority 50 and their routing tables.
|
|
||||||
if let Ok(rules) = Command::new("ip")
|
|
||||||
.arg("rule")
|
|
||||||
.arg("show")
|
|
||||||
.invoke(ErrorKind::Network)
|
|
||||||
.await
|
|
||||||
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
|
||||||
{
|
|
||||||
for line in rules.lines() {
|
|
||||||
let line = line.trim();
|
|
||||||
if !line.starts_with("50:") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(pos) = line.find("lookup ") else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let token = line[pos + 7..].split_whitespace().next().unwrap_or("");
|
|
||||||
let Ok(table_id) = token.parse::<u32>() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if table_id < 1000 || active_tables.contains(&table_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let table_str = table_id.to_string();
|
|
||||||
tracing::debug!("gc_policy_routing: removing stale table {table_id}");
|
|
||||||
Command::new("ip")
|
Command::new("ip")
|
||||||
.arg("rule")
|
.arg("rule")
|
||||||
.arg("del")
|
.arg("del")
|
||||||
@@ -895,46 +835,25 @@ async fn gc_policy_routing(active_ifaces: &BTreeSet<GatewayId>) {
|
|||||||
.invoke(ErrorKind::Network)
|
.invoke(ErrorKind::Network)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
Command::new("iptables")
|
||||||
}
|
.arg("-t")
|
||||||
|
.arg("mangle")
|
||||||
// GC iptables CONNMARK set-mark rules for defunct interfaces.
|
.arg("-D")
|
||||||
if let Ok(rules) = Command::new("iptables")
|
.arg("PREROUTING")
|
||||||
.arg("-t")
|
.arg("-i")
|
||||||
.arg("mangle")
|
.arg(&iface)
|
||||||
.arg("-S")
|
.arg("-m")
|
||||||
.arg("PREROUTING")
|
.arg("conntrack")
|
||||||
.invoke(ErrorKind::Network)
|
.arg("--ctstate")
|
||||||
.await
|
.arg("NEW")
|
||||||
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
.arg("-j")
|
||||||
{
|
.arg("CONNMARK")
|
||||||
// Rules look like:
|
.arg("--set-mark")
|
||||||
// -A PREROUTING -i wg0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1005
|
.arg(&table_str)
|
||||||
for line in rules.lines() {
|
.invoke(ErrorKind::Network)
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
.await
|
||||||
if parts.first() != Some(&"-A") {
|
.ok();
|
||||||
continue;
|
});
|
||||||
}
|
|
||||||
if !parts.contains(&"--set-mark") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(iface_idx) = parts.iter().position(|&p| p == "-i") else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(&iface) = parts.get(iface_idx + 1) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if active_ifaces.contains(&GatewayId::from(InternedString::intern(iface))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tracing::debug!("gc_policy_routing: removing stale iptables rule for {iface}");
|
|
||||||
let mut cmd = Command::new("iptables");
|
|
||||||
cmd.arg("-t").arg("mangle").arg("-D");
|
|
||||||
for &arg in &parts[1..] {
|
|
||||||
cmd.arg(arg);
|
|
||||||
}
|
|
||||||
cmd.invoke(ErrorKind::Network).await.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,8 +985,11 @@ async fn watch_ip(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let policy_guard: Option<PolicyRoutingGuard> =
|
let policy_guard: Option<PolicyRoutingCleanup> =
|
||||||
policy_table_id.map(|t| PolicyRoutingGuard { table_id: t });
|
policy_table_id.map(|t| PolicyRoutingCleanup {
|
||||||
|
table_id: t,
|
||||||
|
iface: iface.as_str().to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
until
|
until
|
||||||
@@ -1094,7 +1016,7 @@ async fn watch_ip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_policy_routing(
|
async fn apply_policy_routing(
|
||||||
guard: &PolicyRoutingGuard,
|
guard: &PolicyRoutingCleanup,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
lan_ip: &OrdSet<IpAddr>,
|
lan_ip: &OrdSet<IpAddr>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -1328,7 +1250,7 @@ async fn poll_ip_info(
|
|||||||
ip4_proxy: &Ip4ConfigProxy<'_>,
|
ip4_proxy: &Ip4ConfigProxy<'_>,
|
||||||
ip6_proxy: &Ip6ConfigProxy<'_>,
|
ip6_proxy: &Ip6ConfigProxy<'_>,
|
||||||
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
||||||
policy_guard: &Option<PolicyRoutingGuard>,
|
policy_guard: &Option<PolicyRoutingCleanup>,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
||||||
db: Option<&TypedPatchDb<Database>>,
|
db: Option<&TypedPatchDb<Database>>,
|
||||||
@@ -1377,49 +1299,6 @@ async fn poll_ip_info(
|
|||||||
apply_policy_routing(guard, iface, &lan_ip).await?;
|
apply_policy_routing(guard, iface, &lan_ip).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write IP info to the watch immediately so the gateway appears in the
|
|
||||||
// DB without waiting for the (slow) WAN IP fetch. The echoip HTTP
|
|
||||||
// request has a 5-second timeout per URL and is easily cancelled by
|
|
||||||
// D-Bus signals via the Until mechanism, which would prevent the
|
|
||||||
// gateway from ever appearing if we waited.
|
|
||||||
let mut ip_info = IpInfo {
|
|
||||||
name: name.clone(),
|
|
||||||
scope_id,
|
|
||||||
device_type,
|
|
||||||
subnets: subnets.clone(),
|
|
||||||
lan_ip,
|
|
||||||
wan_ip: None,
|
|
||||||
ntp_servers,
|
|
||||||
dns_servers,
|
|
||||||
};
|
|
||||||
|
|
||||||
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
|
||||||
let (name, secure, gateway_type, prev_wan_ip) =
|
|
||||||
m.get(iface).map_or((None, None, None, None), |i| {
|
|
||||||
(
|
|
||||||
i.name.clone(),
|
|
||||||
i.secure,
|
|
||||||
i.gateway_type,
|
|
||||||
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
ip_info.wan_ip = prev_wan_ip;
|
|
||||||
let ip_info = Arc::new(ip_info);
|
|
||||||
m.insert(
|
|
||||||
iface.clone(),
|
|
||||||
NetworkInterfaceInfo {
|
|
||||||
name,
|
|
||||||
secure,
|
|
||||||
ip_info: Some(ip_info.clone()),
|
|
||||||
gateway_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.filter(|old| &old.ip_info == &Some(ip_info))
|
|
||||||
.is_none()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now fetch the WAN IP in a second pass. Even if this is slow or
|
|
||||||
// gets cancelled, the gateway already has valid ip_info above.
|
|
||||||
let echoip_urls = if let Some(db) = db {
|
let echoip_urls = if let Some(db) = db {
|
||||||
db.peek()
|
db.peek()
|
||||||
.await
|
.await
|
||||||
@@ -1443,14 +1322,7 @@ async fn poll_ip_info(
|
|||||||
Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
|
Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
let local_ipv4 = subnets
|
match get_wan_ipv4(iface.as_str(), &echoip_url).await {
|
||||||
.iter()
|
|
||||||
.find_map(|s| match s.addr() {
|
|
||||||
IpAddr::V4(v4) => Some(v4),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.unwrap_or(Ipv4Addr::UNSPECIFIED);
|
|
||||||
match get_wan_ipv4(iface.as_str(), &echoip_url, local_ipv4).await {
|
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
wan_ip = a;
|
wan_ip = a;
|
||||||
}
|
}
|
||||||
@@ -1477,25 +1349,41 @@ async fn poll_ip_info(
|
|||||||
);
|
);
|
||||||
tracing::debug!("{e:?}");
|
tracing::debug!("{e:?}");
|
||||||
}
|
}
|
||||||
|
let mut ip_info = IpInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
scope_id,
|
||||||
|
device_type,
|
||||||
|
subnets,
|
||||||
|
lan_ip,
|
||||||
|
wan_ip,
|
||||||
|
ntp_servers,
|
||||||
|
dns_servers,
|
||||||
|
};
|
||||||
|
|
||||||
// Update with WAN IP if we obtained one
|
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
||||||
if wan_ip.is_some() {
|
let (name, secure, gateway_type, prev_wan_ip) =
|
||||||
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
m.get(iface).map_or((None, None, None, None), |i| {
|
||||||
let Some(entry) = m.get_mut(iface) else {
|
(
|
||||||
return false;
|
i.name.clone(),
|
||||||
};
|
i.secure,
|
||||||
let Some(ref existing_ip) = entry.ip_info else {
|
i.gateway_type,
|
||||||
return false;
|
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
||||||
};
|
)
|
||||||
if existing_ip.wan_ip == wan_ip {
|
});
|
||||||
return false;
|
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip);
|
||||||
}
|
let ip_info = Arc::new(ip_info);
|
||||||
let mut updated = (**existing_ip).clone();
|
m.insert(
|
||||||
updated.wan_ip = wan_ip;
|
iface.clone(),
|
||||||
entry.ip_info = Some(Arc::new(updated));
|
NetworkInterfaceInfo {
|
||||||
true
|
name,
|
||||||
});
|
secure,
|
||||||
}
|
ip_info: Some(ip_info.clone()),
|
||||||
|
gateway_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.filter(|old| &old.ip_info == &Some(ip_info))
|
||||||
|
.is_none()
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use patch_db::json_ptr::ROOT;
|
|||||||
use reqwest::{Client, Proxy};
|
use reqwest::{Client, Proxy};
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest};
|
use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest};
|
||||||
use rusqlite::Connection;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
use tokio::sync::broadcast::Sender;
|
use tokio::sync::broadcast::Sender;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -34,7 +34,6 @@ use crate::registry::signer::SignerInfo;
|
|||||||
use crate::rpc_continuations::RpcContinuations;
|
use crate::rpc_continuations::RpcContinuations;
|
||||||
use crate::sign::AnyVerifyingKey;
|
use crate::sign::AnyVerifyingKey;
|
||||||
use crate::util::io::{append_file, read_file_to_string};
|
use crate::util::io::{append_file, read_file_to_string};
|
||||||
use crate::util::sync::SyncMutex;
|
|
||||||
|
|
||||||
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
||||||
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
|
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
|
||||||
@@ -58,6 +57,12 @@ pub struct RegistryConfig {
|
|||||||
pub tor_proxy: Option<Url>,
|
pub tor_proxy: Option<Url>,
|
||||||
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
|
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
|
||||||
pub datadir: Option<PathBuf>,
|
pub datadir: Option<PathBuf>,
|
||||||
|
#[arg(
|
||||||
|
short = 'u',
|
||||||
|
long = "pg-connection-url",
|
||||||
|
help = "help.arg.postgres-connection-url"
|
||||||
|
)]
|
||||||
|
pub pg_connection_url: Option<String>,
|
||||||
}
|
}
|
||||||
impl ContextConfig for RegistryConfig {
|
impl ContextConfig for RegistryConfig {
|
||||||
fn next(&mut self) -> Option<PathBuf> {
|
fn next(&mut self) -> Option<PathBuf> {
|
||||||
@@ -88,7 +93,7 @@ pub struct RegistryContextSeed {
|
|||||||
pub rpc_continuations: RpcContinuations,
|
pub rpc_continuations: RpcContinuations,
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
pub shutdown: Sender<()>,
|
pub shutdown: Sender<()>,
|
||||||
pub metrics_db: SyncMutex<Connection>,
|
pub pool: Option<PgPool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -119,29 +124,13 @@ impl RegistryContext {
|
|||||||
.clone()
|
.clone()
|
||||||
.map(Ok)
|
.map(Ok)
|
||||||
.unwrap_or_else(|| "socks5h://tor.startos:9050".parse())?;
|
.unwrap_or_else(|| "socks5h://tor.startos:9050".parse())?;
|
||||||
let metrics_db_path = datadir.join("metrics.db");
|
let pool: Option<PgPool> = match &config.pg_connection_url {
|
||||||
let metrics_db = Connection::open(&metrics_db_path).with_kind(ErrorKind::Database)?;
|
Some(url) => match PgPool::connect(url.as_str()).await {
|
||||||
metrics_db
|
Ok(pool) => Some(pool),
|
||||||
.execute_batch(
|
Err(_) => None,
|
||||||
"CREATE TABLE IF NOT EXISTS user_activity (
|
},
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
None => None,
|
||||||
created_at TEXT NOT NULL,
|
};
|
||||||
server_id TEXT NOT NULL,
|
|
||||||
arch TEXT,
|
|
||||||
os_version TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS package_request (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
pkg_id TEXT NOT NULL,
|
|
||||||
version TEXT
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_activity_created_at ON user_activity(created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_package_request_created_at ON package_request(created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_package_request_pkg_id ON package_request(pkg_id);",
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
let metrics_db = SyncMutex::new(metrics_db);
|
|
||||||
if config.registry_hostname.is_empty() {
|
if config.registry_hostname.is_empty() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("registry.context.missing-hostname")),
|
eyre!("{}", t!("registry.context.missing-hostname")),
|
||||||
@@ -165,7 +154,7 @@ impl RegistryContext {
|
|||||||
.build()
|
.build()
|
||||||
.with_kind(crate::ErrorKind::ParseUrl)?,
|
.with_kind(crate::ErrorKind::ParseUrl)?,
|
||||||
shutdown,
|
shutdown,
|
||||||
metrics_db,
|
pool,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ impl HardwareInfo {
|
|||||||
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
||||||
let s = ctx.db.peek().await.into_public().into_server_info();
|
let s = ctx.db.peek().await.into_public().into_server_info();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
arch: InternedString::intern(&*crate::ARCH),
|
arch: s.as_arch().de()?,
|
||||||
ram: s.as_ram().de()?,
|
ram: s.as_ram().de()?,
|
||||||
devices: Some(s.as_devices().de()?),
|
devices: Some(s.as_devices().de()?),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
@@ -106,8 +107,8 @@ pub async fn set_icon(
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CliSetIconParams {
|
pub struct CliSetIconParams {
|
||||||
#[arg(help = "help.arg.icon-source")]
|
#[arg(help = "help.arg.icon-path")]
|
||||||
pub icon: String,
|
pub icon: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cli_set_icon(
|
pub async fn cli_set_icon(
|
||||||
@@ -119,23 +120,7 @@ pub async fn cli_set_icon(
|
|||||||
..
|
..
|
||||||
}: HandlerArgs<CliContext, CliSetIconParams>,
|
}: HandlerArgs<CliContext, CliSetIconParams>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let data_url = if icon.starts_with("data:") {
|
let data_url = DataUrl::from_path(icon).await?;
|
||||||
icon.parse::<DataUrl<'static>>()
|
|
||||||
.with_kind(ErrorKind::ParseUrl)?
|
|
||||||
} else if icon.starts_with("https://") || icon.starts_with("http://") {
|
|
||||||
let res = ctx
|
|
||||||
.client
|
|
||||||
.get(&icon)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
DataUrl::from_response(res).await?
|
|
||||||
} else {
|
|
||||||
let path = icon
|
|
||||||
.strip_prefix("file://")
|
|
||||||
.unwrap_or(&icon);
|
|
||||||
DataUrl::from_path(path).await?
|
|
||||||
};
|
|
||||||
ctx.call_remote::<RegistryContext>(
|
ctx.call_remote::<RegistryContext>(
|
||||||
&parent_method.into_iter().chain(method).join("."),
|
&parent_method.into_iter().chain(method).join("."),
|
||||||
imbl_value::json!({
|
imbl_value::json!({
|
||||||
|
|||||||
25
core/src/registry/metrics-db/registry-sqlx-data.sh
Executable file
25
core/src/registry/metrics-db/registry-sqlx-data.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
mkdir $TMP_DIR/pgdata
|
||||||
|
docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres
|
||||||
|
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
ctr=0
|
||||||
|
until docker exec tmp_postgres psql -U postgres 2> /dev/null || [ $ctr -ge 5 ]; do
|
||||||
|
ctr=$[ctr + 1]
|
||||||
|
sleep 5;
|
||||||
|
done
|
||||||
|
|
||||||
|
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres)
|
||||||
|
|
||||||
|
cat "./registry_schema.sql" | docker exec -i tmp_postgres psql -U postgres -d postgres -f-
|
||||||
|
cd ../../..
|
||||||
|
DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace
|
||||||
|
echo "Subscript Complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
docker stop tmp_postgres
|
||||||
|
sudo rm -rf $TMP_DIR
|
||||||
828
core/src/registry/metrics-db/registry_schema.sql
Normal file
828
core/src/registry/metrics-db/registry_schema.sql
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Dumped from database version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
||||||
|
-- Dumped by pg_dump version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
SET default_tablespace = '';
|
||||||
|
|
||||||
|
SET default_table_access_method = heap;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.admin (
|
||||||
|
id character varying NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
pass_hash character varying NOT NULL,
|
||||||
|
deleted_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.admin OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.admin_pkgs (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
admin character varying NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.admin_pkgs OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.admin_pkgs_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.admin_pkgs_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.admin_pkgs_id_seq OWNED BY public.admin_pkgs.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.category (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
name character varying NOT NULL,
|
||||||
|
description character varying NOT NULL,
|
||||||
|
priority bigint DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.category OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.category_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.category_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.eos_hash (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
version character varying NOT NULL,
|
||||||
|
hash character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.eos_hash OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.eos_hash_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.eos_hash_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.eos_hash_id_seq OWNED BY public.eos_hash.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.error_log_record (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
epoch character varying NOT NULL,
|
||||||
|
commit_hash character varying NOT NULL,
|
||||||
|
source_file character varying NOT NULL,
|
||||||
|
line bigint NOT NULL,
|
||||||
|
target character varying NOT NULL,
|
||||||
|
level character varying NOT NULL,
|
||||||
|
message character varying NOT NULL,
|
||||||
|
incidents bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.error_log_record OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.error_log_record_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.error_log_record_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.error_log_record_id_seq OWNED BY public.error_log_record.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.metric (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
version character varying NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.metric OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.metric_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.metric_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.metric_id_seq OWNED BY public.metric.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: os_version; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.os_version (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone NOT NULL,
|
||||||
|
number character varying NOT NULL,
|
||||||
|
headline character varying NOT NULL,
|
||||||
|
release_notes character varying NOT NULL,
|
||||||
|
arch character varying
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.os_version OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: os_version_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.os_version_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.os_version_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: os_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.os_version_id_seq OWNED BY public.os_version.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: persistent_migration; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.persistent_migration (
|
||||||
|
id integer NOT NULL,
|
||||||
|
version integer NOT NULL,
|
||||||
|
label character varying,
|
||||||
|
"timestamp" timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.persistent_migration OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: persistent_migration_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.persistent_migration_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.persistent_migration_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: persistent_migration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.persistent_migration_id_seq OWNED BY public.persistent_migration.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.pkg_category (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
category_id bigint NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.pkg_category OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.pkg_dependency (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL,
|
||||||
|
pkg_version character varying NOT NULL,
|
||||||
|
dep_id character varying NOT NULL,
|
||||||
|
dep_version_range character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.pkg_dependency OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.pkg_dependency_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.pkg_dependency_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.pkg_dependency_id_seq OWNED BY public.pkg_dependency.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.pkg_record (
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
pkg_id character varying NOT NULL,
|
||||||
|
hidden boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.pkg_record OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: service_category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.service_category_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.service_category_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: service_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.service_category_id_seq OWNED BY public.pkg_category.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.upload (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
uploader character varying NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL,
|
||||||
|
pkg_version character varying NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.upload OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.upload_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.upload_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.upload_id_seq OWNED BY public.upload.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_activity; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.user_activity (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
server_id character varying NOT NULL,
|
||||||
|
os_version character varying,
|
||||||
|
arch character varying
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.user_activity OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.user_activity_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.user_activity_id_seq OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.user_activity_id_seq OWNED BY public.user_activity.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.version (
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
number character varying NOT NULL,
|
||||||
|
release_notes character varying NOT NULL,
|
||||||
|
os_version character varying NOT NULL,
|
||||||
|
pkg_id character varying NOT NULL,
|
||||||
|
title character varying NOT NULL,
|
||||||
|
desc_short character varying NOT NULL,
|
||||||
|
desc_long character varying NOT NULL,
|
||||||
|
icon_type character varying NOT NULL,
|
||||||
|
deprecated_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.version OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version_platform; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.version_platform (
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
pkg_id character varying NOT NULL,
|
||||||
|
version_number character varying NOT NULL,
|
||||||
|
arch character varying NOT NULL,
|
||||||
|
ram bigint,
|
||||||
|
device jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.version_platform OWNER TO alpha_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.admin_pkgs ALTER COLUMN id SET DEFAULT nextval('public.admin_pkgs_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.eos_hash ALTER COLUMN id SET DEFAULT nextval('public.eos_hash_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.error_log_record ALTER COLUMN id SET DEFAULT nextval('public.error_log_record_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.metric ALTER COLUMN id SET DEFAULT nextval('public.metric_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: os_version id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.os_version ALTER COLUMN id SET DEFAULT nextval('public.os_version_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: persistent_migration id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.persistent_migration ALTER COLUMN id SET DEFAULT nextval('public.persistent_migration_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_category ALTER COLUMN id SET DEFAULT nextval('public.service_category_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_dependency ALTER COLUMN id SET DEFAULT nextval('public.pkg_dependency_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upload ALTER COLUMN id SET DEFAULT nextval('public.upload_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_activity id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.user_activity ALTER COLUMN id SET DEFAULT nextval('public.user_activity_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.admin
|
||||||
|
ADD CONSTRAINT admin_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs admin_pkgs_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.admin_pkgs
|
||||||
|
ADD CONSTRAINT admin_pkgs_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.category
|
||||||
|
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash eos_hash_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.eos_hash
|
||||||
|
ADD CONSTRAINT eos_hash_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record error_log_record_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.error_log_record
|
||||||
|
ADD CONSTRAINT error_log_record_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric metric_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.metric
|
||||||
|
ADD CONSTRAINT metric_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: os_version os_version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.os_version
|
||||||
|
ADD CONSTRAINT os_version_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: persistent_migration persistent_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.persistent_migration
|
||||||
|
ADD CONSTRAINT persistent_migration_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category pkg_category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_category
|
||||||
|
ADD CONSTRAINT pkg_category_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency pkg_dependency_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_dependency
|
||||||
|
ADD CONSTRAINT pkg_dependency_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs unique_admin_pkg; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.admin_pkgs
|
||||||
|
ADD CONSTRAINT unique_admin_pkg UNIQUE (pkg_id, admin);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: error_log_record unique_log_record; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.error_log_record
|
||||||
|
ADD CONSTRAINT unique_log_record UNIQUE (epoch, commit_hash, source_file, line, target, level, message);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category unique_name; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.category
|
||||||
|
ADD CONSTRAINT unique_name UNIQUE (name);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category unique_pkg_category; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_category
|
||||||
|
ADD CONSTRAINT unique_pkg_category UNIQUE (pkg_id, category_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency unique_pkg_dep_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_dependency
|
||||||
|
ADD CONSTRAINT unique_pkg_dep_version UNIQUE (pkg_id, pkg_version, dep_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: eos_hash unique_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.eos_hash
|
||||||
|
ADD CONSTRAINT unique_version UNIQUE (version);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload upload_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upload
|
||||||
|
ADD CONSTRAINT upload_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_activity user_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.user_activity
|
||||||
|
ADD CONSTRAINT user_activity_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.version
|
||||||
|
ADD CONSTRAINT version_pkey PRIMARY KEY (pkg_id, number);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version_platform version_platform_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.version_platform
|
||||||
|
ADD CONSTRAINT version_platform_pkey PRIMARY KEY (pkg_id, version_number, arch);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: category_name_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX category_name_idx ON public.category USING btree (name);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_record_pkg_id_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX pkg_record_pkg_id_idx ON public.pkg_record USING btree (pkg_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version_number_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX version_number_idx ON public.version USING btree (number);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: admin_pkgs admin_pkgs_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.admin_pkgs
|
||||||
|
ADD CONSTRAINT admin_pkgs_admin_fkey FOREIGN KEY (admin) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: metric metric_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.metric
|
||||||
|
ADD CONSTRAINT metric_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category pkg_category_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_category
|
||||||
|
ADD CONSTRAINT pkg_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_category pkg_category_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_category
|
||||||
|
ADD CONSTRAINT pkg_category_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency pkg_dependency_dep_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_dependency
|
||||||
|
ADD CONSTRAINT pkg_dependency_dep_id_fkey FOREIGN KEY (dep_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pkg_dependency pkg_dependency_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pkg_dependency
|
||||||
|
ADD CONSTRAINT pkg_dependency_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload upload_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upload
|
||||||
|
ADD CONSTRAINT upload_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upload upload_uploader_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upload
|
||||||
|
ADD CONSTRAINT upload_uploader_fkey FOREIGN KEY (uploader) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version version_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.version
|
||||||
|
ADD CONSTRAINT version_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: version_platform version_platform_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.version_platform
|
||||||
|
ADD CONSTRAINT version_platform_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database dump complete
|
||||||
|
--
|
||||||
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
use clap::Parser;
|
|
||||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
use crate::context::CliContext;
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::registry::context::RegistryContext;
|
|
||||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
|
||||||
|
|
||||||
pub fn metrics_api<C: Context>() -> ParentHandler<C> {
|
|
||||||
ParentHandler::new()
|
|
||||||
.subcommand(
|
|
||||||
"summary",
|
|
||||||
from_fn_async(get_summary)
|
|
||||||
.with_metadata("admin", Value::Bool(true))
|
|
||||||
.with_display_serializable()
|
|
||||||
.with_custom_display_fn(|handle, result| display_summary(handle.params, result))
|
|
||||||
.with_about("about.get-metrics-summary")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"users",
|
|
||||||
from_fn_async(get_users)
|
|
||||||
.with_metadata("admin", Value::Bool(true))
|
|
||||||
.with_display_serializable()
|
|
||||||
.with_custom_display_fn(|handle, result| display_users(handle.params, result))
|
|
||||||
.with_about("about.get-metrics-users")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"downloads",
|
|
||||||
from_fn_async(get_downloads)
|
|
||||||
.with_metadata("admin", Value::Bool(true))
|
|
||||||
.with_display_serializable()
|
|
||||||
.with_custom_display_fn(|handle, result| {
|
|
||||||
display_downloads(handle.params, result)
|
|
||||||
})
|
|
||||||
.with_about("about.get-metrics-downloads")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- summary ---
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct CountEntry {
|
|
||||||
pub label: String,
|
|
||||||
pub count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct MetricsSummary {
|
|
||||||
pub total_checkins: u64,
|
|
||||||
pub unique_servers: u64,
|
|
||||||
pub total_package_requests: u64,
|
|
||||||
pub by_arch: Vec<CountEntry>,
|
|
||||||
pub by_os_version: Vec<CountEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_summary(ctx: RegistryContext) -> Result<MetricsSummary, Error> {
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
ctx.metrics_db.peek(|conn| {
|
|
||||||
let total_checkins: u64 = conn
|
|
||||||
.query_row("SELECT COUNT(*) FROM user_activity", [], |row| row.get(0))
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
let unique_servers: u64 = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT COUNT(DISTINCT server_id) FROM user_activity",
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
let total_package_requests: u64 = conn
|
|
||||||
.query_row("SELECT COUNT(*) FROM package_request", [], |row| row.get(0))
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
let by_arch = query_count_entries(
|
|
||||||
conn,
|
|
||||||
"SELECT COALESCE(arch, 'unknown'), COUNT(*) FROM user_activity GROUP BY arch ORDER BY COUNT(*) DESC",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let by_os_version = query_count_entries(
|
|
||||||
conn,
|
|
||||||
"SELECT COALESCE(os_version, 'unknown'), COUNT(*) FROM user_activity GROUP BY os_version ORDER BY COUNT(*) DESC",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(MetricsSummary {
|
|
||||||
total_checkins,
|
|
||||||
unique_servers,
|
|
||||||
total_package_requests,
|
|
||||||
by_arch,
|
|
||||||
by_os_version,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Unknown)?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_summary<T>(params: WithIoFormat<T>, summary: MetricsSummary) -> Result<(), Error> {
|
|
||||||
use prettytable::*;
|
|
||||||
|
|
||||||
if let Some(format) = params.format {
|
|
||||||
return display_serializable(format, summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Total check-ins: {}", summary.total_checkins);
|
|
||||||
println!("Unique servers: {}", summary.unique_servers);
|
|
||||||
println!("Total package requests: {}", summary.total_package_requests);
|
|
||||||
|
|
||||||
if !summary.by_arch.is_empty() {
|
|
||||||
println!();
|
|
||||||
let mut table = Table::new();
|
|
||||||
table.add_row(row![bc => "ARCHITECTURE", "COUNT"]);
|
|
||||||
for entry in &summary.by_arch {
|
|
||||||
table.add_row(row![&entry.label, entry.count]);
|
|
||||||
}
|
|
||||||
table.print_tty(false)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !summary.by_os_version.is_empty() {
|
|
||||||
println!();
|
|
||||||
let mut table = Table::new();
|
|
||||||
table.add_row(row![bc => "OS VERSION", "COUNT"]);
|
|
||||||
for entry in &summary.by_os_version {
|
|
||||||
table.add_row(row![&entry.label, entry.count]);
|
|
||||||
}
|
|
||||||
table.print_tty(false)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- users ---
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
|
||||||
#[group(skip)]
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct GetUsersParams {
|
|
||||||
/// Start of time range (RFC 3339)
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-after")]
|
|
||||||
pub after: Option<String>,
|
|
||||||
/// End of time range (RFC 3339)
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-before")]
|
|
||||||
pub before: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct UsersResponse {
|
|
||||||
pub unique_servers: u64,
|
|
||||||
pub total_checkins: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_users(
|
|
||||||
ctx: RegistryContext,
|
|
||||||
GetUsersParams { after, before }: GetUsersParams,
|
|
||||||
) -> Result<UsersResponse, Error> {
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
ctx.metrics_db.peek(|conn| {
|
|
||||||
let (where_clause, params) = time_range_where(&after, &before);
|
|
||||||
|
|
||||||
let unique_servers: u64 = conn
|
|
||||||
.query_row(
|
|
||||||
&format!("SELECT COUNT(DISTINCT server_id) FROM user_activity{where_clause}"),
|
|
||||||
rusqlite::params_from_iter(¶ms),
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
let total_checkins: u64 = conn
|
|
||||||
.query_row(
|
|
||||||
&format!("SELECT COUNT(*) FROM user_activity{where_clause}"),
|
|
||||||
rusqlite::params_from_iter(¶ms),
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
Ok(UsersResponse {
|
|
||||||
unique_servers,
|
|
||||||
total_checkins,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Unknown)?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_users<T>(params: WithIoFormat<T>, response: UsersResponse) -> Result<(), Error> {
|
|
||||||
if let Some(format) = params.format {
|
|
||||||
return display_serializable(format, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Unique servers: {}", response.unique_servers);
|
|
||||||
println!("Total check-ins: {}", response.total_checkins);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- downloads ---
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
|
||||||
#[group(skip)]
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct GetDownloadsParams {
|
|
||||||
/// Filter by package ID
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-pkg-id")]
|
|
||||||
pub pkg_id: Option<String>,
|
|
||||||
/// Filter by version
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-version")]
|
|
||||||
pub version: Option<String>,
|
|
||||||
/// Start of time range (RFC 3339)
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-after")]
|
|
||||||
pub after: Option<String>,
|
|
||||||
/// End of time range (RFC 3339)
|
|
||||||
#[ts(type = "string | null")]
|
|
||||||
#[arg(long, help = "help.arg.metrics-before")]
|
|
||||||
pub before: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct PackageVersionCount {
|
|
||||||
pub pkg_id: String,
|
|
||||||
pub version: String,
|
|
||||||
pub count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct DownloadsResponse {
|
|
||||||
pub total_requests: u64,
|
|
||||||
pub by_package: Vec<CountEntry>,
|
|
||||||
pub by_package_version: Vec<PackageVersionCount>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_downloads(
|
|
||||||
ctx: RegistryContext,
|
|
||||||
GetDownloadsParams {
|
|
||||||
pkg_id,
|
|
||||||
version,
|
|
||||||
after,
|
|
||||||
before,
|
|
||||||
}: GetDownloadsParams,
|
|
||||||
) -> Result<DownloadsResponse, Error> {
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
ctx.metrics_db.peek(|conn| {
|
|
||||||
let (where_clause, params) =
|
|
||||||
downloads_where(&pkg_id, &version, &after, &before);
|
|
||||||
|
|
||||||
let total_requests: u64 = conn
|
|
||||||
.query_row(
|
|
||||||
&format!("SELECT COUNT(*) FROM package_request{where_clause}"),
|
|
||||||
rusqlite::params_from_iter(¶ms),
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
let by_package = query_count_entries_with_params(
|
|
||||||
conn,
|
|
||||||
&format!(
|
|
||||||
"SELECT pkg_id, COUNT(*) FROM package_request{where_clause} GROUP BY pkg_id ORDER BY COUNT(*) DESC"
|
|
||||||
),
|
|
||||||
¶ms,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let by_package_version = {
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(&format!(
|
|
||||||
"SELECT pkg_id, COALESCE(version, 'unknown'), COUNT(*) FROM package_request{where_clause} GROUP BY pkg_id, version ORDER BY pkg_id, COUNT(*) DESC"
|
|
||||||
))
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
let rows = stmt
|
|
||||||
.query_map(rusqlite::params_from_iter(¶ms), |row| {
|
|
||||||
Ok(PackageVersionCount {
|
|
||||||
pkg_id: row.get(0)?,
|
|
||||||
version: row.get(1)?,
|
|
||||||
count: row.get(2)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
rows.map(|r| r.with_kind(ErrorKind::Database))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(DownloadsResponse {
|
|
||||||
total_requests,
|
|
||||||
by_package,
|
|
||||||
by_package_version,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Unknown)?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_downloads(
|
|
||||||
params: WithIoFormat<GetDownloadsParams>,
|
|
||||||
response: DownloadsResponse,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
use prettytable::*;
|
|
||||||
|
|
||||||
if let Some(format) = params.format {
|
|
||||||
return display_serializable(format, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Total requests: {}", response.total_requests);
|
|
||||||
|
|
||||||
if !response.by_package.is_empty() {
|
|
||||||
println!();
|
|
||||||
let mut table = Table::new();
|
|
||||||
table.add_row(row![bc => "PACKAGE", "COUNT"]);
|
|
||||||
for entry in &response.by_package {
|
|
||||||
table.add_row(row![&entry.label, entry.count]);
|
|
||||||
}
|
|
||||||
table.print_tty(false)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.by_package_version.is_empty() {
|
|
||||||
println!();
|
|
||||||
let mut table = Table::new();
|
|
||||||
table.add_row(row![bc => "PACKAGE", "VERSION", "COUNT"]);
|
|
||||||
for entry in &response.by_package_version {
|
|
||||||
table.add_row(row![&entry.pkg_id, &entry.version, entry.count]);
|
|
||||||
}
|
|
||||||
table.print_tty(false)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ---
|
|
||||||
|
|
||||||
fn query_count_entries(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
sql: &str,
|
|
||||||
) -> Result<Vec<CountEntry>, Error> {
|
|
||||||
query_count_entries_with_params(conn, sql, &[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_count_entries_with_params(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
sql: &str,
|
|
||||||
params: &[String],
|
|
||||||
) -> Result<Vec<CountEntry>, Error> {
|
|
||||||
let mut stmt = conn.prepare(sql).with_kind(ErrorKind::Database)?;
|
|
||||||
let rows = stmt
|
|
||||||
.query_map(rusqlite::params_from_iter(params), |row| {
|
|
||||||
Ok(CountEntry {
|
|
||||||
label: row.get(0)?,
|
|
||||||
count: row.get(1)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
rows.map(|r| r.with_kind(ErrorKind::Database))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn time_range_where(
|
|
||||||
after: &Option<String>,
|
|
||||||
before: &Option<String>,
|
|
||||||
) -> (String, Vec<String>) {
|
|
||||||
let mut conditions = Vec::new();
|
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(a) = after {
|
|
||||||
params.push(a.clone());
|
|
||||||
conditions.push(format!("created_at >= ?{}", params.len()));
|
|
||||||
}
|
|
||||||
if let Some(b) = before {
|
|
||||||
params.push(b.clone());
|
|
||||||
conditions.push(format!("created_at < ?{}", params.len()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let clause = if conditions.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" WHERE {}", conditions.join(" AND "))
|
|
||||||
};
|
|
||||||
|
|
||||||
(clause, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn downloads_where(
|
|
||||||
pkg_id: &Option<String>,
|
|
||||||
version: &Option<String>,
|
|
||||||
after: &Option<String>,
|
|
||||||
before: &Option<String>,
|
|
||||||
) -> (String, Vec<String>) {
|
|
||||||
let mut conditions = Vec::new();
|
|
||||||
let mut params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(id) = pkg_id {
|
|
||||||
params.push(id.clone());
|
|
||||||
conditions.push(format!("pkg_id = ?{}", params.len()));
|
|
||||||
}
|
|
||||||
if let Some(v) = version {
|
|
||||||
params.push(v.clone());
|
|
||||||
conditions.push(format!("version = ?{}", params.len()));
|
|
||||||
}
|
|
||||||
if let Some(a) = after {
|
|
||||||
params.push(a.clone());
|
|
||||||
conditions.push(format!("created_at >= ?{}", params.len()));
|
|
||||||
}
|
|
||||||
if let Some(b) = before {
|
|
||||||
params.push(b.clone());
|
|
||||||
conditions.push(format!("created_at < ?{}", params.len()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let clause = if conditions.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" WHERE {}", conditions.join(" AND "))
|
|
||||||
};
|
|
||||||
|
|
||||||
(clause, params)
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,6 @@ pub mod context;
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod device_info;
|
pub mod device_info;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod metrics;
|
|
||||||
mod migrations;
|
mod migrations;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
pub mod package;
|
pub mod package;
|
||||||
@@ -101,10 +100,6 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
|
|||||||
"db",
|
"db",
|
||||||
db::db_api::<C>().with_about("about.commands-registry-db"),
|
db::db_api::<C>().with_about("about.commands-registry-db"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
|
||||||
"metrics",
|
|
||||||
metrics::metrics_api::<C>().with_about("about.commands-registry-metrics"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn registry_router(ctx: RegistryContext) -> Router {
|
pub fn registry_router(ctx: RegistryContext) -> Router {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ pub const SIG_CONTEXT: &str = "startos";
|
|||||||
|
|
||||||
pub mod asset;
|
pub mod asset;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod promote;
|
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
pub fn os_api<C: Context>() -> ParentHandler<C> {
|
pub fn os_api<C: Context>() -> ParentHandler<C> {
|
||||||
@@ -29,10 +28,4 @@ pub fn os_api<C: Context>() -> ParentHandler<C> {
|
|||||||
"version",
|
"version",
|
||||||
version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
|
version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
|
||||||
"promote",
|
|
||||||
from_fn_async(promote::cli_os_promote)
|
|
||||||
.no_display()
|
|
||||||
.with_about("about.promote-os-registry"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
use clap::Parser;
|
|
||||||
use exver::Version;
|
|
||||||
use imbl_value::InternedString;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::context::CliContext;
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::registry::os::SIG_CONTEXT;
|
|
||||||
use crate::registry::os::index::OsIndex;
|
|
||||||
use crate::registry::package::promote::{call_registry, resolve_registry_url};
|
|
||||||
use crate::sign::commitment::blake3::Blake3Commitment;
|
|
||||||
use crate::sign::ed25519::Ed25519;
|
|
||||||
use crate::sign::{AnySignature, SignatureScheme};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
|
||||||
#[group(skip)]
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CliOsPromoteParams {
|
|
||||||
#[arg(long, help = "help.arg.from-registry-url")]
|
|
||||||
pub from: Option<Url>,
|
|
||||||
#[arg(long, help = "help.arg.to-registry-url")]
|
|
||||||
pub to: Option<Url>,
|
|
||||||
#[arg(help = "help.arg.os-version")]
|
|
||||||
pub version: Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn cli_os_promote(
|
|
||||||
ctx: CliContext,
|
|
||||||
CliOsPromoteParams { from, to, version }: CliOsPromoteParams,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if from.is_none() && to.is_none() {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("{}", t!("registry.os.promote.need-from-or-to")),
|
|
||||||
ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let from_url = resolve_registry_url(from.as_ref(), &ctx)?;
|
|
||||||
let to_url = resolve_registry_url(to.as_ref(), &ctx)?;
|
|
||||||
|
|
||||||
// Fetch OS index from source registry
|
|
||||||
let res: Value = call_registry(&ctx, from_url, "os.index", imbl_value::json!({})).await?;
|
|
||||||
let os_index: OsIndex = from_value(res)?;
|
|
||||||
|
|
||||||
// Find the target version
|
|
||||||
let version_info = os_index
|
|
||||||
.versions
|
|
||||||
.0
|
|
||||||
.get(&version)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::new(
|
|
||||||
eyre!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"registry.os.promote.version-not-found",
|
|
||||||
version = &version
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ErrorKind::NotFound,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Add the version to the target registry
|
|
||||||
call_registry(
|
|
||||||
&ctx,
|
|
||||||
to_url.clone(),
|
|
||||||
"os.version.add",
|
|
||||||
imbl_value::json!({
|
|
||||||
"version": &version,
|
|
||||||
"headline": &version_info.headline,
|
|
||||||
"releaseNotes": &version_info.release_notes,
|
|
||||||
"sourceVersion": &version_info.source_version,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Promote all assets for each type and platform
|
|
||||||
promote_assets(&ctx, &to_url, &version, &version_info.iso, "os.asset.add.iso").await?;
|
|
||||||
promote_assets(&ctx, &to_url, &version, &version_info.squashfs, "os.asset.add.squashfs").await?;
|
|
||||||
promote_assets(&ctx, &to_url, &version, &version_info.img, "os.asset.add.img").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn promote_assets(
|
|
||||||
ctx: &CliContext,
|
|
||||||
to_url: &Url,
|
|
||||||
version: &Version,
|
|
||||||
assets: &std::collections::BTreeMap<InternedString, crate::registry::asset::RegistryAsset<Blake3Commitment>>,
|
|
||||||
method: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
for (platform, asset) in assets {
|
|
||||||
let commitment = &asset.commitment;
|
|
||||||
let signature =
|
|
||||||
AnySignature::Ed25519(Ed25519.sign_commitment(ctx.developer_key()?, commitment, SIG_CONTEXT)?);
|
|
||||||
|
|
||||||
call_registry(
|
|
||||||
ctx,
|
|
||||||
to_url.clone(),
|
|
||||||
method,
|
|
||||||
imbl_value::json!({
|
|
||||||
"version": version,
|
|
||||||
"platform": platform,
|
|
||||||
"url": &asset.urls[0],
|
|
||||||
"signature": signature,
|
|
||||||
"commitment": commitment,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use exver::{Version, VersionRange};
|
use exver::{Version, VersionRange};
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use rusqlite::params;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
@@ -161,6 +159,33 @@ pub struct GetOsVersionParams {
|
|||||||
pub device_info: Option<DeviceInfo>,
|
pub device_info: Option<DeviceInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PgDateTime(DateTime<Utc>);
|
||||||
|
impl sqlx::Type<sqlx::Postgres> for PgDateTime {
|
||||||
|
fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
|
||||||
|
sqlx::postgres::PgTypeInfo::with_oid(sqlx::postgres::types::Oid(1184))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl sqlx::Encode<'_, sqlx::Postgres> for PgDateTime {
|
||||||
|
fn encode_by_ref(
|
||||||
|
&self,
|
||||||
|
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'_>,
|
||||||
|
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
||||||
|
fn postgres_epoch_datetime() -> NaiveDateTime {
|
||||||
|
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||||
|
.expect("expected 2000-01-01 to be a valid NaiveDate")
|
||||||
|
.and_hms_opt(0, 0, 0)
|
||||||
|
.expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime")
|
||||||
|
}
|
||||||
|
let micros = (self.0.naive_utc() - postgres_epoch_datetime())
|
||||||
|
.num_microseconds()
|
||||||
|
.ok_or_else(|| format!("NaiveDateTime out of range for Postgres: {:?}", self.0))?;
|
||||||
|
micros.encode(buf)
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> usize {
|
||||||
|
std::mem::size_of::<i64>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_version(
|
pub async fn get_version(
|
||||||
ctx: RegistryContext,
|
ctx: RegistryContext,
|
||||||
GetOsVersionParams {
|
GetOsVersionParams {
|
||||||
@@ -174,28 +199,16 @@ pub async fn get_version(
|
|||||||
{
|
{
|
||||||
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
|
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
|
||||||
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
|
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
|
||||||
if let (Some(server_id), Some(arch)) = (server_id, &platform) {
|
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
|
||||||
const MAX_SERVER_ID_LEN: usize = 256;
|
let created_at = Utc::now();
|
||||||
if server_id.len() <= MAX_SERVER_ID_LEN {
|
|
||||||
let created_at = Utc::now().to_rfc3339();
|
sqlx::query("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)")
|
||||||
let arch = arch.to_string();
|
.bind(PgDateTime(created_at))
|
||||||
let os_version = source.as_ref().map(|v| v.to_string());
|
.bind(server_id)
|
||||||
let ctx = ctx.clone();
|
.bind(&**arch)
|
||||||
tokio::task::spawn_blocking(move || {
|
.execute(pool)
|
||||||
ctx.metrics_db.mutate(|conn| {
|
.await
|
||||||
if let Err(e) = conn.execute(
|
.with_kind(ErrorKind::Database)?;
|
||||||
concat!(
|
|
||||||
"INSERT INTO user_activity ",
|
|
||||||
"(created_at, server_id, arch, os_version) ",
|
|
||||||
"VALUES (?1, ?2, ?3, ?4)"
|
|
||||||
),
|
|
||||||
params![created_at, server_id, arch, os_version],
|
|
||||||
) {
|
|
||||||
warn!("failed to record user activity metric: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let target = target.unwrap_or(VersionRange::Any);
|
let target = target.unwrap_or(VersionRange::Any);
|
||||||
let mut res = to_value::<BTreeMap<Version, OsVersionInfo>>(
|
let mut res = to_value::<BTreeMap<Version, OsVersionInfo>>(
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ use itertools::Itertools;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
use rusqlite::params;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::PackageId;
|
use crate::PackageId;
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -237,25 +233,6 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(id) = ¶ms.id {
|
if let Some(id) = ¶ms.id {
|
||||||
if params.target_version.is_some() {
|
|
||||||
let created_at = Utc::now().to_rfc3339();
|
|
||||||
let pkg_id = id.to_string();
|
|
||||||
let version = best
|
|
||||||
.get(id)
|
|
||||||
.and_then(|b| b.keys().last())
|
|
||||||
.map(|v| v.to_string());
|
|
||||||
let ctx = ctx.clone();
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
ctx.metrics_db.mutate(|conn| {
|
|
||||||
if let Err(e) = conn.execute(
|
|
||||||
"INSERT INTO package_request (created_at, pkg_id, version) VALUES (?1, ?2, ?3)",
|
|
||||||
params![created_at, pkg_id, version],
|
|
||||||
) {
|
|
||||||
warn!("failed to record package request metric: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let categories = peek
|
let categories = peek
|
||||||
.as_index()
|
.as_index()
|
||||||
.as_package()
|
.as_package()
|
||||||
@@ -638,7 +615,6 @@ fn check_matching_info_short() {
|
|||||||
sdk_version: None,
|
sdk_version: None,
|
||||||
hardware_acceleration: false,
|
hardware_acceleration: false,
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
satisfies: BTreeSet::new(),
|
|
||||||
},
|
},
|
||||||
icon: DataUrl::from_vec("image/png", vec![]),
|
icon: DataUrl::from_vec("image/png", vec![]),
|
||||||
dependency_metadata: BTreeMap::new(),
|
dependency_metadata: BTreeMap::new(),
|
||||||
|
|||||||
@@ -110,8 +110,6 @@ pub struct PackageMetadata {
|
|||||||
pub hardware_acceleration: bool,
|
pub hardware_acceleration: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub plugins: BTreeSet<PluginId>,
|
pub plugins: BTreeSet<PluginId>,
|
||||||
#[serde(default)]
|
|
||||||
pub satisfies: BTreeSet<VersionString>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ pub mod add;
|
|||||||
pub mod category;
|
pub mod category;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod promote;
|
|
||||||
pub mod signer;
|
pub mod signer;
|
||||||
|
|
||||||
pub fn package_api<C: Context>() -> ParentHandler<C> {
|
pub fn package_api<C: Context>() -> ParentHandler<C> {
|
||||||
@@ -99,12 +98,6 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.download-s9pk"),
|
.with_about("about.download-s9pk"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
|
||||||
"promote",
|
|
||||||
from_fn_async(promote::cli_promote)
|
|
||||||
.no_display()
|
|
||||||
.with_about("about.promote-package-registry"),
|
|
||||||
)
|
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"category",
|
"category",
|
||||||
category::category_api::<C>().with_about("about.update-categories-registry"),
|
category::category_api::<C>().with_about("about.update-categories-registry"),
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
use clap::Parser;
|
|
||||||
use http::HeaderMap;
|
|
||||||
use imbl_value::InternedString;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::PackageId;
|
|
||||||
use crate::context::CliContext;
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::registry::package::get::{GetPackageResponseFull, PackageDetailLevel};
|
|
||||||
use crate::s9pk::v2::SIG_CONTEXT;
|
|
||||||
use crate::sign::ed25519::Ed25519;
|
|
||||||
use crate::sign::{AnySignature, SignatureScheme};
|
|
||||||
use crate::util::VersionString;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
|
||||||
#[group(skip)]
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CliPromoteParams {
|
|
||||||
#[arg(long, help = "help.arg.from-registry-url")]
|
|
||||||
pub from: Option<Url>,
|
|
||||||
#[arg(long, help = "help.arg.to-registry-url")]
|
|
||||||
pub to: Option<Url>,
|
|
||||||
#[arg(help = "help.arg.package-id")]
|
|
||||||
pub id: PackageId,
|
|
||||||
#[arg(help = "help.arg.package-version")]
|
|
||||||
pub version: VersionString,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn registry_rpc_url(url: &Url) -> Result<Url, Error> {
|
|
||||||
let mut url = url.clone();
|
|
||||||
url.path_segments_mut()
|
|
||||||
.map_err(|_| eyre!("Url cannot be base"))
|
|
||||||
.with_kind(ErrorKind::ParseUrl)?
|
|
||||||
.push("rpc")
|
|
||||||
.push("v0");
|
|
||||||
Ok(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_registry_url(explicit: Option<&Url>, ctx: &CliContext) -> Result<Url, Error> {
|
|
||||||
if let Some(url) = explicit {
|
|
||||||
registry_rpc_url(url)
|
|
||||||
} else if let Some(url) = &ctx.registry_url {
|
|
||||||
Ok(url.clone())
|
|
||||||
} else {
|
|
||||||
Err(Error::new(
|
|
||||||
eyre!("{}", t!("registry.context.registry-required")),
|
|
||||||
ErrorKind::InvalidRequest,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn call_registry(
|
|
||||||
ctx: &CliContext,
|
|
||||||
url: Url,
|
|
||||||
method: &str,
|
|
||||||
params: Value,
|
|
||||||
) -> Result<Value, Error> {
|
|
||||||
let sig_context = url.host().as_ref().map(InternedString::from_display);
|
|
||||||
crate::middleware::auth::signature::call_remote(
|
|
||||||
ctx,
|
|
||||||
url,
|
|
||||||
HeaderMap::new(),
|
|
||||||
sig_context.as_deref(),
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn cli_promote(
|
|
||||||
ctx: CliContext,
|
|
||||||
CliPromoteParams {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
id,
|
|
||||||
version,
|
|
||||||
}: CliPromoteParams,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if from.is_none() && to.is_none() {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("{}", t!("registry.package.promote.need-from-or-to")),
|
|
||||||
ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let from_url = resolve_registry_url(from.as_ref(), &ctx)?;
|
|
||||||
let to_url = resolve_registry_url(to.as_ref(), &ctx)?;
|
|
||||||
|
|
||||||
// Fetch package info from source registry
|
|
||||||
let res: Value = call_registry(
|
|
||||||
&ctx,
|
|
||||||
from_url,
|
|
||||||
"package.get",
|
|
||||||
imbl_value::json!({
|
|
||||||
"id": &id,
|
|
||||||
"otherVersions": PackageDetailLevel::Full,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response: GetPackageResponseFull = from_value(res)?;
|
|
||||||
|
|
||||||
// Find the target version
|
|
||||||
let version_info = response
|
|
||||||
.best
|
|
||||||
.get(&version)
|
|
||||||
.or_else(|| response.other_versions.get(&version))
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::new(
|
|
||||||
eyre!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"registry.package.promote.version-not-found",
|
|
||||||
id = &id,
|
|
||||||
version = &version
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ErrorKind::NotFound,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Promote each s9pk variant to the target registry
|
|
||||||
for (_, asset) in &version_info.s9pks {
|
|
||||||
let commitment = &asset.commitment;
|
|
||||||
let signature = Ed25519.sign_commitment(ctx.developer_key()?, commitment, SIG_CONTEXT)?;
|
|
||||||
|
|
||||||
call_registry(
|
|
||||||
&ctx,
|
|
||||||
to_url.clone(),
|
|
||||||
"package.add",
|
|
||||||
imbl_value::json!({
|
|
||||||
"urls": &asset.urls,
|
|
||||||
"signature": AnySignature::Ed25519(signature),
|
|
||||||
"commitment": commitment,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -194,15 +194,10 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
if &*value.id == "nostr" {
|
if &*value.id == "nostr" {
|
||||||
value.id = "nostr-rs-relay".parse()?;
|
value.id = "nostr-rs-relay".parse()?;
|
||||||
}
|
}
|
||||||
if &*value.id == "ghost" {
|
|
||||||
value.id = "ghost-legacy".parse()?;
|
|
||||||
}
|
|
||||||
if &*value.id == "synapse" {
|
|
||||||
value.id = "synapse-legacy".parse()?;
|
|
||||||
}
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
version: version.into(),
|
version: version.into(),
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
can_migrate_from: VersionRange::any(),
|
can_migrate_from: VersionRange::any(),
|
||||||
can_migrate_to: VersionRange::none(),
|
can_migrate_to: VersionRange::none(),
|
||||||
metadata: PackageMetadata {
|
metadata: PackageMetadata {
|
||||||
@@ -224,7 +219,6 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
PackageProcedure::Script(_) => false,
|
PackageProcedure::Script(_) => false,
|
||||||
},
|
},
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
satisfies: BTreeSet::new(),
|
|
||||||
},
|
},
|
||||||
images: BTreeMap::new(),
|
images: BTreeMap::new(),
|
||||||
volumes: value
|
volumes: value
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub(crate) fn current_version() -> Version {
|
|||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
pub id: PackageId,
|
pub id: PackageId,
|
||||||
pub version: VersionString,
|
pub version: VersionString,
|
||||||
|
pub satisfies: BTreeSet<VersionString>,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub can_migrate_to: VersionRange,
|
pub can_migrate_to: VersionRange,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ pub async fn check_dependencies(
|
|||||||
};
|
};
|
||||||
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
||||||
let installed_version = manifest.as_version().de()?.into_version();
|
let installed_version = manifest.as_version().de()?.into_version();
|
||||||
let satisfies = manifest.as_metadata().as_satisfies().de()?;
|
let satisfies = manifest.as_satisfies().de()?;
|
||||||
let installed_version = Some(installed_version.clone().into());
|
let installed_version = Some(installed_version.clone().into());
|
||||||
let is_running = package
|
let is_running = package
|
||||||
.as_status_info()
|
.as_status_info()
|
||||||
|
|||||||
@@ -134,9 +134,8 @@ pub async fn list_service_interfaces(
|
|||||||
.expect("valid json pointer");
|
.expect("valid json pointer");
|
||||||
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
||||||
|
|
||||||
let Some(res) = from_value(watch.peek_and_mark_seen()?)? else {
|
let res = imbl_value::from_value(watch.peek_and_mark_seen()?)
|
||||||
return Ok(BTreeMap::new());
|
.unwrap_or_default();
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(callback) = callback {
|
if let Some(callback) = callback {
|
||||||
let callback = callback.register(&context.seed.persistent_container);
|
let callback = callback.register(&context.seed.persistent_container);
|
||||||
@@ -175,7 +174,9 @@ pub async fn clear_service_interfaces(
|
|||||||
.as_idx_mut(&package_id)
|
.as_idx_mut(&package_id)
|
||||||
.or_not_found(&package_id)?
|
.or_not_found(&package_id)?
|
||||||
.as_service_interfaces_mut()
|
.as_service_interfaces_mut()
|
||||||
.mutate(|s| Ok(s.retain(|id, _| except.contains(id))))
|
.mutate(|s| {
|
||||||
|
Ok(s.retain(|id, _| except.contains(id)))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::bins::set_locale;
|
use crate::bins::set_locale;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::public::RestartReason;
|
|
||||||
use crate::disk::util::{get_available, get_used};
|
use crate::disk::util::{get_available, get_used};
|
||||||
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -352,9 +351,10 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let server_info = db.as_public_mut().as_server_info_mut();
|
db.as_public_mut()
|
||||||
server_info.as_kiosk_mut().ser(&Some(true))?;
|
.as_server_info_mut()
|
||||||
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
.as_kiosk_mut()
|
||||||
|
.ser(&Some(true))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -369,9 +369,10 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let server_info = db.as_public_mut().as_server_info_mut();
|
db.as_public_mut()
|
||||||
server_info.as_kiosk_mut().ser(&Some(false))?;
|
.as_server_info_mut()
|
||||||
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
.as_kiosk_mut()
|
||||||
|
.ser(&Some(false))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -1366,11 +1367,10 @@ pub async fn set_language(
|
|||||||
save_language(&*language).await?;
|
save_language(&*language).await?;
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let server_info = db.as_public_mut().as_server_info_mut();
|
db.as_public_mut()
|
||||||
server_info
|
.as_server_info_mut()
|
||||||
.as_language_mut()
|
.as_language_mut()
|
||||||
.ser(&Some(language.clone()))?;
|
.ser(&Some(language.clone()))
|
||||||
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Language))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::PLATFORM;
|
use crate::PLATFORM;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::public::RestartReason;
|
|
||||||
use crate::notifications::{NotificationLevel, notify};
|
use crate::notifications::{NotificationLevel, notify};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::{
|
use crate::progress::{
|
||||||
@@ -82,9 +81,8 @@ pub async fn update_system(
|
|||||||
.into_public()
|
.into_public()
|
||||||
.into_server_info()
|
.into_server_info()
|
||||||
.into_status_info()
|
.into_status_info()
|
||||||
.into_restart()
|
.into_updated()
|
||||||
.de()?
|
.de()?
|
||||||
== Some(RestartReason::Update)
|
|
||||||
{
|
{
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
@@ -283,18 +281,10 @@ async fn maybe_do_update(
|
|||||||
|
|
||||||
let start_progress = progress.snapshot();
|
let start_progress = progress.snapshot();
|
||||||
|
|
||||||
ctx.db
|
let status = ctx
|
||||||
|
.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let server_info = db.as_public_mut().as_server_info_mut();
|
let mut status = peeked.as_public().as_server_info().as_status_info().de()?;
|
||||||
|
|
||||||
if server_info.as_status_info().as_restart().de()?.is_some() {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
|
||||||
crate::ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut status = server_info.as_status_info().de()?;
|
|
||||||
if status.update_progress.is_some() {
|
if status.update_progress.is_some() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updating")),
|
eyre!("{}", t!("update.already-updating")),
|
||||||
@@ -303,12 +293,22 @@ async fn maybe_do_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
status.update_progress = Some(start_progress);
|
status.update_progress = Some(start_progress);
|
||||||
server_info.as_status_info_mut().ser(&status)?;
|
db.as_public_mut()
|
||||||
Ok(())
|
.as_server_info_mut()
|
||||||
|
.as_status_info_mut()
|
||||||
|
.ser(&status)?;
|
||||||
|
Ok(status)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
|
if status.updated {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
|
crate::ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
||||||
ctx.db.clone(),
|
ctx.db.clone(),
|
||||||
|db| {
|
|db| {
|
||||||
@@ -338,15 +338,10 @@ async fn maybe_do_update(
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let server_info = db.as_public_mut().as_server_info_mut();
|
let status_info =
|
||||||
server_info
|
db.as_public_mut().as_server_info_mut().as_status_info_mut();
|
||||||
.as_status_info_mut()
|
status_info.as_update_progress_mut().ser(&None)?;
|
||||||
.as_update_progress_mut()
|
status_info.as_updated_mut().ser(&true)
|
||||||
.ser(&None)?;
|
|
||||||
server_info
|
|
||||||
.as_status_info_mut()
|
|
||||||
.as_restart_mut()
|
|
||||||
.ser(&Some(RestartReason::Update))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -63,9 +63,8 @@ mod v0_4_0_alpha_20;
|
|||||||
mod v0_4_0_alpha_21;
|
mod v0_4_0_alpha_21;
|
||||||
mod v0_4_0_alpha_22;
|
mod v0_4_0_alpha_22;
|
||||||
mod v0_4_0_alpha_23;
|
mod v0_4_0_alpha_23;
|
||||||
mod v0_4_0_beta_0;
|
|
||||||
|
|
||||||
pub type Current = v0_4_0_beta_0::Version; // VERSION_BUMP
|
pub type Current = v0_4_0_alpha_23::Version; // VERSION_BUMP
|
||||||
|
|
||||||
impl Current {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[instrument(skip(self, db))]
|
||||||
@@ -196,8 +195,7 @@ enum Version {
|
|||||||
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>),
|
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>),
|
||||||
V0_4_0_alpha_21(Wrapper<v0_4_0_alpha_21::Version>),
|
V0_4_0_alpha_21(Wrapper<v0_4_0_alpha_21::Version>),
|
||||||
V0_4_0_alpha_22(Wrapper<v0_4_0_alpha_22::Version>),
|
V0_4_0_alpha_22(Wrapper<v0_4_0_alpha_22::Version>),
|
||||||
V0_4_0_alpha_23(Wrapper<v0_4_0_alpha_23::Version>),
|
V0_4_0_alpha_23(Wrapper<v0_4_0_alpha_23::Version>), // VERSION_BUMP
|
||||||
V0_4_0_beta_0(Wrapper<v0_4_0_beta_0::Version>), // VERSION_BUMP
|
|
||||||
Other(exver::Version),
|
Other(exver::Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +261,7 @@ impl Version {
|
|||||||
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_22(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_22(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_23(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_23(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||||
Self::V0_4_0_beta_0(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
|
||||||
Self::Other(v) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
eyre!("unknown version {v}"),
|
||||||
@@ -322,8 +319,7 @@ impl Version {
|
|||||||
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_22(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_22(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_23(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_23(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||||
Version::V0_4_0_beta_0(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
|
||||||
Version::Other(x) => x.clone(),
|
Version::Other(x) => x.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,22 +40,6 @@ lazy_static::lazy_static! {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All pre-0.4.0 StartOS images were initialized with the en_GB.UTF-8 locale.
|
|
||||||
/// The current trixie image does not ship it. Without it PostgreSQL starts
|
|
||||||
/// but refuses connections, breaking the migration.
|
|
||||||
async fn ensure_en_gb_locale() -> Result<(), Error> {
|
|
||||||
Command::new("localedef")
|
|
||||||
.arg("-i")
|
|
||||||
.arg("en_GB")
|
|
||||||
.arg("-c")
|
|
||||||
.arg("-f")
|
|
||||||
.arg("UTF-8")
|
|
||||||
.arg("en_GB.UTF-8")
|
|
||||||
.invoke(crate::ErrorKind::Database)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
||||||
let db_dir = datadir.as_ref().join("main/postgresql");
|
let db_dir = datadir.as_ref().join("main/postgresql");
|
||||||
@@ -107,12 +91,6 @@ async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
|||||||
|
|
||||||
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
||||||
|
|
||||||
// The cluster may have been created with a locale not present on the
|
|
||||||
// current image (e.g. en_GB.UTF-8 on a server that predates the trixie
|
|
||||||
// image). Detect and generate it before starting PostgreSQL, otherwise
|
|
||||||
// PG will start but refuse connections.
|
|
||||||
ensure_en_gb_locale().await?;
|
|
||||||
|
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("start")
|
.arg("start")
|
||||||
.arg(format!("postgresql@{pg_version}-main.service"))
|
.arg(format!("postgresql@{pg_version}-main.service"))
|
||||||
@@ -348,28 +326,6 @@ impl VersionT for Version {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokio::fs::metadata("/media/startos/data/package-data/volumes/ghost")
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
tokio::fs::rename(
|
|
||||||
"/media/startos/data/package-data/volumes/ghost",
|
|
||||||
"/media/startos/data/package-data/volumes/ghost-legacy",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokio::fs::metadata("/media/startos/data/package-data/volumes/synapse")
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
tokio::fs::rename(
|
|
||||||
"/media/startos/data/package-data/volumes/synapse",
|
|
||||||
"/media/startos/data/package-data/volumes/synapse-legacy",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load bundled migration images (start9/compat, start9/utils,
|
// Load bundled migration images (start9/compat, start9/utils,
|
||||||
// tonistiigi/binfmt) so the v1->v2 s9pk conversion doesn't need
|
// tonistiigi/binfmt) so the v1->v2 s9pk conversion doesn't need
|
||||||
// internet access.
|
// internet access.
|
||||||
@@ -400,10 +356,7 @@ impl VersionT for Version {
|
|||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
if let Some(ref id) = *current_package.borrow() {
|
if let Some(ref id) = *current_package.borrow() {
|
||||||
tracing::info!(
|
tracing::info!("{}", t!("migration.migrating-package", package = id.to_string()));
|
||||||
"{}",
|
|
||||||
t!("migration.migrating-package", package = id.to_string())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -448,10 +401,7 @@ impl VersionT for Version {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!("{}", t!("migration.migrating-package", package = id.to_string()));
|
||||||
"{}",
|
|
||||||
t!("migration.migrating-package", package = id.to_string())
|
|
||||||
);
|
|
||||||
current_package.send_replace(Some(id.clone()));
|
current_package.send_replace(Some(id.clone()));
|
||||||
|
|
||||||
if let Err(e) = async {
|
if let Err(e) = async {
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ impl VersionT for Version {
|
|||||||
&V0_3_0_COMPAT
|
&V0_3_0_COMPAT
|
||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
let status_info = db["public"]["serverInfo"]["statusInfo"]
|
|
||||||
.as_object_mut();
|
|
||||||
if let Some(m) = status_info {
|
|
||||||
m.remove("updated");
|
|
||||||
m.insert("restart".into(), Value::Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Value::Null)
|
Ok(Value::Null)
|
||||||
}
|
}
|
||||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
use exver::{PreReleaseSegment, VersionRange};
|
|
||||||
|
|
||||||
use super::v0_3_5::V0_3_0_COMPAT;
|
|
||||||
use super::{VersionT, v0_4_0_alpha_23};
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref V0_4_0_beta_0: exver::Version = exver::Version::new(
|
|
||||||
[0, 4, 0],
|
|
||||||
[PreReleaseSegment::String("beta".into()), 0.into()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct Version;
|
|
||||||
|
|
||||||
impl VersionT for Version {
|
|
||||||
type Previous = v0_4_0_alpha_23::Version;
|
|
||||||
type PreUpRes = ();
|
|
||||||
|
|
||||||
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn semver(self) -> exver::Version {
|
|
||||||
V0_4_0_beta_0.clone()
|
|
||||||
}
|
|
||||||
fn compat(self) -> &'static VersionRange {
|
|
||||||
&V0_3_0_COMPAT
|
|
||||||
}
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
|
||||||
Ok(Value::Null)
|
|
||||||
}
|
|
||||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type CountEntry = { label: string; count: bigint }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
import type { CountEntry } from './CountEntry'
|
|
||||||
import type { PackageVersionCount } from './PackageVersionCount'
|
|
||||||
|
|
||||||
export type DownloadsResponse = {
|
|
||||||
totalRequests: bigint
|
|
||||||
byPackage: Array<CountEntry>
|
|
||||||
byPackageVersion: Array<PackageVersionCount>
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type GetDownloadsParams = {
|
|
||||||
/**
|
|
||||||
* Filter by package ID
|
|
||||||
*/
|
|
||||||
pkgId: string | null
|
|
||||||
/**
|
|
||||||
* Filter by version
|
|
||||||
*/
|
|
||||||
version: string | null
|
|
||||||
/**
|
|
||||||
* Start of time range (RFC 3339)
|
|
||||||
*/
|
|
||||||
after: string | null
|
|
||||||
/**
|
|
||||||
* End of time range (RFC 3339)
|
|
||||||
*/
|
|
||||||
before: string | null
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type GetUsersParams = {
|
|
||||||
/**
|
|
||||||
* Start of time range (RFC 3339)
|
|
||||||
*/
|
|
||||||
after: string | null
|
|
||||||
/**
|
|
||||||
* End of time range (RFC 3339)
|
|
||||||
*/
|
|
||||||
before: string | null
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@ import type { VolumeId } from './VolumeId'
|
|||||||
export type Manifest = {
|
export type Manifest = {
|
||||||
id: PackageId
|
id: PackageId
|
||||||
version: Version
|
version: Version
|
||||||
|
satisfies: Array<Version>
|
||||||
canMigrateTo: string
|
canMigrateTo: string
|
||||||
canMigrateFrom: string
|
canMigrateFrom: string
|
||||||
images: { [key: ImageId]: ImageConfig }
|
images: { [key: ImageId]: ImageConfig }
|
||||||
@@ -36,5 +37,4 @@ export type Manifest = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
satisfies: Array<Version>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
import type { CountEntry } from './CountEntry'
|
|
||||||
|
|
||||||
export type MetricsSummary = {
|
|
||||||
totalCheckins: bigint
|
|
||||||
uniqueServers: bigint
|
|
||||||
totalPackageRequests: bigint
|
|
||||||
byArch: Array<CountEntry>
|
|
||||||
byOsVersion: Array<CountEntry>
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type PackageVersionCount = {
|
|
||||||
pkgId: string
|
|
||||||
version: string
|
|
||||||
count: bigint
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import type { MerkleArchiveCommitment } from './MerkleArchiveCommitment'
|
|||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
import type { PluginId } from './PluginId'
|
import type { PluginId } from './PluginId'
|
||||||
import type { RegistryAsset } from './RegistryAsset'
|
import type { RegistryAsset } from './RegistryAsset'
|
||||||
import type { Version } from './Version'
|
|
||||||
|
|
||||||
export type PackageVersionInfo = {
|
export type PackageVersionInfo = {
|
||||||
icon: DataUrl
|
icon: DataUrl
|
||||||
@@ -32,5 +31,4 @@ export type PackageVersionInfo = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
satisfies: Array<Version>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type RestartReason = 'mdns' | 'language' | 'kiosk' | 'update'
|
|
||||||
@@ -7,6 +7,8 @@ import type { ServerStatus } from './ServerStatus'
|
|||||||
import type { SmtpValue } from './SmtpValue'
|
import type { SmtpValue } from './SmtpValue'
|
||||||
|
|
||||||
export type ServerInfo = {
|
export type ServerInfo = {
|
||||||
|
arch: string
|
||||||
|
platform: string
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
hostname: string
|
hostname: string
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
import type { BackupProgress } from './BackupProgress'
|
import type { BackupProgress } from './BackupProgress'
|
||||||
import type { FullProgress } from './FullProgress'
|
import type { FullProgress } from './FullProgress'
|
||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
import type { RestartReason } from './RestartReason'
|
|
||||||
|
|
||||||
export type ServerStatus = {
|
export type ServerStatus = {
|
||||||
backupProgress: { [key: PackageId]: BackupProgress } | null
|
backupProgress: { [key: PackageId]: BackupProgress } | null
|
||||||
|
updated: boolean
|
||||||
updateProgress: FullProgress | null
|
updateProgress: FullProgress | null
|
||||||
shuttingDown: boolean
|
shuttingDown: boolean
|
||||||
restarting: boolean
|
restarting: boolean
|
||||||
restart: RestartReason | null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
||||||
|
|
||||||
export type UsersResponse = { uniqueServers: bigint; totalCheckins: bigint }
|
|
||||||
@@ -74,7 +74,6 @@ export { ClearTasksParams } from './ClearTasksParams'
|
|||||||
export { CliSetIconParams } from './CliSetIconParams'
|
export { CliSetIconParams } from './CliSetIconParams'
|
||||||
export { ContactInfo } from './ContactInfo'
|
export { ContactInfo } from './ContactInfo'
|
||||||
export { ControlParams } from './ControlParams'
|
export { ControlParams } from './ControlParams'
|
||||||
export { CountEntry } from './CountEntry'
|
|
||||||
export { CreateSubcontainerFsParams } from './CreateSubcontainerFsParams'
|
export { CreateSubcontainerFsParams } from './CreateSubcontainerFsParams'
|
||||||
export { CreateTaskParams } from './CreateTaskParams'
|
export { CreateTaskParams } from './CreateTaskParams'
|
||||||
export { CurrentDependencies } from './CurrentDependencies'
|
export { CurrentDependencies } from './CurrentDependencies'
|
||||||
@@ -91,7 +90,6 @@ export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
|||||||
export { DeviceFilter } from './DeviceFilter'
|
export { DeviceFilter } from './DeviceFilter'
|
||||||
export { DnsSettings } from './DnsSettings'
|
export { DnsSettings } from './DnsSettings'
|
||||||
export { DomainSettings } from './DomainSettings'
|
export { DomainSettings } from './DomainSettings'
|
||||||
export { DownloadsResponse } from './DownloadsResponse'
|
|
||||||
export { Duration } from './Duration'
|
export { Duration } from './Duration'
|
||||||
export { EchoParams } from './EchoParams'
|
export { EchoParams } from './EchoParams'
|
||||||
export { EditSignerParams } from './EditSignerParams'
|
export { EditSignerParams } from './EditSignerParams'
|
||||||
@@ -112,7 +110,6 @@ export { GenerateCertificateParams } from './GenerateCertificateParams'
|
|||||||
export { GenerateCertificateResponse } from './GenerateCertificateResponse'
|
export { GenerateCertificateResponse } from './GenerateCertificateResponse'
|
||||||
export { GetActionInputParams } from './GetActionInputParams'
|
export { GetActionInputParams } from './GetActionInputParams'
|
||||||
export { GetContainerIpParams } from './GetContainerIpParams'
|
export { GetContainerIpParams } from './GetContainerIpParams'
|
||||||
export { GetDownloadsParams } from './GetDownloadsParams'
|
|
||||||
export { GetHostInfoParams } from './GetHostInfoParams'
|
export { GetHostInfoParams } from './GetHostInfoParams'
|
||||||
export { GetOsAssetParams } from './GetOsAssetParams'
|
export { GetOsAssetParams } from './GetOsAssetParams'
|
||||||
export { GetOsVersionParams } from './GetOsVersionParams'
|
export { GetOsVersionParams } from './GetOsVersionParams'
|
||||||
@@ -127,7 +124,6 @@ export { GetSslCertificateParams } from './GetSslCertificateParams'
|
|||||||
export { GetSslKeyParams } from './GetSslKeyParams'
|
export { GetSslKeyParams } from './GetSslKeyParams'
|
||||||
export { GetStatusParams } from './GetStatusParams'
|
export { GetStatusParams } from './GetStatusParams'
|
||||||
export { GetSystemSmtpParams } from './GetSystemSmtpParams'
|
export { GetSystemSmtpParams } from './GetSystemSmtpParams'
|
||||||
export { GetUsersParams } from './GetUsersParams'
|
|
||||||
export { GigaBytes } from './GigaBytes'
|
export { GigaBytes } from './GigaBytes'
|
||||||
export { GitHash } from './GitHash'
|
export { GitHash } from './GitHash'
|
||||||
export { Governor } from './Governor'
|
export { Governor } from './Governor'
|
||||||
@@ -179,7 +175,6 @@ export { MetricsDisk } from './MetricsDisk'
|
|||||||
export { MetricsFollowResponse } from './MetricsFollowResponse'
|
export { MetricsFollowResponse } from './MetricsFollowResponse'
|
||||||
export { MetricsGeneral } from './MetricsGeneral'
|
export { MetricsGeneral } from './MetricsGeneral'
|
||||||
export { MetricsMemory } from './MetricsMemory'
|
export { MetricsMemory } from './MetricsMemory'
|
||||||
export { MetricsSummary } from './MetricsSummary'
|
|
||||||
export { Metrics } from './Metrics'
|
export { Metrics } from './Metrics'
|
||||||
export { ModifyNotificationBeforeParams } from './ModifyNotificationBeforeParams'
|
export { ModifyNotificationBeforeParams } from './ModifyNotificationBeforeParams'
|
||||||
export { ModifyNotificationParams } from './ModifyNotificationParams'
|
export { ModifyNotificationParams } from './ModifyNotificationParams'
|
||||||
@@ -207,7 +202,6 @@ export { PackageInfoShort } from './PackageInfoShort'
|
|||||||
export { PackageInfo } from './PackageInfo'
|
export { PackageInfo } from './PackageInfo'
|
||||||
export { PackagePlugin } from './PackagePlugin'
|
export { PackagePlugin } from './PackagePlugin'
|
||||||
export { PackageState } from './PackageState'
|
export { PackageState } from './PackageState'
|
||||||
export { PackageVersionCount } from './PackageVersionCount'
|
|
||||||
export { PackageVersionInfo } from './PackageVersionInfo'
|
export { PackageVersionInfo } from './PackageVersionInfo'
|
||||||
export { PartitionInfo } from './PartitionInfo'
|
export { PartitionInfo } from './PartitionInfo'
|
||||||
export { PassthroughInfo } from './PassthroughInfo'
|
export { PassthroughInfo } from './PassthroughInfo'
|
||||||
@@ -242,7 +236,6 @@ export { RenameGatewayParams } from './RenameGatewayParams'
|
|||||||
export { ReplayId } from './ReplayId'
|
export { ReplayId } from './ReplayId'
|
||||||
export { RequestCommitment } from './RequestCommitment'
|
export { RequestCommitment } from './RequestCommitment'
|
||||||
export { ResetPasswordParams } from './ResetPasswordParams'
|
export { ResetPasswordParams } from './ResetPasswordParams'
|
||||||
export { RestartReason } from './RestartReason'
|
|
||||||
export { RestorePackageParams } from './RestorePackageParams'
|
export { RestorePackageParams } from './RestorePackageParams'
|
||||||
export { RunActionParams } from './RunActionParams'
|
export { RunActionParams } from './RunActionParams'
|
||||||
export { Security } from './Security'
|
export { Security } from './Security'
|
||||||
@@ -306,7 +299,6 @@ export { UrlPluginClearUrlsParams } from './UrlPluginClearUrlsParams'
|
|||||||
export { UrlPluginExportUrlParams } from './UrlPluginExportUrlParams'
|
export { UrlPluginExportUrlParams } from './UrlPluginExportUrlParams'
|
||||||
export { UrlPluginRegisterParams } from './UrlPluginRegisterParams'
|
export { UrlPluginRegisterParams } from './UrlPluginRegisterParams'
|
||||||
export { UrlPluginRegistration } from './UrlPluginRegistration'
|
export { UrlPluginRegistration } from './UrlPluginRegistration'
|
||||||
export { UsersResponse } from './UsersResponse'
|
|
||||||
export { VerifyCifsParams } from './VerifyCifsParams'
|
export { VerifyCifsParams } from './VerifyCifsParams'
|
||||||
export { VersionSignerParams } from './VersionSignerParams'
|
export { VersionSignerParams } from './VersionSignerParams'
|
||||||
export { Version } from './Version'
|
export { Version } from './Version'
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Performs a deep structural equality check across all provided arguments.
|
* Performs a deep structural equality check across all provided arguments.
|
||||||
* Returns true only if every argument is deeply equal to every other argument.
|
* Returns true only if every argument is deeply equal to every other argument.
|
||||||
* Handles primitives, arrays, and plain objects (JSON-like) recursively.
|
* Handles primitives, arrays, and plain objects recursively.
|
||||||
*
|
|
||||||
* Non-plain objects (Set, Map, Date, etc.) are compared by reference only,
|
|
||||||
* since Object.keys() does not enumerate their contents.
|
|
||||||
*
|
*
|
||||||
* @param args - Two or more values to compare for deep equality
|
* @param args - Two or more values to compare for deep equality
|
||||||
* @returns True if all arguments are deeply equal
|
* @returns True if all arguments are deeply equal
|
||||||
@@ -26,18 +23,6 @@ export function deepEqual(...args: unknown[]) {
|
|||||||
}
|
}
|
||||||
if (objects.length !== args.length) return false
|
if (objects.length !== args.length) return false
|
||||||
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
||||||
if (
|
|
||||||
objects.some(
|
|
||||||
(x) => !Array.isArray(x) && Object.getPrototypeOf(x) !== Object.prototype,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
objects.reduce<object | null>(
|
|
||||||
(a, b) => (a === b ? a : null),
|
|
||||||
objects[0],
|
|
||||||
) !== null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
||||||
for (const key of allKeys) {
|
for (const key of allKeys) {
|
||||||
for (const x of objects) {
|
for (const x of objects) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import { createVolumes } from './util/Volume'
|
|||||||
import { getDataVersion, setDataVersion } from './version'
|
import { getDataVersion, setDataVersion } from './version'
|
||||||
|
|
||||||
/** The minimum StartOS version required by this SDK release */
|
/** The minimum StartOS version required by this SDK release */
|
||||||
export const OSVersion = testTypeVersion('0.4.0-beta.0')
|
export const OSVersion = testTypeVersion('0.4.0-alpha.23')
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
type AnyNeverCond<T extends any[], Then, Else> =
|
||||||
|
|||||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "1.0.0",
|
"version": "0.4.0-beta.66",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "1.0.0",
|
"version": "0.4.0-beta.66",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "1.0.0",
|
"version": "0.4.0-beta.66",
|
||||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"types": "./package/lib/index.d.ts",
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-beta.0",
|
"version": "0.4.0-alpha.23",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-beta.0",
|
"version": "0.4.0-alpha.23",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^21.2.1",
|
"@angular/cdk": "^21.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-beta.0",
|
"version": "0.4.0-alpha.23",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,16 +4,8 @@ import {
|
|||||||
HostListener,
|
HostListener,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
|
||||||
AbstractControl,
|
|
||||||
FormControl,
|
|
||||||
FormGroup,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
ValidatorFn,
|
|
||||||
Validators,
|
|
||||||
} from '@angular/forms'
|
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { WA_IS_MOBILE } from '@ng-web-apis/platform'
|
import { FormsModule } from '@angular/forms'
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
DiskInfo,
|
DiskInfo,
|
||||||
@@ -22,14 +14,13 @@ import {
|
|||||||
i18nPipe,
|
i18nPipe,
|
||||||
toGuid,
|
toGuid,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
|
import { WA_IS_MOBILE } from '@ng-web-apis/platform'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiError,
|
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiLoader,
|
TuiLoader,
|
||||||
|
TuiInput,
|
||||||
TuiNotification,
|
TuiNotification,
|
||||||
TUI_VALIDATION_ERRORS,
|
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
@@ -38,55 +29,49 @@ import {
|
|||||||
TuiSelect,
|
TuiSelect,
|
||||||
TuiTooltip,
|
TuiTooltip,
|
||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { TuiCardLarge, TuiForm, TuiHeader } from '@taiga-ui/layout'
|
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||||
import { distinctUntilChanged, filter, Subscription } from 'rxjs'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { PRESERVE_OVERWRITE } from '../components/preserve-overwrite.dialog'
|
import { filter, Subscription } from 'rxjs'
|
||||||
import { ApiService } from '../services/api.service'
|
import { ApiService } from '../services/api.service'
|
||||||
import { StateService } from '../services/state.service'
|
import { StateService } from '../services/state.service'
|
||||||
|
import { PRESERVE_OVERWRITE } from '../components/preserve-overwrite.dialog'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (!shuttingDown) {
|
@if (!shuttingDown) {
|
||||||
@if (loading) {
|
<section tuiCardLarge="compact">
|
||||||
<section tuiCardLarge="compact">
|
<header tuiHeader>
|
||||||
<header tuiHeader>
|
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
||||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
</header>
|
||||||
</header>
|
|
||||||
|
@if (loading) {
|
||||||
<tui-loader />
|
<tui-loader />
|
||||||
</section>
|
} @else if (drives.length === 0) {
|
||||||
} @else if (drives.length === 0) {
|
|
||||||
<section tuiCardLarge="compact">
|
|
||||||
<header tuiHeader>
|
|
||||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
|
||||||
</header>
|
|
||||||
<p tuiNotification size="m" appearance="warning">
|
<p tuiNotification size="m" appearance="warning">
|
||||||
{{
|
{{
|
||||||
'No drives found. Please connect a drive and click Refresh.'
|
'No drives found. Please connect a drive and click Refresh.'
|
||||||
| i18n
|
| i18n
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<footer>
|
} @else {
|
||||||
<button tuiButton appearance="secondary" (click)="refresh()">
|
<tui-textfield
|
||||||
{{ 'Refresh' | i18n }}
|
[stringify]="stringify"
|
||||||
</button>
|
[disabledItemHandler]="osDisabled"
|
||||||
</footer>
|
>
|
||||||
</section>
|
|
||||||
} @else {
|
|
||||||
<form tuiCardLarge="compact" tuiForm [formGroup]="form">
|
|
||||||
<header tuiHeader>
|
|
||||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<tui-textfield [stringify]="stringify">
|
|
||||||
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
||||||
@if (mobile) {
|
@if (mobile) {
|
||||||
<select
|
<select
|
||||||
tuiSelect
|
tuiSelect
|
||||||
formControlName="osDrive"
|
[ngModel]="selectedOsDrive"
|
||||||
|
(ngModelChange)="onOsDriveChange($event)"
|
||||||
[items]="drives"
|
[items]="drives"
|
||||||
></select>
|
></select>
|
||||||
} @else {
|
} @else {
|
||||||
<input tuiSelect formControlName="osDrive" />
|
<input
|
||||||
|
tuiSelect
|
||||||
|
[ngModel]="selectedOsDrive"
|
||||||
|
(ngModelChange)="onOsDriveChange($event)"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
@if (!mobile) {
|
@if (!mobile) {
|
||||||
<tui-data-list-wrapper
|
<tui-data-list-wrapper
|
||||||
@@ -97,28 +82,24 @@ import { StateService } from '../services/state.service'
|
|||||||
}
|
}
|
||||||
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
||||||
</tui-textfield>
|
</tui-textfield>
|
||||||
@if (form.controls.osDrive.touched && form.controls.osDrive.invalid) {
|
|
||||||
<tui-error formControlName="osDrive" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<tui-textfield [stringify]="stringify">
|
<tui-textfield
|
||||||
|
[stringify]="stringify"
|
||||||
|
[disabledItemHandler]="dataDisabled"
|
||||||
|
>
|
||||||
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
||||||
@if (mobile) {
|
@if (mobile) {
|
||||||
<select
|
<select
|
||||||
tuiSelect
|
tuiSelect
|
||||||
formControlName="dataDrive"
|
[(ngModel)]="selectedDataDrive"
|
||||||
|
(ngModelChange)="onDataDriveChange($event)"
|
||||||
[items]="drives"
|
[items]="drives"
|
||||||
[tuiValidator]="
|
|
||||||
form.controls.osDrive.value | tuiMapper: dataValidator
|
|
||||||
"
|
|
||||||
></select>
|
></select>
|
||||||
} @else {
|
} @else {
|
||||||
<input
|
<input
|
||||||
tuiSelect
|
tuiSelect
|
||||||
formControlName="dataDrive"
|
[(ngModel)]="selectedDataDrive"
|
||||||
[tuiValidator]="
|
(ngModelChange)="onDataDriveChange($event)"
|
||||||
form.controls.osDrive.value | tuiMapper: dataValidator
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@if (!mobile) {
|
@if (!mobile) {
|
||||||
@@ -136,11 +117,6 @@ import { StateService } from '../services/state.service'
|
|||||||
}
|
}
|
||||||
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
||||||
</tui-textfield>
|
</tui-textfield>
|
||||||
@if (
|
|
||||||
form.controls.dataDrive.touched && form.controls.dataDrive.invalid
|
|
||||||
) {
|
|
||||||
<tui-error formControlName="dataDrive" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<ng-template #driveContent let-drive>
|
<ng-template #driveContent let-drive>
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
@@ -150,14 +126,24 @@ import { StateService } from '../services/state.service'
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<button tuiButton [disabled]="form.invalid" (click)="continue()">
|
@if (drives.length === 0) {
|
||||||
|
<button tuiButton appearance="secondary" (click)="refresh()">
|
||||||
|
{{ 'Refresh' | i18n }}
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
[disabled]="!selectedOsDrive || !selectedDataDrive"
|
||||||
|
(click)="continue()"
|
||||||
|
>
|
||||||
{{ 'Continue' | i18n }}
|
{{ 'Continue' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
}
|
||||||
</form>
|
</footer>
|
||||||
}
|
</section>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -166,34 +152,20 @@ import { StateService } from '../services/state.service'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
imports: [
|
imports: [
|
||||||
ReactiveFormsModule,
|
FormsModule,
|
||||||
TuiCardLarge,
|
TuiCardLarge,
|
||||||
TuiForm,
|
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiError,
|
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiLoader,
|
TuiLoader,
|
||||||
|
TuiInput,
|
||||||
TuiNotification,
|
TuiNotification,
|
||||||
TuiSelect,
|
TuiSelect,
|
||||||
TuiDataListWrapper,
|
TuiDataListWrapper,
|
||||||
TuiTooltip,
|
TuiTooltip,
|
||||||
TuiValidator,
|
|
||||||
TuiMapperPipe,
|
|
||||||
TuiHeader,
|
TuiHeader,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: TUI_VALIDATION_ERRORS,
|
|
||||||
useFactory: () => {
|
|
||||||
const i18n = inject(i18nPipe)
|
|
||||||
return {
|
|
||||||
required: i18n.transform('Required'),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export default class DrivesPage {
|
export default class DrivesPage {
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
@@ -216,63 +188,29 @@ export default class DrivesPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly osDriveTooltip = this.i18n.transform(
|
readonly osDriveTooltip = this.i18n.transform(
|
||||||
'The drive where the StartOS operating system will be installed. Minimum 18 GB.',
|
'The drive where the StartOS operating system will be installed.',
|
||||||
)
|
)
|
||||||
readonly dataDriveTooltip = this.i18n.transform(
|
readonly dataDriveTooltip = this.i18n.transform(
|
||||||
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive. Minimum 20 GB, or 38 GB if using a single drive for both OS and data.',
|
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive.',
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly MIN_OS = 18 * 2 ** 30 // 18 GiB
|
private readonly MIN_OS = 18 * 2 ** 30 // 18 GiB
|
||||||
private readonly MIN_DATA = 20 * 2 ** 30 // 20 GiB
|
private readonly MIN_DATA = 20 * 2 ** 30 // 20 GiB
|
||||||
private readonly MIN_BOTH = 38 * 2 ** 30 // 38 GiB
|
private readonly MIN_BOTH = 38 * 2 ** 30 // 38 GiB
|
||||||
|
|
||||||
private readonly osCapacityValidator: ValidatorFn = ({
|
|
||||||
value,
|
|
||||||
}: AbstractControl) => {
|
|
||||||
if (!value) return null
|
|
||||||
return value.capacity < this.MIN_OS
|
|
||||||
? {
|
|
||||||
tooSmallOs: this.i18n.transform('OS drive must be at least 18 GB'),
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly form = new FormGroup({
|
|
||||||
osDrive: new FormControl<DiskInfo | null>(null, [
|
|
||||||
Validators.required,
|
|
||||||
this.osCapacityValidator,
|
|
||||||
]),
|
|
||||||
dataDrive: new FormControl<DiskInfo | null>(null, [Validators.required]),
|
|
||||||
})
|
|
||||||
|
|
||||||
readonly dataValidator =
|
|
||||||
(osDrive: DiskInfo | null): ValidatorFn =>
|
|
||||||
({ value }: AbstractControl) => {
|
|
||||||
if (!value) return null
|
|
||||||
const sameAsOs = osDrive && value.logicalname === osDrive.logicalname
|
|
||||||
const min = sameAsOs ? this.MIN_BOTH : this.MIN_DATA
|
|
||||||
if (value.capacity < min) {
|
|
||||||
return sameAsOs
|
|
||||||
? {
|
|
||||||
tooSmallBoth: this.i18n.transform(
|
|
||||||
'OS + data combined require at least 38 GB',
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
tooSmallData: this.i18n.transform(
|
|
||||||
'Data drive must be at least 20 GB',
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
drives: DiskInfo[] = []
|
drives: DiskInfo[] = []
|
||||||
loading = true
|
loading = true
|
||||||
shuttingDown = false
|
shuttingDown = false
|
||||||
private dialogSub?: Subscription
|
private dialogSub?: Subscription
|
||||||
|
selectedOsDrive: DiskInfo | null = null
|
||||||
|
selectedDataDrive: DiskInfo | null = null
|
||||||
preserveData: boolean | null = null
|
preserveData: boolean | null = null
|
||||||
|
|
||||||
|
readonly osDisabled = (drive: DiskInfo): boolean =>
|
||||||
|
drive.capacity < this.MIN_OS
|
||||||
|
|
||||||
|
dataDisabled = (drive: DiskInfo): boolean => drive.capacity < this.MIN_DATA
|
||||||
|
|
||||||
readonly driveName = (drive: DiskInfo): string =>
|
readonly driveName = (drive: DiskInfo): string =>
|
||||||
[drive.vendor, drive.model].filter(Boolean).join(' ') ||
|
[drive.vendor, drive.model].filter(Boolean).join(' ') ||
|
||||||
this.i18n.transform('Unknown Drive')
|
this.i18n.transform('Unknown Drive')
|
||||||
@@ -290,40 +228,51 @@ export default class DrivesPage {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await this.loadDrives()
|
await this.loadDrives()
|
||||||
|
|
||||||
this.form.controls.osDrive.valueChanges.subscribe(drive => {
|
|
||||||
if (drive) {
|
|
||||||
this.form.controls.osDrive.markAsTouched()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.form.controls.dataDrive.valueChanges
|
|
||||||
.pipe(distinctUntilChanged())
|
|
||||||
.subscribe(drive => {
|
|
||||||
this.preserveData = null
|
|
||||||
if (drive) {
|
|
||||||
this.form.controls.dataDrive.markAsTouched()
|
|
||||||
if (toGuid(drive)) {
|
|
||||||
this.showPreserveOverwriteDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.form.reset()
|
this.selectedOsDrive = null
|
||||||
|
this.selectedDataDrive = null
|
||||||
this.preserveData = null
|
this.preserveData = null
|
||||||
await this.loadDrives()
|
await this.loadDrives()
|
||||||
}
|
}
|
||||||
|
|
||||||
continue() {
|
onOsDriveChange(osDrive: DiskInfo | null) {
|
||||||
const osDrive = this.form.controls.osDrive.value
|
this.selectedOsDrive = osDrive
|
||||||
const dataDrive = this.form.controls.dataDrive.value
|
this.dataDisabled = (drive: DiskInfo) => {
|
||||||
if (!osDrive || !dataDrive) return
|
if (osDrive && drive.logicalname === osDrive.logicalname) {
|
||||||
|
return drive.capacity < this.MIN_BOTH
|
||||||
|
}
|
||||||
|
return drive.capacity < this.MIN_DATA
|
||||||
|
}
|
||||||
|
|
||||||
const sameDevice = osDrive.logicalname === dataDrive.logicalname
|
// Clear data drive if it's now invalid
|
||||||
const dataHasStartOS = !!toGuid(dataDrive)
|
if (this.selectedDataDrive && this.dataDisabled(this.selectedDataDrive)) {
|
||||||
|
this.selectedDataDrive = null
|
||||||
|
this.preserveData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataDriveChange(drive: DiskInfo | null) {
|
||||||
|
this.preserveData = null
|
||||||
|
|
||||||
|
if (!drive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasStartOSData = !!toGuid(drive)
|
||||||
|
if (hasStartOSData) {
|
||||||
|
this.showPreserveOverwriteDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue() {
|
||||||
|
if (!this.selectedOsDrive || !this.selectedDataDrive) return
|
||||||
|
|
||||||
|
const sameDevice =
|
||||||
|
this.selectedOsDrive.logicalname === this.selectedDataDrive.logicalname
|
||||||
|
const dataHasStartOS = !!toGuid(this.selectedDataDrive)
|
||||||
|
|
||||||
// Scenario 1: Same drive, has StartOS data, preserving → no warning
|
// Scenario 1: Same drive, has StartOS data, preserving → no warning
|
||||||
if (sameDevice && dataHasStartOS && this.preserveData) {
|
if (sameDevice && dataHasStartOS && this.preserveData) {
|
||||||
@@ -343,7 +292,7 @@ export default class DrivesPage {
|
|||||||
|
|
||||||
private showPreserveOverwriteDialog() {
|
private showPreserveOverwriteDialog() {
|
||||||
let selectionMade = false
|
let selectionMade = false
|
||||||
const drive = this.form.controls.dataDrive.value
|
const drive = this.selectedDataDrive
|
||||||
const filesystem =
|
const filesystem =
|
||||||
drive?.filesystem ||
|
drive?.filesystem ||
|
||||||
drive?.partitions.find(p => p.guid)?.filesystem ||
|
drive?.partitions.find(p => p.guid)?.filesystem ||
|
||||||
@@ -355,20 +304,20 @@ export default class DrivesPage {
|
|||||||
data: { isExt4 },
|
data: { isExt4 },
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: preserve => {
|
next: preserve => {
|
||||||
selectionMade = true
|
selectionMade = true
|
||||||
this.preserveData = preserve
|
this.preserveData = preserve
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
if (!selectionMade) {
|
||||||
|
// Dialog was dismissed without selection - clear the data drive
|
||||||
|
this.selectedDataDrive = null
|
||||||
|
this.preserveData = null
|
||||||
this.cdr.markForCheck()
|
this.cdr.markForCheck()
|
||||||
},
|
}
|
||||||
complete: () => {
|
},
|
||||||
if (!selectionMade) {
|
})
|
||||||
// Dialog was dismissed without selection - clear the data drive
|
|
||||||
this.form.controls.dataDrive.reset()
|
|
||||||
this.preserveData = null
|
|
||||||
this.cdr.markForCheck()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showOsDriveWarning() {
|
private showOsDriveWarning() {
|
||||||
@@ -411,15 +360,13 @@ export default class DrivesPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async installOs(wipe: boolean) {
|
private async installOs(wipe: boolean) {
|
||||||
const osDrive = this.form.controls.osDrive.value!
|
|
||||||
const dataDrive = this.form.controls.dataDrive.value!
|
|
||||||
const loader = this.loader.open('Installing StartOS').subscribe()
|
const loader = this.loader.open('Installing StartOS').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.api.installOs({
|
const result = await this.api.installOs({
|
||||||
osDrive: osDrive.logicalname,
|
osDrive: this.selectedOsDrive!.logicalname,
|
||||||
dataDrive: {
|
dataDrive: {
|
||||||
logicalname: dataDrive.logicalname,
|
logicalname: this.selectedDataDrive!.logicalname,
|
||||||
wipe,
|
wipe,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ export default {
|
|||||||
425: 'Ausführen',
|
425: 'Ausführen',
|
||||||
426: 'Aktion kann nur ausgeführt werden, wenn der Dienst',
|
426: 'Aktion kann nur ausgeführt werden, wenn der Dienst',
|
||||||
427: 'Verboten',
|
427: 'Verboten',
|
||||||
|
428: 'kann vorübergehend Probleme verursachen',
|
||||||
429: 'hat unerfüllte Abhängigkeiten. Es wird nicht wie erwartet funktionieren.',
|
429: 'hat unerfüllte Abhängigkeiten. Es wird nicht wie erwartet funktionieren.',
|
||||||
430: 'Container wird neu gebaut',
|
430: 'Container wird neu gebaut',
|
||||||
431: 'Deinstallation wird gestartet',
|
431: 'Deinstallation wird gestartet',
|
||||||
@@ -484,6 +485,7 @@ export default {
|
|||||||
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
|
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
|
||||||
513: 'Aktivieren',
|
513: 'Aktivieren',
|
||||||
514: 'Deaktivieren',
|
514: 'Deaktivieren',
|
||||||
|
515: 'Diese Änderung wird nach dem nächsten Neustart wirksam',
|
||||||
516: 'Empfohlen',
|
516: 'Empfohlen',
|
||||||
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
||||||
518: 'Verwerfen',
|
518: 'Verwerfen',
|
||||||
@@ -627,8 +629,8 @@ export default {
|
|||||||
697: 'Geben Sie das Passwort ein, das zum Verschlüsseln dieses Backups verwendet wurde.',
|
697: 'Geben Sie das Passwort ein, das zum Verschlüsseln dieses Backups verwendet wurde.',
|
||||||
698: 'Mehrere Backups gefunden. Wählen Sie aus, welches wiederhergestellt werden soll.',
|
698: 'Mehrere Backups gefunden. Wählen Sie aus, welches wiederhergestellt werden soll.',
|
||||||
699: 'Backups',
|
699: 'Backups',
|
||||||
700: 'Das Laufwerk, auf dem das StartOS-Betriebssystem installiert wird. Mindestens 18 GB.',
|
700: 'Das Laufwerk, auf dem das StartOS-Betriebssystem installiert wird.',
|
||||||
701: 'Das Laufwerk, auf dem Ihre StartOS-Daten (Dienste, Einstellungen usw.) gespeichert werden. Dies kann dasselbe wie das OS-Laufwerk oder ein separates Laufwerk sein. Mindestens 20 GB, oder 38 GB bei Verwendung eines einzelnen Laufwerks für OS und Daten.',
|
701: 'Das Laufwerk, auf dem Ihre StartOS-Daten (Dienste, Einstellungen usw.) gespeichert werden. Dies kann dasselbe wie das OS-Laufwerk oder ein separates Laufwerk sein.',
|
||||||
702: 'Versuchen Sie nach der Datenübertragung von diesem Laufwerk nicht, erneut als Start9-Server davon zu booten. Dies kann zu Fehlfunktionen von Diensten, Datenbeschädigung oder Geldverlust führen.',
|
702: 'Versuchen Sie nach der Datenübertragung von diesem Laufwerk nicht, erneut als Start9-Server davon zu booten. Dies kann zu Fehlfunktionen von Diensten, Datenbeschädigung oder Geldverlust führen.',
|
||||||
703: 'Muss mindestens 12 Zeichen lang sein',
|
703: 'Muss mindestens 12 Zeichen lang sein',
|
||||||
704: 'Darf höchstens 64 Zeichen lang sein',
|
704: 'Darf höchstens 64 Zeichen lang sein',
|
||||||
@@ -715,15 +717,11 @@ export default {
|
|||||||
799: 'Nach Klick auf "Enroll MOK":',
|
799: 'Nach Klick auf "Enroll MOK":',
|
||||||
800: 'Geben Sie bei Aufforderung Ihr StartOS-Passwort ein',
|
800: 'Geben Sie bei Aufforderung Ihr StartOS-Passwort ein',
|
||||||
801: 'Ihr System hat Secure Boot aktiviert, was erfordert, dass alle Kernel-Module mit einem vertrauenswürdigen Schlüssel signiert sind. Einige Hardware-Treiber \u2014 wie die für NVIDIA-GPUs \u2014 sind nicht mit dem Standard-Distributionsschlüssel signiert. Die Registrierung des StartOS-Signaturschlüssels ermöglicht es Ihrer Firmware, diesen Modulen zu vertrauen, damit Ihre Hardware vollständig genutzt werden kann.',
|
801: 'Ihr System hat Secure Boot aktiviert, was erfordert, dass alle Kernel-Module mit einem vertrauenswürdigen Schlüssel signiert sind. Einige Hardware-Treiber \u2014 wie die für NVIDIA-GPUs \u2014 sind nicht mit dem Standard-Distributionsschlüssel signiert. Die Registrierung des StartOS-Signaturschlüssels ermöglicht es Ihrer Firmware, diesen Modulen zu vertrauen, damit Ihre Hardware vollständig genutzt werden kann.',
|
||||||
|
802: 'Die Übersetzungen auf Betriebssystemebene sind bereits aktiv. Ein Neustart ist erforderlich, damit die Übersetzungen auf Dienstebene wirksam werden.',
|
||||||
803: 'Dieses Laufwerk verwendet ext4 und wird automatisch in btrfs konvertiert. Ein Backup wird dringend empfohlen, bevor Sie fortfahren.',
|
803: 'Dieses Laufwerk verwendet ext4 und wird automatisch in btrfs konvertiert. Ein Backup wird dringend empfohlen, bevor Sie fortfahren.',
|
||||||
804: 'Ich habe ein Backup meiner Daten',
|
804: 'Ich habe ein Backup meiner Daten',
|
||||||
805: 'Öffentliche Domain hinzufügen',
|
805: 'Öffentliche Domain hinzufügen',
|
||||||
806: 'Ergebnis',
|
806: 'Ergebnis',
|
||||||
807: 'Download abgeschlossen. Neustart zum Anwenden.',
|
807: 'Nach dem Öffnen der neuen Adresse werden Sie zum Neustart aufgefordert.',
|
||||||
808: 'Hostname geändert, Neustart damit installierte Dienste die neue Adresse verwenden',
|
808: 'Ein Neustart ist erforderlich, damit die Dienstschnittstellen den neuen Hostnamen verwenden.',
|
||||||
809: 'Sprache geändert, Neustart damit installierte Dienste die neue Sprache verwenden',
|
|
||||||
810: 'Kioskmodus geändert, Neustart zum Anwenden',
|
|
||||||
811: 'OS-Laufwerk muss mindestens 18 GB groß sein',
|
|
||||||
812: 'Datenlaufwerk muss mindestens 20 GB groß sein',
|
|
||||||
813: 'OS + Daten zusammen erfordern mindestens 38 GB',
|
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Run': 425, // as in, run a piece of software
|
'Run': 425, // as in, run a piece of software
|
||||||
'Action can only be executed when service is': 426,
|
'Action can only be executed when service is': 426,
|
||||||
'Forbidden': 427,
|
'Forbidden': 427,
|
||||||
|
'may temporarily experiences issues': 428,
|
||||||
'has unmet dependencies. It will not work as expected.': 429,
|
'has unmet dependencies. It will not work as expected.': 429,
|
||||||
'Rebuilding container': 430,
|
'Rebuilding container': 430,
|
||||||
'Beginning uninstall': 431,
|
'Beginning uninstall': 431,
|
||||||
@@ -483,6 +484,7 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Kiosk Mode is unavailable on this device': 512,
|
'Kiosk Mode is unavailable on this device': 512,
|
||||||
'Enable': 513,
|
'Enable': 513,
|
||||||
'Disable': 514,
|
'Disable': 514,
|
||||||
|
'This change will take effect after the next boot': 515,
|
||||||
'Recommended': 516, // as in, we recommend this
|
'Recommended': 516, // as in, we recommend this
|
||||||
'Are you sure you want to dismiss this task?': 517,
|
'Are you sure you want to dismiss this task?': 517,
|
||||||
'Dismiss': 518, // as in, dismiss or delete a task
|
'Dismiss': 518, // as in, dismiss or delete a task
|
||||||
@@ -627,8 +629,8 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Enter the password that was used to encrypt this backup.': 697,
|
'Enter the password that was used to encrypt this backup.': 697,
|
||||||
'Multiple backups found. Select which one to restore.': 698,
|
'Multiple backups found. Select which one to restore.': 698,
|
||||||
'Backups': 699,
|
'Backups': 699,
|
||||||
'The drive where the StartOS operating system will be installed. Minimum 18 GB.': 700,
|
'The drive where the StartOS operating system will be installed.': 700,
|
||||||
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive. Minimum 20 GB, or 38 GB if using a single drive for both OS and data.': 701,
|
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive.': 701,
|
||||||
'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.': 702,
|
'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.': 702,
|
||||||
'Must be 12 characters or greater': 703,
|
'Must be 12 characters or greater': 703,
|
||||||
'Must be 64 character or less': 704,
|
'Must be 64 character or less': 704,
|
||||||
@@ -716,15 +718,11 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'After clicking "Enroll MOK":': 799,
|
'After clicking "Enroll MOK":': 799,
|
||||||
'When prompted, enter your StartOS password': 800,
|
'When prompted, enter your StartOS password': 800,
|
||||||
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers \u2014 such as those for NVIDIA GPUs \u2014 are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.': 801,
|
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers \u2014 such as those for NVIDIA GPUs \u2014 are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.': 801,
|
||||||
|
'OS-level translations are already in effect. A restart is required for service-level translations to take effect.': 802,
|
||||||
'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803,
|
'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803,
|
||||||
'I have a backup of my data': 804,
|
'I have a backup of my data': 804,
|
||||||
'Add Public Domain': 805,
|
'Add Public Domain': 805,
|
||||||
'Result': 806,
|
'Result': 806,
|
||||||
'Download complete. Restart to apply.': 807,
|
'After opening the new address, you will be prompted to restart.': 807,
|
||||||
'Hostname changed, restart for installed services to use the new address': 808,
|
'A restart is required for service interfaces to use the new hostname.': 808,
|
||||||
'Language changed, restart for installed services to use the new language': 809,
|
|
||||||
'Kiosk mode changed, restart to apply': 810,
|
|
||||||
'OS drive must be at least 18 GB': 811,
|
|
||||||
'Data drive must be at least 20 GB': 812,
|
|
||||||
'OS + data combined require at least 38 GB': 813,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ export default {
|
|||||||
425: 'Ejecutar',
|
425: 'Ejecutar',
|
||||||
426: 'La acción solo se puede ejecutar cuando el servicio está',
|
426: 'La acción solo se puede ejecutar cuando el servicio está',
|
||||||
427: 'Prohibido',
|
427: 'Prohibido',
|
||||||
|
428: 'puede experimentar problemas temporales',
|
||||||
429: 'tiene dependencias no satisfechas. No funcionará como se espera.',
|
429: 'tiene dependencias no satisfechas. No funcionará como se espera.',
|
||||||
430: 'Reconstruyendo contenedor',
|
430: 'Reconstruyendo contenedor',
|
||||||
431: 'Iniciando desinstalación',
|
431: 'Iniciando desinstalación',
|
||||||
@@ -484,6 +485,7 @@ export default {
|
|||||||
512: 'El modo quiosco no está disponible en este dispositivo',
|
512: 'El modo quiosco no está disponible en este dispositivo',
|
||||||
513: 'Activar',
|
513: 'Activar',
|
||||||
514: 'Desactivar',
|
514: 'Desactivar',
|
||||||
|
515: 'Este cambio tendrá efecto después del próximo inicio',
|
||||||
516: 'Recomendado',
|
516: 'Recomendado',
|
||||||
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
||||||
518: 'Descartar',
|
518: 'Descartar',
|
||||||
@@ -627,8 +629,8 @@ export default {
|
|||||||
697: 'Introduzca la contraseña que se utilizó para cifrar esta copia de seguridad.',
|
697: 'Introduzca la contraseña que se utilizó para cifrar esta copia de seguridad.',
|
||||||
698: 'Se encontraron varias copias de seguridad. Seleccione cuál restaurar.',
|
698: 'Se encontraron varias copias de seguridad. Seleccione cuál restaurar.',
|
||||||
699: 'Copias de seguridad',
|
699: 'Copias de seguridad',
|
||||||
700: 'La unidad donde se instalará el sistema operativo StartOS. Mínimo 18 GB.',
|
700: 'La unidad donde se instalará el sistema operativo StartOS.',
|
||||||
701: 'La unidad donde se almacenarán sus datos de StartOS (servicios, ajustes, etc.). Puede ser la misma que la unidad del sistema operativo o una unidad separada. Mínimo 20 GB, o 38 GB si se usa una sola unidad para el sistema operativo y los datos.',
|
701: 'La unidad donde se almacenarán sus datos de StartOS (servicios, ajustes, etc.). Puede ser la misma que la unidad del sistema operativo o una unidad separada.',
|
||||||
702: 'Después de transferir datos desde esta unidad, no intente arrancar desde ella nuevamente como un servidor Start9. Esto puede provocar fallos en los servicios, corrupción de datos o pérdida de fondos.',
|
702: 'Después de transferir datos desde esta unidad, no intente arrancar desde ella nuevamente como un servidor Start9. Esto puede provocar fallos en los servicios, corrupción de datos o pérdida de fondos.',
|
||||||
703: 'Debe tener 12 caracteres o más',
|
703: 'Debe tener 12 caracteres o más',
|
||||||
704: 'Debe tener 64 caracteres o menos',
|
704: 'Debe tener 64 caracteres o menos',
|
||||||
@@ -715,15 +717,11 @@ export default {
|
|||||||
799: 'Después de hacer clic en "Enroll MOK":',
|
799: 'Después de hacer clic en "Enroll MOK":',
|
||||||
800: 'Cuando se le solicite, ingrese su contraseña de StartOS',
|
800: 'Cuando se le solicite, ingrese su contraseña de StartOS',
|
||||||
801: 'Su sistema tiene Secure Boot habilitado, lo que requiere que todos los módulos del kernel estén firmados con una clave de confianza. Algunos controladores de hardware \u2014 como los de las GPU NVIDIA \u2014 no están firmados con la clave de distribución predeterminada. Registrar la clave de firma de StartOS permite que su firmware confíe en estos módulos para que su hardware pueda utilizarse completamente.',
|
801: 'Su sistema tiene Secure Boot habilitado, lo que requiere que todos los módulos del kernel estén firmados con una clave de confianza. Algunos controladores de hardware \u2014 como los de las GPU NVIDIA \u2014 no están firmados con la clave de distribución predeterminada. Registrar la clave de firma de StartOS permite que su firmware confíe en estos módulos para que su hardware pueda utilizarse completamente.',
|
||||||
|
802: 'Las traducciones a nivel del sistema operativo ya están en vigor. Se requiere un reinicio para que las traducciones a nivel de servicio surtan efecto.',
|
||||||
803: 'Esta unidad usa ext4 y se convertirá automáticamente a btrfs. Se recomienda encarecidamente hacer una copia de seguridad antes de continuar.',
|
803: 'Esta unidad usa ext4 y se convertirá automáticamente a btrfs. Se recomienda encarecidamente hacer una copia de seguridad antes de continuar.',
|
||||||
804: 'Tengo una copia de seguridad de mis datos',
|
804: 'Tengo una copia de seguridad de mis datos',
|
||||||
805: 'Agregar dominio público',
|
805: 'Agregar dominio público',
|
||||||
806: 'Resultado',
|
806: 'Resultado',
|
||||||
807: 'Descarga completa. Reiniciar para aplicar.',
|
807: 'Después de abrir la nueva dirección, se le pedirá que reinicie.',
|
||||||
808: 'Nombre de host cambiado, reiniciar para que los servicios instalados usen la nueva dirección',
|
808: 'Se requiere un reinicio para que las interfaces de servicio utilicen el nuevo nombre de host.',
|
||||||
809: 'Idioma cambiado, reiniciar para que los servicios instalados usen el nuevo idioma',
|
|
||||||
810: 'Modo kiosco cambiado, reiniciar para aplicar',
|
|
||||||
811: 'La unidad del SO debe tener al menos 18 GB',
|
|
||||||
812: 'La unidad de datos debe tener al menos 20 GB',
|
|
||||||
813: 'SO + datos combinados requieren al menos 38 GB',
|
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ export default {
|
|||||||
425: 'Exécuter',
|
425: 'Exécuter',
|
||||||
426: 'Action possible uniquement lorsque le service est',
|
426: 'Action possible uniquement lorsque le service est',
|
||||||
427: 'Interdit',
|
427: 'Interdit',
|
||||||
|
428: 'peut rencontrer des problèmes temporaires',
|
||||||
429: 'a des dépendances non satisfaites. Il ne fonctionnera pas comme prévu.',
|
429: 'a des dépendances non satisfaites. Il ne fonctionnera pas comme prévu.',
|
||||||
430: 'Reconstruction du conteneur',
|
430: 'Reconstruction du conteneur',
|
||||||
431: 'Désinstallation initiée',
|
431: 'Désinstallation initiée',
|
||||||
@@ -484,6 +485,7 @@ export default {
|
|||||||
512: 'Le mode kiosque n’est pas disponible sur cet appareil',
|
512: 'Le mode kiosque n’est pas disponible sur cet appareil',
|
||||||
513: 'Activer',
|
513: 'Activer',
|
||||||
514: 'Désactiver',
|
514: 'Désactiver',
|
||||||
|
515: 'Ce changement va prendre effet après le prochain démarrage',
|
||||||
516: 'Recommandé',
|
516: 'Recommandé',
|
||||||
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
||||||
518: 'Ignorer',
|
518: 'Ignorer',
|
||||||
@@ -627,8 +629,8 @@ export default {
|
|||||||
697: 'Saisissez le mot de passe utilisé pour chiffrer cette sauvegarde.',
|
697: 'Saisissez le mot de passe utilisé pour chiffrer cette sauvegarde.',
|
||||||
698: 'Plusieurs sauvegardes trouvées. Sélectionnez celle à restaurer.',
|
698: 'Plusieurs sauvegardes trouvées. Sélectionnez celle à restaurer.',
|
||||||
699: 'Sauvegardes',
|
699: 'Sauvegardes',
|
||||||
700: 'Le disque sur lequel le système d’exploitation StartOS sera installé. Minimum 18 Go.',
|
700: 'Le disque sur lequel le système d’exploitation StartOS sera installé.',
|
||||||
701: 'Le disque sur lequel vos données StartOS (services, paramètres, etc.) seront stockées. Il peut s’agir du même disque que le système ou d’un disque séparé. Minimum 20 Go, ou 38 Go si un seul disque est utilisé pour le système et les données.',
|
701: 'Le disque sur lequel vos données StartOS (services, paramètres, etc.) seront stockées. Il peut s’agir du même disque que le système ou d’un disque séparé.',
|
||||||
702: 'Après le transfert des données depuis ce disque, n’essayez pas de démarrer dessus à nouveau en tant que serveur Start9. Cela peut entraîner des dysfonctionnements des services, une corruption des données ou une perte de fonds.',
|
702: 'Après le transfert des données depuis ce disque, n’essayez pas de démarrer dessus à nouveau en tant que serveur Start9. Cela peut entraîner des dysfonctionnements des services, une corruption des données ou une perte de fonds.',
|
||||||
703: 'Doit comporter au moins 12 caractères',
|
703: 'Doit comporter au moins 12 caractères',
|
||||||
704: 'Doit comporter au maximum 64 caractères',
|
704: 'Doit comporter au maximum 64 caractères',
|
||||||
@@ -715,15 +717,11 @@ export default {
|
|||||||
799: 'Après avoir cliqué sur "Enroll MOK" :',
|
799: 'Après avoir cliqué sur "Enroll MOK" :',
|
||||||
800: 'Lorsque vous y êtes invité, entrez votre mot de passe StartOS',
|
800: 'Lorsque vous y êtes invité, entrez votre mot de passe StartOS',
|
||||||
801: "Votre système a Secure Boot activé, ce qui exige que tous les modules du noyau soient signés avec une clé de confiance. Certains pilotes matériels \u2014 comme ceux des GPU NVIDIA \u2014 ne sont pas signés par la clé de distribution par défaut. L'enregistrement de la clé de signature StartOS permet à votre firmware de faire confiance à ces modules afin que votre matériel puisse être pleinement utilisé.",
|
801: "Votre système a Secure Boot activé, ce qui exige que tous les modules du noyau soient signés avec une clé de confiance. Certains pilotes matériels \u2014 comme ceux des GPU NVIDIA \u2014 ne sont pas signés par la clé de distribution par défaut. L'enregistrement de la clé de signature StartOS permet à votre firmware de faire confiance à ces modules afin que votre matériel puisse être pleinement utilisé.",
|
||||||
|
802: "Les traductions au niveau du système d'exploitation sont déjà en vigueur. Un redémarrage est nécessaire pour que les traductions au niveau des services prennent effet.",
|
||||||
803: 'Ce disque utilise ext4 et sera automatiquement converti en btrfs. Il est fortement recommandé de faire une sauvegarde avant de continuer.',
|
803: 'Ce disque utilise ext4 et sera automatiquement converti en btrfs. Il est fortement recommandé de faire une sauvegarde avant de continuer.',
|
||||||
804: "J'ai une sauvegarde de mes données",
|
804: "J'ai une sauvegarde de mes données",
|
||||||
805: 'Ajouter un domaine public',
|
805: 'Ajouter un domaine public',
|
||||||
806: 'Résultat',
|
806: 'Résultat',
|
||||||
807: 'Téléchargement terminé. Redémarrer pour appliquer.',
|
807: 'Après avoir ouvert la nouvelle adresse, vous serez invité à redémarrer.',
|
||||||
808: "Nom d'hôte modifié, redémarrer pour que les services installés utilisent la nouvelle adresse",
|
808: "Un redémarrage est nécessaire pour que les interfaces de service utilisent le nouveau nom d'hôte.",
|
||||||
809: 'Langue modifiée, redémarrer pour que les services installés utilisent la nouvelle langue',
|
|
||||||
810: 'Mode kiosque modifié, redémarrer pour appliquer',
|
|
||||||
811: 'Le disque système doit faire au moins 18 Go',
|
|
||||||
812: 'Le disque de données doit faire au moins 20 Go',
|
|
||||||
813: 'Système + données combinés nécessitent au moins 38 Go',
|
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ export default {
|
|||||||
425: 'Uruchom',
|
425: 'Uruchom',
|
||||||
426: 'Akcja może być wykonana tylko gdy serwis jest',
|
426: 'Akcja może być wykonana tylko gdy serwis jest',
|
||||||
427: 'Zabronione',
|
427: 'Zabronione',
|
||||||
|
428: 'może tymczasowo napotkać problemy',
|
||||||
429: 'ma niespełnione zależności. Nie będzie działać zgodnie z oczekiwaniami.',
|
429: 'ma niespełnione zależności. Nie będzie działać zgodnie z oczekiwaniami.',
|
||||||
430: 'Odbudowywanie kontenera',
|
430: 'Odbudowywanie kontenera',
|
||||||
431: 'Rozpoczynanie odinstalowania',
|
431: 'Rozpoczynanie odinstalowania',
|
||||||
@@ -484,6 +485,7 @@ export default {
|
|||||||
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
|
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
|
||||||
513: 'Włącz',
|
513: 'Włącz',
|
||||||
514: 'Wyłącz',
|
514: 'Wyłącz',
|
||||||
|
515: 'Ta zmiana zacznie obowiązywać po następnym uruchomieniu',
|
||||||
516: 'Zalecane',
|
516: 'Zalecane',
|
||||||
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
||||||
518: 'Odrzuć',
|
518: 'Odrzuć',
|
||||||
@@ -627,8 +629,8 @@ export default {
|
|||||||
697: 'Wprowadź hasło użyte do zaszyfrowania tej kopii zapasowej.',
|
697: 'Wprowadź hasło użyte do zaszyfrowania tej kopii zapasowej.',
|
||||||
698: 'Znaleziono wiele kopii zapasowych. Wybierz, którą przywrócić.',
|
698: 'Znaleziono wiele kopii zapasowych. Wybierz, którą przywrócić.',
|
||||||
699: 'Kopie zapasowe',
|
699: 'Kopie zapasowe',
|
||||||
700: 'Dysk, na którym zostanie zainstalowany system operacyjny StartOS. Minimum 18 GB.',
|
700: 'Dysk, na którym zostanie zainstalowany system operacyjny StartOS.',
|
||||||
701: 'Dysk, na którym będą przechowywane dane StartOS (usługi, ustawienia itp.). Może to być ten sam dysk co systemowy lub oddzielny dysk. Minimum 20 GB lub 38 GB w przypadku jednego dysku na system i dane.',
|
701: 'Dysk, na którym będą przechowywane dane StartOS (usługi, ustawienia itp.). Może to być ten sam dysk co systemowy lub oddzielny dysk.',
|
||||||
702: 'Po przeniesieniu danych z tego dysku nie próbuj ponownie uruchamiać z niego systemu jako serwer Start9. Może to spowodować nieprawidłowe działanie usług, uszkodzenie danych lub utratę środków.',
|
702: 'Po przeniesieniu danych z tego dysku nie próbuj ponownie uruchamiać z niego systemu jako serwer Start9. Może to spowodować nieprawidłowe działanie usług, uszkodzenie danych lub utratę środków.',
|
||||||
703: 'Musi mieć co najmniej 12 znaków',
|
703: 'Musi mieć co najmniej 12 znaków',
|
||||||
704: 'Musi mieć maksymalnie 64 znaki',
|
704: 'Musi mieć maksymalnie 64 znaki',
|
||||||
@@ -715,15 +717,11 @@ export default {
|
|||||||
799: 'Po kliknięciu "Enroll MOK":',
|
799: 'Po kliknięciu "Enroll MOK":',
|
||||||
800: 'Po wyświetleniu monitu wprowadź swoje hasło StartOS',
|
800: 'Po wyświetleniu monitu wprowadź swoje hasło StartOS',
|
||||||
801: 'Twój system ma włączony Secure Boot, co wymaga, aby wszystkie moduły jądra były podpisane zaufanym kluczem. Niektóre sterowniki sprzętowe \u2014 takie jak te dla GPU NVIDIA \u2014 nie są podpisane domyślnym kluczem dystrybucji. Zarejestrowanie klucza podpisu StartOS pozwala firmware ufać tym modułom, aby sprzęt mógł być w pełni wykorzystany.',
|
801: 'Twój system ma włączony Secure Boot, co wymaga, aby wszystkie moduły jądra były podpisane zaufanym kluczem. Niektóre sterowniki sprzętowe \u2014 takie jak te dla GPU NVIDIA \u2014 nie są podpisane domyślnym kluczem dystrybucji. Zarejestrowanie klucza podpisu StartOS pozwala firmware ufać tym modułom, aby sprzęt mógł być w pełni wykorzystany.',
|
||||||
|
802: 'Tłumaczenia na poziomie systemu operacyjnego są już aktywne. Wymagane jest ponowne uruchomienie, aby tłumaczenia na poziomie usług zaczęły obowiązywać.',
|
||||||
803: 'Ten dysk używa ext4 i zostanie automatycznie skonwertowany na btrfs. Zdecydowanie zaleca się wykonanie kopii zapasowej przed kontynuowaniem.',
|
803: 'Ten dysk używa ext4 i zostanie automatycznie skonwertowany na btrfs. Zdecydowanie zaleca się wykonanie kopii zapasowej przed kontynuowaniem.',
|
||||||
804: 'Mam kopię zapasową moich danych',
|
804: 'Mam kopię zapasową moich danych',
|
||||||
805: 'Dodaj domenę publiczną',
|
805: 'Dodaj domenę publiczną',
|
||||||
806: 'Wynik',
|
806: 'Wynik',
|
||||||
807: 'Pobieranie zakończone. Uruchom ponownie, aby zastosować.',
|
807: 'Po otwarciu nowego adresu zostaniesz poproszony o ponowne uruchomienie.',
|
||||||
808: 'Nazwa hosta zmieniona, uruchom ponownie, aby zainstalowane usługi używały nowego adresu',
|
808: 'Ponowne uruchomienie jest wymagane, aby interfejsy usług używały nowej nazwy hosta.',
|
||||||
809: 'Język zmieniony, uruchom ponownie, aby zainstalowane usługi używały nowego języka',
|
|
||||||
810: 'Tryb kiosku zmieniony, uruchom ponownie, aby zastosować',
|
|
||||||
811: 'Dysk systemowy musi mieć co najmniej 18 GB',
|
|
||||||
812: 'Dysk danych musi mieć co najmniej 20 GB',
|
|
||||||
813: 'System + dane łącznie wymagają co najmniej 38 GB',
|
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -50,32 +50,45 @@ import { CHANGE_PASSWORD } from './change-password'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div tuiCell>
|
||||||
<div tuiCardLarge [style.align-items]="'start'">
|
<span tuiTitle>
|
||||||
<button tuiButton size="s" (click)="onChangePassword()">
|
<strong>Change password</strong>
|
||||||
Change password
|
</span>
|
||||||
</button>
|
<button tuiButton size="s" (click)="onChangePassword()">Change</button>
|
||||||
<button
|
</div>
|
||||||
tuiButton
|
<div tuiCell>
|
||||||
size="s"
|
<span tuiTitle>
|
||||||
iconStart="@tui.rotate-cw"
|
<strong>Restart</strong>
|
||||||
[loading]="restarting()"
|
<span tuiSubtitle>Restart the VPS</span>
|
||||||
(click)="onRestart()"
|
</span>
|
||||||
>
|
<button
|
||||||
Reboot VPS
|
tuiButton
|
||||||
</button>
|
size="s"
|
||||||
<button tuiButton size="s" iconStart="@tui.log-out" (click)="onLogout()">
|
appearance="secondary"
|
||||||
Logout
|
iconStart="@tui.rotate-cw"
|
||||||
</button>
|
[loading]="restarting()"
|
||||||
|
(click)="onRestart()"
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div tuiCell>
|
||||||
|
<span tuiTitle>
|
||||||
|
<strong>Logout</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="s"
|
||||||
|
appearance="secondary-destructive"
|
||||||
|
iconStart="@tui.log-out"
|
||||||
|
(click)="onLogout()"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[tuiCardLarge] {
|
[tuiCardLarge] {
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
|
|
||||||
@@ -135,9 +148,9 @@ export default class Settings {
|
|||||||
await this.api.restart()
|
await this.api.restart()
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(
|
.open(
|
||||||
'The VPS is rebooting. Please wait 1\u20132 minutes, then refresh the page.',
|
'The VPS is restarting. Please wait 1\u20132 minutes, then refresh the page.',
|
||||||
{
|
{
|
||||||
label: 'Rebooting',
|
label: 'Restarting',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ body {
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--tui-background-base, #171717), var(--tui-background-base, #171717)),
|
conic-gradient(var(--tui-background-base)),
|
||||||
radial-gradient(circle at top right, #5240a8, transparent 40%),
|
radial-gradient(circle at top right, #5240a8, transparent 40%),
|
||||||
radial-gradient(circle at bottom right, #9236c9, transparent),
|
radial-gradient(circle at bottom right, #9236c9, transparent),
|
||||||
radial-gradient(circle at 25% 100%, #5b65d5, transparent 30%),
|
radial-gradient(circle at 25% 100%, #5b65d5, transparent 30%),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { RouterOutlet } from '@angular/router'
|
import { RouterOutlet } from '@angular/router'
|
||||||
import { ErrorService, i18nPipe } from '@start9labs/shared'
|
import { ErrorService } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
@@ -39,7 +39,10 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
@if (update(); as update) {
|
@if (update(); as update) {
|
||||||
<tui-action-bar *tuiPopup="bar()">
|
<tui-action-bar *tuiPopup="bar()">
|
||||||
<span tuiCell="m">
|
<span tuiCell="m">
|
||||||
@if (
|
@if (update === true) {
|
||||||
|
<tui-icon icon="@tui.check" class="g-positive" />
|
||||||
|
Download complete, restart to apply changes
|
||||||
|
} @else if (
|
||||||
update.overall && update.overall !== true && update.overall.total
|
update.overall && update.overall !== true && update.overall.total
|
||||||
) {
|
) {
|
||||||
<tui-progress-circle
|
<tui-progress-circle
|
||||||
@@ -55,36 +58,9 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
Calculating download size
|
Calculating download size
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</tui-action-bar>
|
@if (update === true) {
|
||||||
}
|
<button tuiButton size="s" (click)="restart()">Restart</button>
|
||||||
@if (restartReason(); as reason) {
|
}
|
||||||
<tui-action-bar *tuiPopup="bar()">
|
|
||||||
<span tuiCell="m">
|
|
||||||
<tui-icon icon="@tui.refresh-cw" />
|
|
||||||
@switch (reason) {
|
|
||||||
@case ('update') {
|
|
||||||
{{ 'Download complete. Restart to apply.' | i18n }}
|
|
||||||
}
|
|
||||||
@case ('mdns') {
|
|
||||||
{{
|
|
||||||
'Hostname changed, restart for installed services to use the new address'
|
|
||||||
| i18n
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
@case ('language') {
|
|
||||||
{{
|
|
||||||
'Language changed, restart for installed services to use the new language'
|
|
||||||
| i18n
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
@case ('kiosk') {
|
|
||||||
{{ 'Kiosk mode changed, restart to apply' | i18n }}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<button tuiButton size="s" appearance="primary" (click)="restart()">
|
|
||||||
{{ 'Restart' | i18n }}
|
|
||||||
</button>
|
|
||||||
</tui-action-bar>
|
</tui-action-bar>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -138,7 +114,6 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
TuiPopup,
|
TuiPopup,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
i18nPipe,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PortalComponent {
|
export class PortalComponent {
|
||||||
@@ -149,9 +124,6 @@ export class PortalComponent {
|
|||||||
|
|
||||||
readonly name = toSignal(this.patch.watch$('serverInfo', 'name'))
|
readonly name = toSignal(this.patch.watch$('serverInfo', 'name'))
|
||||||
readonly update = toSignal(inject(OSService).updating$)
|
readonly update = toSignal(inject(OSService).updating$)
|
||||||
readonly restartReason = toSignal(
|
|
||||||
this.patch.watch$('serverInfo', 'statusInfo', 'restart'),
|
|
||||||
)
|
|
||||||
readonly bar = signal(true)
|
readonly bar = signal(true)
|
||||||
|
|
||||||
getProgress(size: number, downloaded: number): number {
|
getProgress(size: number, downloaded: number): number {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
|||||||
|
|
||||||
import { MarketplaceAlertsService } from '../services/alerts.service'
|
import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||||
|
|
||||||
type KEYS = 'id' | 'version' | 'alerts' | 'flavor' | 'satisfies'
|
type KEYS = 'id' | 'version' | 'alerts' | 'flavor'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-controls',
|
selector: 'marketplace-controls',
|
||||||
@@ -185,13 +185,9 @@ export class MarketplaceControlsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async dryInstall(url: string | null) {
|
private async dryInstall(url: string | null) {
|
||||||
const { id, version, satisfies } = this.pkg()
|
const { id, version } = this.pkg()
|
||||||
const packages = await getAllPackages(this.patch)
|
const packages = await getAllPackages(this.patch)
|
||||||
const breakages = dryUpdate(
|
const breakages = dryUpdate({ id, version }, packages, this.exver)
|
||||||
{ id, version, satisfies: satisfies || [] },
|
|
||||||
packages,
|
|
||||||
this.exver,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
|
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
|
||||||
this.installOrUpload(url)
|
this.installOrUpload(url)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { InterfaceService } from '../../../components/interfaces/interface.servi
|
|||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
iconStart="@tui.rotate-cw"
|
iconStart="@tui.rotate-cw"
|
||||||
(click)="controls.restart(manifest().id)"
|
(click)="controls.restart(manifest())"
|
||||||
>
|
>
|
||||||
{{ 'Restart' | i18n }}
|
{{ 'Restart' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TuiNotification,
|
TuiNotification,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||||
|
import * as json from 'fast-json-patch'
|
||||||
import { compare } from 'fast-json-patch'
|
import { compare } from 'fast-json-patch'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
||||||
@@ -190,7 +191,9 @@ export class ActionInputModal {
|
|||||||
task.actionId === this.actionId &&
|
task.actionId === this.actionId &&
|
||||||
task.when?.condition === 'input-not-matches' &&
|
task.when?.condition === 'input-not-matches' &&
|
||||||
task.input &&
|
task.input &&
|
||||||
conflicts(task.input.value, input),
|
json
|
||||||
|
.compare(input, task.input.value)
|
||||||
|
.some(op => op.op === 'add' || op.op === 'replace'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map(id => id)
|
.map(id => id)
|
||||||
@@ -211,26 +214,3 @@ export class ActionInputModal {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors the Rust backend's `conflicts()` function in core/src/service/action.rs.
|
|
||||||
// A key in the partial that is missing from the full input is NOT a conflict.
|
|
||||||
function conflicts(left: unknown, right: unknown): boolean {
|
|
||||||
if (
|
|
||||||
typeof left === 'object' &&
|
|
||||||
left !== null &&
|
|
||||||
!Array.isArray(left) &&
|
|
||||||
typeof right === 'object' &&
|
|
||||||
right !== null &&
|
|
||||||
!Array.isArray(right)
|
|
||||||
) {
|
|
||||||
const l = left as Record<string, unknown>
|
|
||||||
const r = right as Record<string, unknown>
|
|
||||||
return Object.keys(l).some(k => (k in r ? conflicts(l[k], r[k]) : false))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(left) && Array.isArray(right)) {
|
|
||||||
return left.some(v => right.every(vr => conflicts(v, vr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return left !== right
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
inject,
|
inject,
|
||||||
INJECTOR,
|
INJECTOR,
|
||||||
|
OnInit,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { RouterLink } from '@angular/router'
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
||||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -47,7 +48,6 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { OSService } from 'src/app/services/os.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
@@ -96,10 +96,14 @@ import { UPDATE } from './update.component'
|
|||||||
[disabled]="os.updatingOrBackingUp$ | async"
|
[disabled]="os.updatingOrBackingUp$ | async"
|
||||||
(click)="onUpdate()"
|
(click)="onUpdate()"
|
||||||
>
|
>
|
||||||
@if (os.showUpdate$ | async) {
|
@if (server.statusInfo.updated) {
|
||||||
{{ 'Update' | i18n }}
|
{{ 'Restart to apply' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
{{ 'Check for updates' | i18n }}
|
@if (os.showUpdate$ | async) {
|
||||||
|
{{ 'Update' | i18n }}
|
||||||
|
} @else {
|
||||||
|
{{ 'Check for updates' | i18n }}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +278,7 @@ import { UPDATE } from './update.component'
|
|||||||
TuiAnimated,
|
TuiAnimated,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SystemGeneralComponent {
|
export default class SystemGeneralComponent implements OnInit {
|
||||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||||
private readonly loader = inject(TuiNotificationMiddleService)
|
private readonly loader = inject(TuiNotificationMiddleService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -284,7 +288,20 @@ export default class SystemGeneralComponent {
|
|||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
private readonly injector = inject(INJECTOR)
|
private readonly injector = inject(INJECTOR)
|
||||||
private readonly win = inject(WA_WINDOW)
|
private readonly win = inject(WA_WINDOW)
|
||||||
private readonly config = inject(ConfigService)
|
private readonly route = inject(ActivatedRoute)
|
||||||
|
private readonly router = inject(Router)
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(filter(params => params['restart'] === 'hostname'))
|
||||||
|
.subscribe(async () => {
|
||||||
|
await this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: {},
|
||||||
|
})
|
||||||
|
this.promptHostnameRestart()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
@@ -304,6 +321,7 @@ export default class SystemGeneralComponent {
|
|||||||
|
|
||||||
onLanguageChange(language: Language) {
|
onLanguageChange(language: Language) {
|
||||||
this.i18nService.setLang(language.name)
|
this.i18nService.setLang(language.name)
|
||||||
|
this.promptLanguageRestart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose shared utilities for template use
|
// Expose shared utilities for template use
|
||||||
@@ -353,7 +371,9 @@ export default class SystemGeneralComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
if (this.os.updateAvailable$.value) {
|
if (this.server()?.statusInfo.updated) {
|
||||||
|
this.restart()
|
||||||
|
} else if (this.os.updateAvailable$.value) {
|
||||||
this.update()
|
this.update()
|
||||||
} else {
|
} else {
|
||||||
this.check()
|
this.check()
|
||||||
@@ -380,7 +400,7 @@ export default class SystemGeneralComponent {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subscribe(result => {
|
.subscribe(result => {
|
||||||
if (this.config.accessType === 'mdns') {
|
if (this.win.location.hostname.endsWith('.local')) {
|
||||||
this.confirmNameChange(result)
|
this.confirmNameChange(result)
|
||||||
} else {
|
} else {
|
||||||
this.saveName(result)
|
this.saveName(result)
|
||||||
@@ -413,18 +433,24 @@ export default class SystemGeneralComponent {
|
|||||||
await this.api.setHostname({ name, hostname })
|
await this.api.setHostname({ name, hostname })
|
||||||
|
|
||||||
if (wasLocal) {
|
if (wasLocal) {
|
||||||
|
const { protocol, port } = this.win.location
|
||||||
|
const portSuffix = port ? ':' + port : ''
|
||||||
|
const newUrl = `${protocol}//${hostname}.local${portSuffix}/system/general?restart=hostname`
|
||||||
|
|
||||||
this.dialog
|
this.dialog
|
||||||
.openConfirm({
|
.openConfirm({
|
||||||
label: 'Hostname Changed',
|
label: 'Hostname Changed',
|
||||||
data: {
|
data: {
|
||||||
content:
|
content:
|
||||||
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local` as i18nKey,
|
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local. ${this.i18n.transform('After opening the new address, you will be prompted to restart.')}` as i18nKey,
|
||||||
yes: 'Open new address',
|
yes: 'Open new address',
|
||||||
no: 'Dismiss',
|
no: 'Dismiss',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.pipe(filter(Boolean))
|
.pipe(filter(Boolean))
|
||||||
.subscribe(() => this.win.open(`https://${hostname}.local`, '_blank'))
|
.subscribe(() => this.win.open(newUrl, '_blank'))
|
||||||
|
} else {
|
||||||
|
this.promptHostnameRestart()
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
@@ -500,6 +526,7 @@ export default class SystemGeneralComponent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.toggleKiosk(true)
|
await this.api.toggleKiosk(true)
|
||||||
|
this.promptRestart()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -519,6 +546,7 @@ export default class SystemGeneralComponent {
|
|||||||
options: [],
|
options: [],
|
||||||
})
|
})
|
||||||
await this.api.toggleKiosk(true)
|
await this.api.toggleKiosk(true)
|
||||||
|
this.promptRestart()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -531,6 +559,7 @@ export default class SystemGeneralComponent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.toggleKiosk(false)
|
await this.api.toggleKiosk(false)
|
||||||
|
this.promptRestart()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -538,6 +567,50 @@ export default class SystemGeneralComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private promptRestart() {
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Restart to apply',
|
||||||
|
data: {
|
||||||
|
content: 'This change will take effect after the next boot',
|
||||||
|
yes: 'Restart now',
|
||||||
|
no: 'Later',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.restart())
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptHostnameRestart() {
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Restart to apply',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'A restart is required for service interfaces to use the new hostname.',
|
||||||
|
yes: 'Restart now',
|
||||||
|
no: 'Later',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.restart())
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptLanguageRestart() {
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Restart to apply',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
'OS-level translations are already in effect. A restart is required for service-level translations to take effect.',
|
||||||
|
yes: 'Restart now',
|
||||||
|
no: 'Later',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.restart())
|
||||||
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(UPDATE, {
|
.open(UPDATE, {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default class StartOsUiComponent {
|
|||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
readonly iface: T.ServiceInterface = {
|
readonly iface: T.ServiceInterface = {
|
||||||
id: 'startos-ui',
|
id: '',
|
||||||
name: 'StartOS UI',
|
name: 'StartOS UI',
|
||||||
description: this.i18n.transform(
|
description: this.i18n.transform(
|
||||||
'The web user interface for your StartOS server, accessible from any browser.',
|
'The web user interface for your StartOS server, accessible from any browser.',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { RouterLink } from '@angular/router'
|
|||||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
|
i18nKey,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
LocalizePipe,
|
LocalizePipe,
|
||||||
MarkdownPipe,
|
MarkdownPipe,
|
||||||
@@ -17,10 +18,10 @@ import {
|
|||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiExpand,
|
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiLink,
|
TuiLink,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
|
TuiExpand,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
TuiProgressCircle,
|
TuiProgressCircle,
|
||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +41,8 @@ import {
|
|||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
UpdatingState,
|
UpdatingState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
|
import { getAllPackages } from 'src/app/utils/get-package-data'
|
||||||
|
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||||
import UpdatesComponent from './updates.component'
|
import UpdatesComponent from './updates.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -102,7 +106,7 @@ import UpdatesComponent from './updates.component'
|
|||||||
size="s"
|
size="s"
|
||||||
[loading]="!ready()"
|
[loading]="!ready()"
|
||||||
[appearance]="error() ? 'destructive' : 'primary'"
|
[appearance]="error() ? 'destructive' : 'primary'"
|
||||||
(click.stop)="update()"
|
(click.stop)="onClick()"
|
||||||
>
|
>
|
||||||
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
||||||
</button>
|
</button>
|
||||||
@@ -195,7 +199,6 @@ import UpdatesComponent from './updates.component'
|
|||||||
&[colspan]:only-child {
|
&[colspan]:only-child {
|
||||||
padding: 0 3rem;
|
padding: 0 3rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +273,22 @@ export class UpdatesItemComponent {
|
|||||||
readonly local =
|
readonly local =
|
||||||
input.required<PackageDataEntry<InstalledState | UpdatingState>>()
|
input.required<PackageDataEntry<InstalledState | UpdatingState>>()
|
||||||
|
|
||||||
async update() {
|
async onClick() {
|
||||||
|
this.ready.set(false)
|
||||||
|
this.error.set('')
|
||||||
|
|
||||||
|
if (hasCurrentDeps(this.item().id, await getAllPackages(this.patch))) {
|
||||||
|
if (await this.alert()) {
|
||||||
|
await this.update()
|
||||||
|
} else {
|
||||||
|
this.ready.set(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async update() {
|
||||||
const { id, version } = this.item()
|
const { id, version } = this.item()
|
||||||
const url = this.parent.current()?.url || ''
|
const url = this.parent.current()?.url || ''
|
||||||
|
|
||||||
@@ -282,4 +300,21 @@ export class UpdatesItemComponent {
|
|||||||
this.error.set(e.message)
|
this.error.set(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async alert(): Promise<boolean> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.dialog
|
||||||
|
.openConfirm({
|
||||||
|
label: 'Warning',
|
||||||
|
size: 's',
|
||||||
|
data: {
|
||||||
|
content:
|
||||||
|
`${this.i18n.transform('Services that depend on')} ${this.local().stateInfo.manifest.title} ${this.i18n.transform('will no longer work properly and may crash.')}` as i18nKey,
|
||||||
|
yes: 'Continue',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(defaultIfEmpty(false)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export namespace Mock {
|
|||||||
export const ServerUpdated: T.ServerStatus = {
|
export const ServerUpdated: T.ServerStatus = {
|
||||||
backupProgress: null,
|
backupProgress: null,
|
||||||
updateProgress: null,
|
updateProgress: null,
|
||||||
|
updated: true,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
shuttingDown: false,
|
shuttingDown: false,
|
||||||
restart: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegistryOSUpdate: T.OsVersionInfoMap = {
|
export const RegistryOSUpdate: T.OsVersionInfoMap = {
|
||||||
@@ -459,7 +459,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -502,7 +501,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -555,7 +553,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -598,7 +595,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -653,7 +649,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -709,7 +704,6 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -763,74 +757,12 @@ export namespace Mock {
|
|||||||
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
|
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
|
||||||
marketingUrl: 'https://bitcoin.org',
|
marketingUrl: 'https://bitcoin.org',
|
||||||
docsUrls: ['https://bitcoin.org'],
|
docsUrls: ['https://bitcoin.org'],
|
||||||
releaseNotes: `# Bitcoin Core v27.0.0 Release Notes
|
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This is a major release of Bitcoin Core with significant performance improvements, new RPC methods, and critical security patches. We strongly recommend all users upgrade as soon as possible.
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
- The deprecated \`getinfo\` RPC has been fully removed. Use \`getblockchaininfo\`, \`getnetworkinfo\`, and \`getwalletinfo\` instead.
|
|
||||||
- Configuration option \`rpcallowip\` no longer accepts hostnames — only CIDR notation is supported (e.g. \`192.168.1.0/24\`).
|
|
||||||
- The wallet database format has been migrated from BerkeleyDB to SQLite. Existing wallets will be automatically converted on first load. **This migration is irreversible.**
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- **Compact Block Filters (BIP 158):** Full support for serving compact block filters to light clients over the P2P network. Enable with \`-blockfilterindex=basic -peerblockfilters=1\`.
|
|
||||||
- **Miniscript support in descriptors:** You can now use miniscript policies inside \`wsh()\` descriptors for more expressive spending conditions.
|
|
||||||
- **New RPC: \`getdescriptoractivity\`:** Returns all wallet-relevant transactions for a given set of output descriptors within a block range.
|
|
||||||
|
|
||||||
## Performance Improvements
|
|
||||||
|
|
||||||
- Block validation is now 18% faster due to improved UTXO cache management and parallel script verification.
|
|
||||||
- Initial block download (IBD) time reduced by approximately 25% on NVMe storage thanks to batched database writes.
|
|
||||||
- Memory usage during reindex reduced from ~4.2 GB to ~2.8 GB peak.
|
|
||||||
|
|
||||||
## Configuration Changes
|
|
||||||
|
|
||||||
\`\`\`ini
|
|
||||||
# New options added in this release
|
|
||||||
blockfilterindex=basic # Enable BIP 158 compact block filter index
|
|
||||||
peerblockfilters=1 # Serve compact block filters to peers
|
|
||||||
shutdownnotify=<cmd> # Execute command on clean shutdown
|
|
||||||
v2transport=1 # Prefer BIP 324 encrypted P2P connections
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
1. Fixed a race condition in the mempool acceptance logic that could cause \`submitblock\` to return stale rejection reasons under high transaction throughput.
|
|
||||||
2. Corrected fee estimation for transactions with many inputs where the estimator previously overestimated by up to 15%.
|
|
||||||
3. Resolved an edge case where \`pruneblockchain\` could delete blocks still needed by an in-progress \`rescanblockchain\` operation.
|
|
||||||
4. Fixed incorrect handling of \`OP_CHECKSIGADD\` in legacy script verification mode that could lead to consensus divergence on certain non-standard transactions.
|
|
||||||
5. Patched a denial-of-service vector where a malicious peer could send specially crafted \`inv\` messages causing excessive memory allocation in the transaction request tracker.
|
|
||||||
|
|
||||||
## Dependency Updates
|
|
||||||
|
|
||||||
| Dependency | Old Version | New Version |
|
|
||||||
|------------|-------------|-------------|
|
|
||||||
| OpenSSL | 1.1.1w | 3.0.13 |
|
|
||||||
| libevent | 2.1.12 | 2.2.1 |
|
|
||||||
| Boost | 1.81.0 | 1.84.0 |
|
|
||||||
| SQLite | 3.38.5 | 3.45.1 |
|
|
||||||
| miniupnpc | 2.2.4 | 2.2.7 |
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
For users running Bitcoin Core as a service behind a reverse proxy, note that the default RPC authentication mechanism now uses cookie-based auth by default. If you previously relied on \`rpcuser\`/\`rpcpassword\`, you must explicitly set \`rpcauth\` in your configuration file. See https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py for the auth string generator.
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
|
|
||||||
- Wallet encryption with very long passphrases (>1024 characters) may cause the wallet to become temporarily unresponsive during unlock. A fix is planned for v27.0.1.
|
|
||||||
- The \`listtransactions\` RPC may return duplicate entries when called with \`include_watchonly=true\` on descriptor wallets that share derivation paths across multiple descriptors.
|
|
||||||
|
|
||||||
For the full changelog, see https://github.com/bitcoin/bitcoin/blob/v27.0.0/doc/release-notes/release-notes-27.0.0.md#full-changelog-with-detailed-descriptions-of-every-commit-and-pull-request-merged`,
|
|
||||||
osVersion: '0.4.0',
|
osVersion: '0.4.0',
|
||||||
sdkVersion: '0.4.0-beta.49',
|
sdkVersion: '0.4.0-beta.49',
|
||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -873,7 +805,6 @@ For the full changelog, see https://github.com/bitcoin/bitcoin/blob/v27.0.0/doc/
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -926,7 +857,6 @@ For the full changelog, see https://github.com/bitcoin/bitcoin/blob/v27.0.0/doc/
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -982,7 +912,6 @@ For the full changelog, see https://github.com/bitcoin/bitcoin/blob/v27.0.0/doc/
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: PROXY_ICON,
|
icon: PROXY_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
satisfies: [],
|
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -435,20 +435,14 @@ export class MockApiService extends ApiService {
|
|||||||
async toggleKiosk(enable: boolean): Promise<null> {
|
async toggleKiosk(enable: boolean): Promise<null> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
this.mockRevision([
|
const patch = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/kiosk',
|
path: '/serverInfo/kiosk',
|
||||||
value: enable,
|
value: enable,
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
this.mockRevision([
|
this.mockRevision(patch)
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: '/serverInfo/statusInfo/restart',
|
|
||||||
value: 'kiosk',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -456,7 +450,7 @@ export class MockApiService extends ApiService {
|
|||||||
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
this.mockRevision([
|
const patch = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/name',
|
path: '/serverInfo/name',
|
||||||
@@ -467,14 +461,8 @@ export class MockApiService extends ApiService {
|
|||||||
path: '/serverInfo/hostname',
|
path: '/serverInfo/hostname',
|
||||||
value: params.hostname,
|
value: params.hostname,
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
this.mockRevision([
|
this.mockRevision(patch)
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: '/serverInfo/statusInfo/restart',
|
|
||||||
value: 'mdns',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -497,20 +485,14 @@ export class MockApiService extends ApiService {
|
|||||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
this.mockRevision([
|
const patch = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/language',
|
path: '/serverInfo/language',
|
||||||
value: params.language,
|
value: params.language,
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
this.mockRevision([
|
this.mockRevision(patch)
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: '/serverInfo/statusInfo/restart',
|
|
||||||
value: 'language',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1849,11 +1831,11 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch3: Operation<string>[] = [
|
const patch3: Operation<boolean>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/statusInfo/restart',
|
path: '/serverInfo/statusInfo/updated',
|
||||||
value: 'update',
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
op: PatchOp.REMOVE,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const mockPatchData: DataModel = {
|
|||||||
snakeHighScore: 0,
|
snakeHighScore: 0,
|
||||||
},
|
},
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
|
arch: 'x86_64',
|
||||||
id: 'abcdefgh',
|
id: 'abcdefgh',
|
||||||
version,
|
version,
|
||||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||||
@@ -226,11 +227,11 @@ export const mockPatchData: DataModel = {
|
|||||||
postInitMigrationTodos: {},
|
postInitMigrationTodos: {},
|
||||||
statusInfo: {
|
statusInfo: {
|
||||||
// currentBackup: null,
|
// currentBackup: null,
|
||||||
|
updated: false,
|
||||||
updateProgress: null,
|
updateProgress: null,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
shuttingDown: false,
|
shuttingDown: false,
|
||||||
backupProgress: null,
|
backupProgress: null,
|
||||||
restart: null,
|
|
||||||
},
|
},
|
||||||
name: 'Random Words',
|
name: 'Random Words',
|
||||||
hostname: 'random-words',
|
hostname: 'random-words',
|
||||||
@@ -239,6 +240,7 @@ export const mockPatchData: DataModel = {
|
|||||||
ntpSynced: false,
|
ntpSynced: false,
|
||||||
smtp: null,
|
smtp: null,
|
||||||
echoipUrls: ['https://ipconfig.me', 'https://ifconfig.co'],
|
echoipUrls: ['https://ipconfig.me', 'https://ifconfig.co'],
|
||||||
|
platform: 'x86_64-nonfree',
|
||||||
zram: true,
|
zram: true,
|
||||||
governor: 'performance',
|
governor: 'performance',
|
||||||
ram: 8 * 1024 * 1024 * 1024,
|
ram: 8 * 1024 * 1024 * 1024,
|
||||||
|
|||||||
@@ -84,16 +84,35 @@ export class ControlsService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async restart(id: string) {
|
async restart({ id, title }: T.Manifest) {
|
||||||
const loader = this.loader.open('Restarting').subscribe()
|
const packages = await getAllPackages(this.patch)
|
||||||
|
|
||||||
try {
|
defer(() =>
|
||||||
await this.api.restartPackage({ id })
|
hasCurrentDeps(id, packages)
|
||||||
} catch (e: any) {
|
? this.dialog
|
||||||
this.errorService.handleError(e)
|
.openConfirm({
|
||||||
} finally {
|
label: 'Warning',
|
||||||
loader.unsubscribe()
|
size: 's',
|
||||||
}
|
data: {
|
||||||
|
content:
|
||||||
|
`${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('may temporarily experiences issues')}` as i18nKey,
|
||||||
|
yes: 'Restart',
|
||||||
|
no: 'Cancel',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
: of(null),
|
||||||
|
).subscribe(async () => {
|
||||||
|
const loader = this.loader.open('Restarting').subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.restartPackage({ id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private alert(content: T.LocaleString): Promise<boolean> {
|
private alert(content: T.LocaleString): Promise<boolean> {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class OSService {
|
|||||||
.pipe(shareReplay({ bufferSize: 1, refCount: true }))
|
.pipe(shareReplay({ bufferSize: 1, refCount: true }))
|
||||||
|
|
||||||
readonly updating$ = this.statusInfo$.pipe(
|
readonly updating$ = this.statusInfo$.pipe(
|
||||||
map(status => status.updateProgress ?? false),
|
map(status => status.updateProgress ?? status.updated),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import { DataModel } from '../services/patch-db/data-model'
|
|||||||
import { getManifest } from './get-package-data'
|
import { getManifest } from './get-package-data'
|
||||||
|
|
||||||
export function dryUpdate(
|
export function dryUpdate(
|
||||||
{
|
{ id, version }: { id: string; version: string },
|
||||||
id,
|
|
||||||
version,
|
|
||||||
satisfies,
|
|
||||||
}: { id: string; version: string; satisfies: string[] },
|
|
||||||
pkgs: DataModel['packageData'],
|
pkgs: DataModel['packageData'],
|
||||||
exver: Exver,
|
exver: Exver,
|
||||||
): string[] {
|
): string[] {
|
||||||
@@ -17,24 +13,10 @@ export function dryUpdate(
|
|||||||
Object.keys(pkg.currentDependencies || {}).some(
|
Object.keys(pkg.currentDependencies || {}).some(
|
||||||
pkgId => pkgId === id,
|
pkgId => pkgId === id,
|
||||||
) &&
|
) &&
|
||||||
!versionSatisfies(
|
!exver.satisfies(
|
||||||
version,
|
version,
|
||||||
satisfies,
|
|
||||||
pkg.currentDependencies[id]?.versionRange || '',
|
pkg.currentDependencies[id]?.versionRange || '',
|
||||||
exver,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map(pkg => getManifest(pkg).title)
|
.map(pkg => getManifest(pkg).title)
|
||||||
}
|
}
|
||||||
|
|
||||||
function versionSatisfies(
|
|
||||||
version: string,
|
|
||||||
satisfies: string[],
|
|
||||||
range: string,
|
|
||||||
exver: Exver,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
exver.satisfies(version, range) ||
|
|
||||||
satisfies.some(v => exver.satisfies(v, range))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user