diff --git a/scripts/check_modules.py b/scripts/check_modules.py index f8ca8ce3..527410c5 100644 --- a/scripts/check_modules.py +++ b/scripts/check_modules.py @@ -23,6 +23,7 @@ modules_to_check = { "rich": "12.6.0", "uvicorn": "0.19.0", "fastapi": "0.85.1", + "pycloudflared": "0.2.0", # "xformers": "0.0.16", } diff --git a/ui/easydiffusion/server.py b/ui/easydiffusion/server.py index a1aab6c0..b9ffe5f9 100644 --- a/ui/easydiffusion/server.py +++ b/ui/easydiffusion/server.py @@ -15,6 +15,7 @@ from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Extra from starlette.responses import FileResponse, JSONResponse, StreamingResponse +from pycloudflared import try_cloudflare log.info(f"started in {app.SD_DIR}") log.info(f"started at {datetime.datetime.now():%x %X}") @@ -113,6 +114,14 @@ def init(): def get_image(task_id: int, img_id: int): return get_image_internal(task_id, img_id) + @server_api.post("/tunnel/cloudflare/start") + def start_cloudflare_tunnel(req: dict): + return start_cloudflare_tunnel_internal(req) + + @server_api.post("/tunnel/cloudflare/stop") + def stop_cloudflare_tunnel(req: dict): + return stop_cloudflare_tunnel_internal(req) + @server_api.get("/") def read_root(): return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS) @@ -211,6 +220,8 @@ def ping_internal(session_id: str = None): session = task_manager.get_cached_session(session_id, update_ttl=True) response["tasks"] = {id(t): t.status for t in session.tasks} response["devices"] = task_manager.get_devices() + if cloudflare.address != None: + response["cloudflare"] = cloudflare.address return JSONResponse(response, headers=NOCACHE_HEADERS) @@ -322,3 +333,46 @@ def get_image_internal(task_id: int, img_id: int): return StreamingResponse(img_data, media_type="image/jpeg") except KeyError as e: raise HTTPException(status_code=500, detail=str(e)) + +#---- Cloudflare Tunnel ---- +class CloudflareTunnel: + def __init__(self): + config = app.getConfig() + self.Urls = None + self.port = config["net"]["listen_port"] + + def start(self): + self.Urls = try_cloudflare(self.port) + + def stop(self): + if self.Urls != None: + try_cloudflare.terminate(self.port) + self.Urls = None + + @property + def address(self): + if self.Urls != None: + return self.Urls.tunnel + else: + return None + +cloudflare = CloudflareTunnel() + +def start_cloudflare_tunnel_internal(req: dict): + try: + cloudflare.start() + log.info(f"- Started cloudflare tunnel. Using address: {cloudflare.address}") + return JSONResponse({"address":cloudflare.address}) + except Exception as e: + log.error(str(e)) + log.error(traceback.format_exc()) + return HTTPException(status_code=500, detail=str(e)) + +def stop_cloudflare_tunnel_internal(req: dict): + try: + cloudflare.stop() + except Exception as e: + log.error(str(e)) + log.error(traceback.format_exc()) + return HTTPException(status_code=500, detail=str(e)) + diff --git a/ui/index.html b/ui/index.html index 6cc6978c..8558cc2c 100644 --- a/ui/index.html +++ b/ui/index.html @@ -361,10 +361,16 @@

System Settings

-
+



+
+

Share Easy Diffusion

+
+
+
+

System Info

@@ -539,7 +545,8 @@ async function init() { SD.init({ events: { statusChange: setServerStatus, - idle: onIdle + idle: onIdle, + ping: tunnelUpdate } }) diff --git a/ui/media/css/auto-save.css b/ui/media/css/auto-save.css index 80aa48d8..119a7e10 100644 --- a/ui/media/css/auto-save.css +++ b/ui/media/css/auto-save.css @@ -69,13 +69,15 @@ } .parameters-table > div:first-child { - border-radius: 12px 12px 0px 0px; + border-top-left-radius: 12px; + border-top-right-radius: 12px; } .parameters-table > div:last-child { - border-radius: 0px 0px 12px 12px; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; } .parameters-table .fa-fire { color: #F7630C; -} \ No newline at end of file +} diff --git a/ui/media/css/main.css b/ui/media/css/main.css index e2d1f79a..bd3cdbe6 100644 --- a/ui/media/css/main.css +++ b/ui/media/css/main.css @@ -1309,6 +1309,21 @@ body.wait-pause { padding-left: 5pt; } +#cloudflare-address { + background-color: var(--background-color3); + padding: 6px; + border-radius: var(--input-border-radius); + border: var(--input-border-size) solid var(--input-border-color); + margin-top: 0.2em; + margin-bottom: 0.2em; + display: inline-block; +} + +#copy-cloudflare-address { + padding: 4px 8px; + margin-left: 0.5em; +} + /* TOAST NOTIFICATIONS */ .toast-notification { position: fixed; diff --git a/ui/media/js/engine.js b/ui/media/js/engine.js index e60409f1..f3ce0551 100644 --- a/ui/media/js/engine.js +++ b/ui/media/js/engine.js @@ -186,6 +186,7 @@ const EVENT_TASK_START = "taskStart" const EVENT_TASK_END = "taskEnd" const EVENT_TASK_ERROR = "task_error" + const EVENT_PING = "ping" const EVENT_UNEXPECTED_RESPONSE = "unexpectedResponse" const EVENTS_TYPES = [ EVENT_IDLE, @@ -196,6 +197,7 @@ EVENT_TASK_START, EVENT_TASK_END, EVENT_TASK_ERROR, + EVENT_PING, EVENT_UNEXPECTED_RESPONSE, ] @@ -240,6 +242,7 @@ setServerStatus("error", "offline") return false } + // Set status switch (serverState.status) { case ServerStates.init: @@ -261,6 +264,7 @@ break } serverState.time = Date.now() + await eventSource.fireEvent(EVENT_PING, serverState) return true } catch (e) { console.error(e) diff --git a/ui/media/js/main.js b/ui/media/js/main.js index e4043fa0..1684046f 100644 --- a/ui/media/js/main.js +++ b/ui/media/js/main.js @@ -1979,6 +1979,38 @@ resumeBtn.addEventListener("click", function() { document.body.classList.remove("wait-pause") }) +function tunnelUpdate(event) { + if ("cloudflare" in event) { + document.getElementById('cloudflare-off').classList.add("displayNone") + document.getElementById('cloudflare-on').classList.remove("displayNone") + cloudflareAddressField.innerHTML = event.cloudflare + document.getElementById('toggle-cloudflare-tunnel').innerHTML = "Stop" + } else { + document.getElementById('cloudflare-on').classList.add("displayNone") + document.getElementById('cloudflare-off').classList.remove("displayNone") + document.getElementById('toggle-cloudflare-tunnel').innerHTML = "Start" + } +} + +document.getElementById('toggle-cloudflare-tunnel').addEventListener("click", async function() { + let command = "stop" + if (document.getElementById('toggle-cloudflare-tunnel').innerHTML == "Start") { + command = "start" + } + showToast(`Cloudflare tunnel ${command} initiated. Please wait.`) + + let res = await fetch("/tunnel/cloudflare/"+command, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }) + res = await res.json() + + console.log(`Cloudflare tunnel ${command} result:`, res) +}) + /* Pause function */ document.querySelectorAll(".tab").forEach(linkTabContents) diff --git a/ui/media/js/parameters.js b/ui/media/js/parameters.js index 373de58d..2f915eeb 100644 --- a/ui/media/js/parameters.js +++ b/ui/media/js/parameters.js @@ -11,6 +11,12 @@ var ParameterType = { custom: "custom", } +/** + * Element shortcuts + */ +let parametersTable = document.querySelector("#system-settings-table") +let networkParametersTable = document.querySelector("#system-settings-network-table") + /** * JSDoc style * @typedef {object} Parameter @@ -186,6 +192,7 @@ var PARAMETERS = [ icon: "fa-network-wired", default: true, saveInAppConfig: true, + table: networkParametersTable, }, { id: "listen_port", @@ -198,6 +205,7 @@ var PARAMETERS = [ return `` }, saveInAppConfig: true, + table: networkParametersTable, }, { id: "use_beta_channel", @@ -218,6 +226,21 @@ var PARAMETERS = [ default: false, saveInAppConfig: true, }, + { + id: "cloudflare", + type: ParameterType.custom, + label: "Cloudflare tunnel", + note: `Create a VPN tunnel to share your Easy Diffusion instance with your friends. This will + generate a web server address on the public Internet for your Easy Diffusion instance. +
This Easy Diffusion server is available on the Internet using the + address:
+ Anyone knowing this address can access your server. The address of your server will change each time + you share a session.
+ Uses Cloudflare services.`, + icon: ["fa-brands", "fa-cloudflare"], + render: () => '', + table: networkParametersTable, + } ] function getParameterSettingsEntry(id) { @@ -266,7 +289,6 @@ function getParameterElement(parameter) { } } -let parametersTable = document.querySelector("#system-settings .parameters-table") /** * fill in the system settings popup table * @param {Array | undefined} parameters @@ -293,7 +315,10 @@ function initParameters(parameters) { noteElements.push(noteElement) } - const icon = parameter.icon ? [createElement("i", undefined, ["fa", parameter.icon])] : [] + if (typeof(parameter.icon) == "string") { + parameter.icon = [parameter.icon] + } + const icon = parameter.icon ? [createElement("i", undefined, ["fa", ...parameter.icon])] : [] const label = typeof parameter.label === "function" ? parameter.label(parameter) : parameter.label const labelElement = createElement("label", { for: parameter.id }) @@ -313,7 +338,13 @@ function initParameters(parameters) { elementWrapper, ] ) - parametersTable.appendChild(newrow) + + let p = parametersTable + if (parameter.table) { + p = parameter.table + } + p.appendChild(newrow) + parameter.settingsEntry = newrow }) } @@ -667,8 +698,25 @@ saveSettingsBtn.addEventListener("click", function() { }) const savePromise = changeAppConfig(updateAppConfigRequest) + showToast("Settings saved") saveSettingsBtn.classList.add("active") Promise.all([savePromise, asyncDelay(300)]).then(() => saveSettingsBtn.classList.remove("active")) }) +listenToNetworkField.addEventListener("change", debounce( ()=>{ + saveSettingsBtn.click() +}, 1000)) + +listenPortField.addEventListener("change", debounce( ()=>{ + saveSettingsBtn.click() +}, 1000)) + +let copyCloudflareAddressBtn = document.querySelector("#copy-cloudflare-address") +let cloudflareAddressField = document.getElementById("cloudflare-address") + +copyCloudflareAddressBtn.addEventListener("click", (e) => { + navigator.clipboard.writeText(cloudflareAddressField.innerHTML) + showToast("Copied server address to clipboard") +}) + document.addEventListener("system_info_update", (e) => setDeviceInfo(e.detail))