Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4d484c5023 | ||
|
547cad7a21 |
@@ -11,6 +11,13 @@ $(function() {
|
||||
self.tailscaleInstalled = ko.observable(true);
|
||||
self.tailscaleRunning = ko.observable(true);
|
||||
|
||||
// Computed URL used across UI and copy handler
|
||||
self.currentUrl = ko.pureComputed(function() {
|
||||
var url = (typeof self.publicUrl === 'function') ? self.publicUrl() : self.publicUrl;
|
||||
if (!url || url === "Not available") return "";
|
||||
return ("" + url).trim();
|
||||
});
|
||||
|
||||
// Button states
|
||||
self.refreshInProgress = ko.observable(false);
|
||||
self.toggleInProgress = ko.observable(false);
|
||||
@@ -126,10 +133,12 @@ $(function() {
|
||||
});
|
||||
};
|
||||
|
||||
// Copy URL to clipboard
|
||||
self.copyUrlToClipboard = function() {
|
||||
if (self.publicUrl() && self.publicUrl() !== "Not available") {
|
||||
navigator.clipboard.writeText(self.publicUrl()).then(function() {
|
||||
// Copy URL to clipboard (robust, event-safe)
|
||||
self.copyUrlToClipboard = function(_, event) {
|
||||
if (event && event.stopPropagation) event.stopPropagation();
|
||||
var url = self.currentUrl();
|
||||
if (!url) return;
|
||||
(navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText(url) : Promise.reject()).then(function() {
|
||||
new PNotify({
|
||||
title: "Copied to Clipboard",
|
||||
text: "Public URL copied to clipboard",
|
||||
@@ -138,7 +147,7 @@ $(function() {
|
||||
}, function() {
|
||||
// Fallback for older browsers
|
||||
var textArea = document.createElement("textarea");
|
||||
textArea.value = self.publicUrl();
|
||||
textArea.value = url;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
@@ -157,7 +166,6 @@ $(function() {
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle messages from the backend
|
||||
|
@@ -2,6 +2,7 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
|
||||
@@ -201,22 +202,49 @@ class TailscaleInterface:
|
||||
if not self.is_tailscale_installed():
|
||||
raise TailscaleNotInstalledError("Tailscale is not installed")
|
||||
|
||||
# Try new CLI first, then legacy
|
||||
cmds = [
|
||||
# Newer: turn off serve mapping and remove funnel config
|
||||
"tailscale serve --http={p} off".format(p=port),
|
||||
# Execute multiple disable attempts tolerantly (idempotent)
|
||||
commands = [
|
||||
# Try to disable funnel first (new + legacy)
|
||||
"tailscale funnel reset",
|
||||
# Legacy
|
||||
"tailscale funnel {p} off".format(p=port),
|
||||
# Then disable serve mappings (newer variants)
|
||||
"tailscale serve --http={p} off".format(p=port),
|
||||
"tailscale serve --https={p} off".format(p=port),
|
||||
]
|
||||
result = self._run_first_success(cmds)
|
||||
if result["success"]:
|
||||
# Double-check disabled
|
||||
|
||||
any_succeeded = False
|
||||
for cmd in commands:
|
||||
res = self._run_first_success([cmd])
|
||||
if res and res.get("success"):
|
||||
any_succeeded = True
|
||||
else:
|
||||
# Treat "not enabled"/"no such" errors as harmless for idempotency
|
||||
err = (res or {}).get("error") or ""
|
||||
if isinstance(err, str) and (
|
||||
"not enabled" in err.lower()
|
||||
or "no handlers" in err.lower()
|
||||
or "no such" in err.lower()
|
||||
or "not found" in err.lower()
|
||||
or "already off" in err.lower()
|
||||
):
|
||||
any_succeeded = True
|
||||
|
||||
# Poll for a short time until status reflects disabled
|
||||
for _ in range(6): # ~3.0s total
|
||||
try:
|
||||
if not self.is_funnel_enabled():
|
||||
return True
|
||||
except Exception:
|
||||
# If status check fails, assume disabled if we attempted commands
|
||||
if any_succeeded:
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
|
||||
# Final status check
|
||||
try:
|
||||
return not self.is_funnel_enabled()
|
||||
except Exception:
|
||||
return True
|
||||
return False
|
||||
return any_succeeded
|
||||
|
||||
def get_public_url(self):
|
||||
"""
|
||||
|
@@ -34,9 +34,9 @@
|
||||
<span id="tailscale_funnel_url" class="input-xlarge uneditable-input" data-bind="text: publicUrl">
|
||||
Not available
|
||||
</span>
|
||||
<button id="tailscale_funnel_copy_url_btn" class="btn" type="button" data-bind="click: copyUrlToClipboard, enable: publicUrl() && publicUrl() !== 'Not available'">
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
<a id="tailscale_funnel_open_url_btn" class="btn" target="_blank" rel="noopener" data-bind="attr: { href: currentUrl }, visible: currentUrl()">
|
||||
<i class="fas fa-external-link-alt"></i> Open
|
||||
</a>
|
||||
</div>
|
||||
<span class="help-block">Public URL for accessing your OctoPrint instance</span>
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@ plugin_package = "octoprint_tailscale_funnel"
|
||||
plugin_name = "OctoPrint-Tailscale-Funnel"
|
||||
|
||||
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
|
||||
plugin_version = "0.1.5"
|
||||
plugin_version = "0.1.6.2"
|
||||
|
||||
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
|
||||
# module
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "0.1.5"
|
||||
"version": "0.1.6.2"
|
||||
}
|
||||
|
||||
|
160
scripts/release_gitea.sh
Normal file
160
scripts/release_gitea.sh
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Load .env if present
|
||||
if [ -f "$(dirname "$0")/../.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. "$(dirname "$0")/../.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
API_URL=${GITEA_API_URL:-"https://gitea.elpatron.me/api/v1"}
|
||||
OWNER=${GITEA_OWNER:-"elpatron"}
|
||||
REPO=${GITEA_REPO:-"octo-funnel"}
|
||||
TOKEN=${GITEA_API_TOKEN:-""}
|
||||
|
||||
TAG=""
|
||||
NAME=""
|
||||
ASSET_PATH=""
|
||||
BODY_FILE=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -t <tag> -a <asset-zip> [-n <name>] [-b <body-file>]" >&2
|
||||
}
|
||||
|
||||
while getopts ":t:a:n:b:" opt; do
|
||||
case $opt in
|
||||
t) TAG="$OPTARG" ;;
|
||||
a) ASSET_PATH="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
b) BODY_FILE="$OPTARG" ;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$TAG" ] || [ -z "$ASSET_PATH" ]; then
|
||||
usage; exit 2
|
||||
fi
|
||||
if [ -z "$NAME" ]; then NAME="$TAG"; fi
|
||||
if [ ! -f "$ASSET_PATH" ]; then
|
||||
echo "Asset not found: $ASSET_PATH" >&2; exit 1
|
||||
fi
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "GITEA_API_TOKEN not set (in .env)." >&2; exit 1
|
||||
fi
|
||||
|
||||
# Derive asset name early for later use
|
||||
ASSET_NAME="$(basename "$ASSET_PATH")"
|
||||
|
||||
BODY="Tailscale Funnel Plugin ${TAG}\n\nAutomated release."
|
||||
if [ -n "$BODY_FILE" ] && [ -f "$BODY_FILE" ]; then
|
||||
BODY=$(cat "$BODY_FILE")
|
||||
fi
|
||||
|
||||
# Try to fetch existing release by tag first
|
||||
get_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" || true)
|
||||
rel_id=$(python3 - <<'PY'
|
||||
import sys, json
|
||||
data=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(data) if data else {}
|
||||
print(obj.get('id',''))
|
||||
except Exception:
|
||||
print('')
|
||||
PY
|
||||
<<<"$get_resp")
|
||||
|
||||
if [ -z "$rel_id" ]; then
|
||||
# Build minimal JSON payload (use existing tag)
|
||||
create_payload=$(REL_TAG="$TAG" REL_NAME="$NAME" REL_BODY_TXT="$BODY" python3 - <<'PY'
|
||||
import json, os
|
||||
payload = {
|
||||
"tag_name": os.environ["REL_TAG"],
|
||||
"name": os.environ["REL_NAME"],
|
||||
"body": os.environ["REL_BODY_TXT"],
|
||||
"draft": False,
|
||||
"prerelease": False
|
||||
}
|
||||
print(json.dumps(payload))
|
||||
PY
|
||||
)
|
||||
|
||||
create_resp=$(curl -sS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-d "$create_payload" \
|
||||
"${API_URL}/repos/${OWNER}/${REPO}/releases" || true)
|
||||
|
||||
# Extract id from create response
|
||||
rel_id=$(python3 - <<'PY'
|
||||
import sys, json
|
||||
data=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(data) if data else {}
|
||||
print(obj.get('id',''))
|
||||
except Exception:
|
||||
print('')
|
||||
PY
|
||||
<<<"$create_resp")
|
||||
fi
|
||||
|
||||
# Fallback: search releases list for matching tag if still empty
|
||||
if [ -z "$rel_id" ]; then
|
||||
list_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases?limit=100")
|
||||
rel_id=$(python3 - <<'PY'
|
||||
import sys, json, os
|
||||
data=sys.stdin.read().strip()
|
||||
try:
|
||||
arr=json.loads(data) if data else []
|
||||
tag=os.environ.get('TAG')
|
||||
for rel in arr:
|
||||
if rel.get('tag_name')==tag:
|
||||
print(rel.get('id',''))
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
PY
|
||||
<<<"$list_resp")
|
||||
fi
|
||||
|
||||
if [ -z "$rel_id" ]; then
|
||||
echo "Failed to get create/fetch release id" >&2
|
||||
echo "$create_resp" | head -c 400 >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete existing asset with same name (if any)
|
||||
assets_json=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets")
|
||||
asset_id=$(python3 - <<'PY'
|
||||
import sys, json, os
|
||||
name=os.environ['ASSET_NAME']
|
||||
try:
|
||||
arr=json.loads(sys.stdin.read())
|
||||
for a in arr:
|
||||
if a.get('name')==name:
|
||||
print(a.get('id',''))
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
PY
|
||||
<<<"$assets_json")
|
||||
|
||||
if [ -n "$asset_id" ]; then
|
||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets/${asset_id}" >/dev/null || true
|
||||
fi
|
||||
|
||||
# Upload asset
|
||||
upload_resp=$(curl -sS -H "Authorization: token ${TOKEN}" -F attachment=@"${ASSET_PATH}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets?name=${ASSET_NAME}")
|
||||
|
||||
html_url=$(python3 - <<'PY'
|
||||
import sys, json
|
||||
try:
|
||||
print(json.loads(sys.stdin.read()).get('browser_download_url',''))
|
||||
except Exception:
|
||||
print("")
|
||||
PY
|
||||
<<<"$upload_resp")
|
||||
|
||||
echo "Release ${TAG} created (id=${rel_id}). Asset uploaded: ${html_url}"
|
||||
|
117
scripts/upload_asset_direct.sh
Executable file
117
scripts/upload_asset_direct.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set +u
|
||||
|
||||
# Load .env
|
||||
if [ -f "$(dirname "$0")/../.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. "$(dirname "$0")/../.env"
|
||||
fi
|
||||
|
||||
API_URL=${GITEA_API_URL:-"https://gitea.elpatron.me/api/v1"}
|
||||
OWNER=${GITEA_OWNER:-"elpatron"}
|
||||
REPO=${GITEA_REPO:-"octo-funnel"}
|
||||
TOKEN=${GITEA_API_TOKEN:-""}
|
||||
|
||||
TAG=${1:-v0.1.6.1}
|
||||
ASSET_PATH=${2:-"$(pwd)/octoprint_tailscale_funnel/dist/OctoPrint-Tailscale-Funnel-0.1.6.1.zip"}
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "GITEA_API_TOKEN not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$ASSET_PATH" ]; then
|
||||
echo "Asset not found: $ASSET_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ASSET_NAME=$(basename "$ASSET_PATH")
|
||||
|
||||
# Get release by tag
|
||||
REL_JSON=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases/tags/$TAG" || true)
|
||||
REL_ID=$(python3 - <<'PY'
|
||||
import sys, json
|
||||
s=sys.stdin.read().strip()
|
||||
try:
|
||||
d=json.loads(s) if s else {}
|
||||
print(d.get('id',''))
|
||||
except Exception:
|
||||
print('')
|
||||
PY
|
||||
<<<"$REL_JSON")
|
||||
|
||||
# Fallback list search
|
||||
if [ -z "$REL_ID" ]; then
|
||||
LIST=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases?limit=100")
|
||||
REL_ID=$(TAG="$TAG" python3 - <<'PY'
|
||||
import os, sys, json
|
||||
tag=os.environ['TAG']
|
||||
try:
|
||||
arr=json.loads(sys.stdin.read())
|
||||
for r in arr:
|
||||
if r.get('tag_name')==tag:
|
||||
print(r.get('id',''))
|
||||
break
|
||||
except Exception:
|
||||
print('')
|
||||
PY
|
||||
<<<"$LIST")
|
||||
fi
|
||||
|
||||
# Create release if still missing
|
||||
if [ -z "$REL_ID" ]; then
|
||||
PAYLOAD=$(TAG="$TAG" python3 - <<'PY'
|
||||
import json, os
|
||||
print(json.dumps({
|
||||
'tag_name': os.environ['TAG'],
|
||||
'name': os.environ['TAG'],
|
||||
'body': 'Automated release'
|
||||
}))
|
||||
PY
|
||||
)
|
||||
CREATE=$(curl -sS -X POST -H 'Content-Type: application/json' -H "Authorization: token $TOKEN" -d "$PAYLOAD" "$API_URL/repos/$OWNER/$REPO/releases")
|
||||
REL_ID=$(python3 - <<'PY'
|
||||
import sys, json
|
||||
try:
|
||||
d=json.loads(sys.stdin.read())
|
||||
print(d.get('id',''))
|
||||
except Exception:
|
||||
print('')
|
||||
PY
|
||||
<<<"$CREATE")
|
||||
fi
|
||||
|
||||
if [ -z "$REL_ID" ]; then
|
||||
echo "Failed to resolve release id for tag $TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check existing assets
|
||||
ASSETS=$(curl -sS -H "Authorization: token $TOKEN" "$API_URL/repos/$OWNER/$REPO/releases/$REL_ID/assets")
|
||||
HAS=$(NAME="$ASSET_NAME" python3 - <<'PY'
|
||||
import os, sys, json
|
||||
name=os.environ['NAME']
|
||||
try:
|
||||
arr=json.loads(sys.stdin.read())
|
||||
print(any(a.get('name')==name for a in arr))
|
||||
except Exception:
|
||||
print(False)
|
||||
PY
|
||||
<<<"$ASSETS")
|
||||
|
||||
if [ "$HAS" = "True" ]; then
|
||||
echo "Asset already exists: $ASSET_NAME"
|
||||
else
|
||||
echo "Uploading asset: $ASSET_NAME"
|
||||
curl -sS -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$ASSET_PATH" \
|
||||
"$API_URL/repos/$OWNER/$REPO/releases/$REL_ID/assets?name=$ASSET_NAME" >/dev/null
|
||||
echo "Upload done."
|
||||
fi
|
||||
|
||||
DL_URL="https://gitea.elpatron.me/$OWNER/$REPO/releases/download/$TAG/$ASSET_NAME"
|
||||
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$DL_URL")
|
||||
echo "DOWNLOAD_URL=$DL_URL"
|
||||
echo "HTTP_STATUS=$CODE"
|
||||
|
||||
|
Reference in New Issue
Block a user