commit b741c156a4ca849c41fd6d31264e3248bf84fada Author: machinepainting Date: Wed Apr 23 03:06:50 2025 -0700 Clean Commit Overwrite Old Files diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..bdbbde6 Binary files /dev/null and b/.DS_Store differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b2f8f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 machinepainting + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..40c6c4c --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# ComfyUI DropSend Node + +This custom node package adds automatic Dropbox upload capabilities to your ComfyUI workflows. + +## πŸš€ Features + +- **πŸ“€ Dropbox AutoUploader Node** + - Monitors a specified folder (e.g., ComfyUI's `output/`) in real time + - Automatically uploads new images to your Dropbox account + +- **πŸ› οΈ Dropbox Setup Node** + - Accepts your App Key, App Secret, and Authorization Code + - Automatically generates a refresh token and saves credentials to a `.env` file + +- **πŸ” Helpers** + - `generate_base64.py`: Easily generate your Base64 App credentials + - `get_refresh_token.py`: Standalone CLI for manually retrieving a refresh token + +--- + +## 🧩 Installation + +Place this folder inside your `ComfyUI/custom_nodes/` directory: + +```bash +ComfyUI/custom_nodes/ComfyUI_DropSendNode/ +``` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +--- + +## πŸ”§ Setup Instructions + +1. Go to the [Dropbox App Console](https://www.dropbox.com/developers/apps) and create a new app. +2. Enable `App Folder` access and note your **App Key** and **App Secret**. +3. Generate an **Authorization Code** via: + ``` + https://www.dropbox.com/oauth2/authorize?client_id=APPKEY&response_type=code&token_access_type=offline + ``` +4. Paste these into the **πŸ› οΈ Dropbox Setup** node and run it once. +5. Then use the **πŸ“€ Dropbox AutoUploader** node in your workflow to monitor and upload. + +--- + +## βœ… Example Workflow +- Connect your image-saving pipeline to the default output folder +- Add the **πŸ“€ AutoUploader** node pointing to that folder +- Files will upload as soon as they’re saved + +--- + +## πŸ§ͺ Tested On +- Python 3.10 / 3.11 +- ComfyUI latest release (2024–2025) +- Dropbox API with refresh token support + +--- + +## License +MIT diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c440aa5 --- /dev/null +++ b/__init__.py @@ -0,0 +1,13 @@ +# ComfyUI_DropSendNode/__init__.py +# Register Dropbox Setup + Run-only nodes + +from .run_dropbox_node import NODE_CLASS_MAPPINGS as RUN_CLASS, NODE_DISPLAY_NAME_MAPPINGS as RUN_DISPLAY +from .setup_dropbox_node import NODE_CLASS_MAPPINGS as SETUP_CLASS, NODE_DISPLAY_NAME_MAPPINGS as SETUP_DISPLAY + +NODE_CLASS_MAPPINGS = {} +NODE_CLASS_MAPPINGS.update(RUN_CLASS) +NODE_CLASS_MAPPINGS.update(SETUP_CLASS) + +NODE_DISPLAY_NAME_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS.update(RUN_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(SETUP_DISPLAY) diff --git a/dropbox_upload.py b/dropbox_upload.py new file mode 100644 index 0000000..be57d23 --- /dev/null +++ b/dropbox_upload.py @@ -0,0 +1,60 @@ +# ComfyUI_DropSendNode/dropbox_upload.py + +import os +import dropbox +import requests +from dotenv import load_dotenv + +def get_token(): + # βœ… 1. Prioritize direct access_token from general environment variables + access_token = os.getenv("DROPBOX_ACCESS_TOKEN") + if access_token: + return access_token + + # βœ… 2. Try refresh_token flow from general environment variables + refresh_token = os.getenv("DROPBOX_REFRESH_TOKEN") + client_id = os.getenv("DROPBOX_APP_KEY") + client_secret = os.getenv("DROPBOX_APP_SECRET") + + # βœ… 3. If missing, check for RunPod-style prefixed secrets + refresh_token = refresh_token or os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN") + access_token = os.getenv("RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN") + if access_token: + return access_token + + # πŸ” 4. Final fallback: load from .env for local users + if not all([refresh_token, client_id, client_secret]): + env_path = os.path.join(os.path.dirname(__file__), ".env") + if os.path.exists(env_path): + load_dotenv(env_path, override=True) + refresh_token = refresh_token or os.getenv("DROPBOX_REFRESH_TOKEN") or os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN") + client_id = client_id or os.getenv("DROPBOX_APP_KEY") + client_secret = client_secret or os.getenv("DROPBOX_APP_SECRET") + + if not all([refresh_token, client_id, client_secret]): + raise Exception("❌ Missing Dropbox credentials. Define them as environment variables or in a .env file.") + + response = requests.post( + "https://api.dropbox.com/oauth2/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": client_id, + "client_secret": client_secret, + }, + ) + response.raise_for_status() + return response.json()["access_token"] + +def upload_to_dropbox(local_file_path, dropbox_folder="/Apps/ComfyUI_Output_Files"): + access_token = get_token() + dbx = dropbox.Dropbox(access_token) + + file_name = os.path.basename(local_file_path) + dropbox_path = f"{dropbox_folder}/{file_name}" + + with open(local_file_path, "rb") as f: + dbx.files_upload(f.read(), dropbox_path, mode=dropbox.files.WriteMode.overwrite) + + print(f"πŸ“¦ Uploaded to Dropbox: {dropbox_path}") diff --git a/generate_base64.py b/generate_base64.py new file mode 100644 index 0000000..05ed4a9 --- /dev/null +++ b/generate_base64.py @@ -0,0 +1,10 @@ +# ComfyUI_DropSendNode/generate_base64.py + +import base64 + +app_key = input("Enter your App Key: ") +app_secret = input("Enter your App Secret: ") + +credentials = f"{app_key}:{app_secret}" +encoded = base64.b64encode(credentials.encode()).decode() +print(f"\nBasic {encoded}") diff --git a/get_refresh_token.py b/get_refresh_token.py new file mode 100644 index 0000000..5c7fafb --- /dev/null +++ b/get_refresh_token.py @@ -0,0 +1,27 @@ +# ComfyUI_DropSendNode/get_refresh_token.py + +import base64 +import requests + +APP_KEY = input("Enter your APP KEY: ") +APP_SECRET = input("Enter your APP SECRET: ") +AUTH_CODE = input("Paste the Dropbox authorization code: ") + +auth_header = base64.b64encode(f"{APP_KEY}:{APP_SECRET}".encode()).decode() +res = requests.post( + "https://api.dropbox.com/oauth2/token", + data={ + "code": AUTH_CODE, + "grant_type": "authorization_code", + "redirect_uri": "https://localhost" + }, + headers={ + "Authorization": f"Basic {auth_header}", + "Content-Type": "application/x-www-form-urlencoded" + } +) + +if res.ok: + print("\nYour refresh token is:\n", res.json().get("refresh_token")) +else: + print("Error:", res.text) diff --git a/monitor_output.py b/monitor_output.py new file mode 100644 index 0000000..a887cca --- /dev/null +++ b/monitor_output.py @@ -0,0 +1,48 @@ +# ComfyUI_DropSendNode/monitor_output.py + +import time +import os +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +from .dropbox_upload import upload_to_dropbox + +uploaded_files = set() + +def start_monitoring(folder, dropbox_dest_folder): + class Handler(FileSystemEventHandler): + def on_created(self, event): + if event.is_directory: + return + file_path = event.src_path + + # Wait for file to finish writing + while True: + try: + with open(file_path, 'rb') as f: + f.read() + break + except Exception: + time.sleep(0.5) + + if file_path not in uploaded_files: + try: + upload_to_dropbox(file_path, dropbox_dest_folder) + uploaded_files.add(file_path) + print(f"πŸ“¦βœ… Uploaded: {file_path}") + except Exception as e: + print(f"πŸ“¦βŒ Upload failed: {e}") + + # Ensure the folder exists + if not os.path.exists(folder): + os.makedirs(folder) + + observer = Observer() + observer.schedule(Handler(), folder, recursive=False) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..24e2bf4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests +python-dotenv +watchdog diff --git a/run_dropbox_node.py b/run_dropbox_node.py new file mode 100644 index 0000000..7ce1383 --- /dev/null +++ b/run_dropbox_node.py @@ -0,0 +1,64 @@ +# ComfyUI_DropSendNode/run_dropbox_node.py + +import os +import threading +from dotenv import load_dotenv, dotenv_values +from .monitor_output import start_monitoring + +class DropSendRunNode: + @classmethod + def INPUT_TYPES(cls): + node_dir = os.path.dirname(__file__) + env_path = os.path.join(node_dir, ".env") + + default_watch = os.path.join(os.getcwd(), "output") + default_dest = "/Apps/ComfyUI_Output_Files" + + # if .env exists, pull last-used folder + if os.path.exists(env_path): + cfg = dotenv_values(env_path) + default_dest = cfg.get("DROPBOX_FOLDER", default_dest) + + return { + "required": { + "watch_folder": ("STRING", {"default": default_watch}), + "dropbox_dest_folder": ("STRING", {"default": default_dest}), + } + } + + RETURN_TYPES = ("STRING",) + OUTPUT_NODE = True + FUNCTION = "start" + + def start(self, watch_folder, dropbox_dest_folder): + # persist new destination into .env alongside secrets + node_dir = os.path.dirname(__file__) + env_path = os.path.join(node_dir, ".env") + + existing = {} + if os.path.exists(env_path): + existing = dotenv_values(env_path) + + existing["DROPBOX_FOLDER"] = dropbox_dest_folder + with open(env_path, "w") as f: + for k, v in existing.items(): + f.write(f"{k}={v}\n") + + load_dotenv(env_path, override=True) + + # start (or restart) the folder watcher + thread = threading.Thread( + target=start_monitoring, + args=(watch_folder, dropbox_dest_folder), + daemon=True + ) + thread.start() + + return ( + f"πŸ“¦βœ… .env updated: DROPBOX_FOLDER={dropbox_dest_folder}", + f"πŸ”ŽπŸ“ Monitoring Folder: {watch_folder} β†’ {dropbox_dest_folder}" + ) + +# Required mappings for ComfyUI +NODE_CLASS_MAPPINGS = {"DropSendRunNode": DropSendRunNode} +NODE_DISPLAY_NAME_MAPPINGS = {"DropSendRunNode": "πŸ“¦πŸ“€ Dropbox AutoUploader Run"} diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py new file mode 100644 index 0000000..cca15ca --- /dev/null +++ b/setup_dropbox_node.py @@ -0,0 +1,72 @@ +# ComfyUI_DropSendNode/setup_dropbox_node.py + +import os +import requests +from dotenv import load_dotenv, dotenv_values + +class DropboxSetupNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "app_key": ("STRING", {"default": ""}), + "app_secret": ("STRING", {"default": ""}), + "auth_code": ("STRING", {"default": ""}), + "dropbox_dest_folder": ("STRING", {"default": "/Apps/ComfyUI_Output_Files"}), + } + } + + RETURN_TYPES = ("STRING",) + OUTPUT_NODE = True + FUNCTION = "setup" + + def setup(self, app_key, app_secret, auth_code, dropbox_dest_folder): + node_dir = os.path.dirname(__file__) + env_path = os.path.join(node_dir, ".env") + + # 1. Skip setup if generic environment variables already exist + general_env_set = all([ + os.getenv("DROPBOX_APP_KEY"), + os.getenv("DROPBOX_APP_SECRET"), + os.getenv("DROPBOX_REFRESH_TOKEN") + ]) + if general_env_set: + return ("⚠️ Dropbox credentials already set in system environment variables. Skipping .env creation.",) + + # 2. Skip setup if RunPod secrets are defined + runpod_env_set = all([ + os.getenv("RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN"), + os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN") + ]) + if runpod_env_set: + return ("⚠️ Detected RunPod secrets (RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN, RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN). Skipping .env creation.",) + + # 3. Run auth_code flow if no env config is present + if not all([app_key, app_secret, auth_code]): + return ("❌ Missing App Key, Secret, or Authorization Code.",) + + data = { + "code": auth_code, + "grant_type": "authorization_code", + "client_id": app_key, + "client_secret": app_secret + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + resp = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data) + resp.raise_for_status() + refresh_token = resp.json().get("refresh_token") + + # Write .env with fallback credentials + with open(env_path, "w") as f: + f.write(f"DROPBOX_APP_KEY={app_key}\n") + f.write(f"DROPBOX_APP_SECRET={app_secret}\n") + f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n") + f.write(f"DROPBOX_FOLDER={dropbox_dest_folder}\n") + + load_dotenv(env_path, override=True) + return ("βœ… .env file created with Dropbox credentials.",) + +# Required mappings for ComfyUI +NODE_CLASS_MAPPINGS = {"DropboxSetupNode": DropboxSetupNode} +NODE_DISPLAY_NAME_MAPPINGS = {"DropboxSetupNode": "πŸ“¦βš™οΈ Dropbox AutoUploader Setup"}