8 Commits

6 changed files with 42 additions and 72 deletions
Symlink
+1
View File
@@ -0,0 +1 @@
octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs
+6
View File
@@ -2,6 +2,8 @@
This plugin makes your OctoPrint instance accessible from anywhere via Tailscale Funnel, without needing to configure port forwarding, dynamic DNS, or complex firewall settings. This plugin makes your OctoPrint instance accessible from anywhere via Tailscale Funnel, without needing to configure port forwarding, dynamic DNS, or complex firewall settings.
Disclaimer: *This plugin was partially vibe-coded*.
## Features ## Features
* Enable/disable Tailscale Funnel access directly from OctoPrint's settings * Enable/disable Tailscale Funnel access directly from OctoPrint's settings
@@ -9,6 +11,10 @@ This plugin makes your OctoPrint instance accessible from anywhere via Tailscale
* Display the public URL for accessing OctoPrint remotely * Display the public URL for accessing OctoPrint remotely
* Configure the port to expose via Funnel * Configure the port to expose via Funnel
## Screenshot
![Tailscale Funnel Settings](octoprint_tailscale_funnel/octoprint_tailscale_funnel/docs/screenshots/funnel-settings.png)
## Requirements ## Requirements
* OctoPrint 1.3.0 or higher * OctoPrint 1.3.0 or higher
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+1 -1
View File
@@ -14,7 +14,7 @@ plugin_package = "octoprint_tailscale_funnel"
plugin_name = "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 # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.1.6.1" plugin_version = "0.1.6.2"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module # module
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"version": "0.1.6.1" "version": "0.1.6.2"
} }
+33 -70
View File
@@ -44,54 +44,36 @@ if [ -z "$TOKEN" ]; then
echo "GITEA_API_TOKEN not set (in .env)." >&2; exit 1 echo "GITEA_API_TOKEN not set (in .env)." >&2; exit 1
fi fi
# Derive asset name early for later use
ASSET_NAME="$(basename "$ASSET_PATH")"
BODY="Tailscale Funnel Plugin ${TAG}\n\nAutomated release." BODY="Tailscale Funnel Plugin ${TAG}\n\nAutomated release."
if [ -n "$BODY_FILE" ] && [ -f "$BODY_FILE" ]; then if [ -n "$BODY_FILE" ] && [ -f "$BODY_FILE" ]; then
BODY=$(cat "$BODY_FILE") BODY=$(cat "$BODY_FILE")
fi fi
# Build JSON payload via python (robust quoting) # Try to fetch existing release by tag first
create_payload=$(REL_TAG="$TAG" REL_NAME="$NAME" REL_BODY_TXT="$BODY" python3 - <<'PY' get_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" || true)
import json, os rel_id=$(echo "$get_resp" | jq -r '.id // empty')
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; if missing, fetch by tag
rel_id=$(python3 - <<'PY'
import sys, json
data=sys.stdin.read().strip()
if not data:
print("")
else:
try:
obj=json.loads(data)
print(obj.get('id',''))
except Exception:
print("")
PY
<<<"$create_resp")
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
get_resp=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}") # Build minimal JSON payload (use existing tag)
rel_id=$(python3 - <<'PY' create_payload=$(jq -n --arg tag "$TAG" --arg name "$NAME" --arg body "$BODY" '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}')
import sys, json
obj=json.loads(sys.stdin.read()) create_resp=$(curl -sS -X POST \
print(obj.get('id','')) -H 'Content-Type: application/json' \
PY -H "Authorization: token ${TOKEN}" \
<<<"$get_resp") -d "$create_payload" \
"${API_URL}/repos/${OWNER}/${REPO}/releases" || true)
# Extract id from create response
rel_id=$(echo "$create_resp" | jq -r '.id // empty')
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=$(echo "$list_resp" | jq -r --arg tag "$TAG" '[.[] | select(.tag_name==$tag)][0].id // empty')
fi fi
if [ -z "$rel_id" ]; then if [ -z "$rel_id" ]; then
@@ -100,37 +82,18 @@ if [ -z "$rel_id" ]; then
exit 1 exit 1
fi fi
# Delete existing asset with same name (if any) # Check if asset exists (skip upload if present)
assets_json=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets") assets_json=$(curl -sS -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets")
asset_id=$(python3 - <<'PY' asset_exists=$(echo "$assets_json" | jq -r --arg name "$ASSET_NAME" 'any(.[]; .name==$name)')
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
ASSET_NAME="$(basename "$ASSET_PATH")" <<<"$assets_json")
if [ -n "$asset_id" ]; then if [ "$asset_exists" = "true" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets/${asset_id}" >/dev/null || true echo "Asset already exists, skipping upload: ${ASSET_NAME}"
else
echo "Uploading asset: ${ASSET_NAME}"
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=$(echo "$upload_resp" | jq -r '.browser_download_url // empty')
echo "Asset uploaded: ${html_url}"
fi fi
# Upload asset echo "Release ${TAG} ready (id=${rel_id})."
upload_resp=$(curl -sS -H "Authorization: token ${TOKEN}" -F attachment=@"${ASSET_PATH}" "${API_URL}/repos/${OWNER}/${REPO}/releases/${rel_id}/assets?name=$(basename "$ASSET_PATH")")
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}"