diff --git a/src/comfyui_ino_nodes/s3_helper/s3_download_audio_node.py b/src/comfyui_ino_nodes/s3_helper/s3_download_audio_node.py index a4ecf81..e789b19 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_download_audio_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_download_audio_node.py @@ -2,39 +2,45 @@ from pathlib import Path from inopyutils import ino_is_err, InoUtilHelper -import folder_paths +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3DownloadAudio: + +class InoS3DownloadAudio(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": "input/example.wav"}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "temp"}), - "folder": ("STRING", {"default": ""}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3DownloadAudio", + display_name="Ino S3 Download Audio", + category="InoS3Helper", + description="Downloads an audio file from S3 and returns it as an AUDIO tensor.", + inputs=[ + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default="input/example.wav"), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="temp"), + io.String.Input("folder", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.Audio.Output(display_name="audio"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "AUDIO", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", "audio", ) - FUNCTION = "function" - - async def function(self, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None): + @classmethod + async def execute(cls, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "", None, ) + return io.NodeOutput(False, "not enabled", "", "", None) validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", None, ) + return io.NodeOutput(False, validate_s3_key["msg"], "", "", None) random_str = InoUtilHelper.get_date_time_utc_base64() file_name = f'{random_str}{Path(s3_key).suffix}' @@ -44,24 +50,20 @@ class InoS3DownloadAudio: s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "", None, ) + return io.NodeOutput(False, s3_instance["msg"], "", "", None) s3_instance = s3_instance["instance"] - downloaded = await s3_instance.download_file( - s3_key=s3_key, - local_file_path=abs_path - ) + downloaded = await s3_instance.download_file(s3_key=s3_key, local_file_path=abs_path) if not downloaded["success"]: - return (downloaded["success"], downloaded["msg"], rel_path, abs_path, None, ) + return io.NodeOutput(downloaded["success"], downloaded["msg"], rel_path, abs_path, None) from comfy_extras.nodes_audio import LoadAudio - # Build annotated name for ComfyUI's audio loader folder_suffix = folder if folder else "" annotated_name = f"{folder_suffix}/{file_name} [{parent_folder}]" if folder_suffix else f"{file_name} [{parent_folder}]" load_audio = LoadAudio.execute(audio=annotated_name) if load_audio[0]: - return (True, "Success", rel_path, abs_path, load_audio[0],) + return io.NodeOutput(True, "Success", rel_path, abs_path, load_audio[0]) - return (False, "failed to load the audio", rel_path, abs_path, None,) + return io.NodeOutput(False, "failed to load the audio", rel_path, abs_path, None) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_download_file_node.py b/src/comfyui_ino_nodes/s3_helper/s3_download_file_node.py index c866405..15d9fd8 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_download_file_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_download_file_node.py @@ -1,53 +1,56 @@ from pathlib import Path -import folder_paths -from inopyutils import ino_ok, ino_err, ino_is_err +from inopyutils import ino_is_err + +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3DownloadFile: + +class InoS3DownloadFile(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": "input/example.png"}), - "parent_folder": (PARENT_FOLDER_OPTIONS,), - "folder": ("STRING", {"default": "s3download/"}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3DownloadFile", + display_name="Ino S3 Download File", + category="InoS3Helper", + description="Downloads a file from S3 to a local folder.", + inputs=[ + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default="input/example.png"), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default="s3download/"), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", ) - FUNCTION = "function" - - async def function(self, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None): + @classmethod + async def execute(cls, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "",) + return io.NodeOutput(False, "not enabled", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "",) + return io.NodeOutput(False, validate_s3_key["msg"], "", "") save_path = S3Helper.get_save_path(s3_key, folder) - rel_path, abs_path = resolve_comfy_path(parent_folder, save_path) + rel_path, abs_path = resolve_comfy_path(parent_folder, str(save_path)) if not Path(abs_path).is_dir(): Path(abs_path).parent.mkdir(parents=True, exist_ok=True) s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "",) + return io.NodeOutput(False, s3_instance["msg"], "", "") s3_instance = s3_instance["instance"] - s3_result = await s3_instance.download_file( - s3_key=s3_key, - local_file_path=abs_path - ) - return (s3_result["success"], s3_result["msg"], rel_path, abs_path, ) + s3_result = await s3_instance.download_file(s3_key=s3_key, local_file_path=abs_path) + return io.NodeOutput(s3_result["success"], s3_result["msg"], rel_path, abs_path) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_download_folder_node.py b/src/comfyui_ino_nodes/s3_helper/s3_download_folder_node.py index 8985623..3f1fa56 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_download_folder_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_download_folder_node.py @@ -1,59 +1,59 @@ from pathlib import Path -import folder_paths +from inopyutils import ino_is_err -from inopyutils import ino_ok, ino_err, ino_is_err +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3DownloadFolder: + +class InoS3DownloadFolder(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": "input/example.png"}), - "parent_folder": (PARENT_FOLDER_OPTIONS,), - "folder": ("STRING", {"default": "input/"}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - "max_concurrent": ("INT", {"default": 5, "min": 1, "max": 10}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3DownloadFolder", + display_name="Ino S3 Download Folder", + category="InoS3Helper", + description="Downloads an entire folder from S3 with concurrent downloads.", + inputs=[ + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default="input/example.png"), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default="input/"), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + io.Int.Input("max_concurrent", default=5, min=1, max=10, optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", ) - FUNCTION = "function" - - async def function(self, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None, max_concurrent=5): + @classmethod + async def execute(cls, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None, max_concurrent=5) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "", ) + return io.NodeOutput(False, "not enabled", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "",) + return io.NodeOutput(False, validate_s3_key["msg"], "", "") rel_path, abs_path = resolve_comfy_path(parent_folder, folder) if Path(abs_path).is_file(): - return (False, "Save path is a file", "", "",) + return io.NodeOutput(False, "Save path is a file", "", "") if not Path(abs_path).is_dir(): Path(abs_path).mkdir(parents=True, exist_ok=True) s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "",) + return io.NodeOutput(False, s3_instance["msg"], "", "") s3_instance = s3_instance["instance"] - s3_result = await s3_instance.download_folder( - s3_folder_key=s3_key, - local_folder_path=abs_path, - max_concurrent=max_concurrent - ) - - return (s3_result["success"], s3_result["msg"], rel_path, abs_path, ) + s3_result = await s3_instance.download_folder(s3_folder_key=s3_key, local_folder_path=abs_path, max_concurrent=max_concurrent) + return io.NodeOutput(s3_result["success"], s3_result["msg"], rel_path, abs_path) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_download_image_node.py b/src/comfyui_ino_nodes/s3_helper/s3_download_image_node.py index 5e7b135..92f7aa2 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_download_image_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_download_image_node.py @@ -2,41 +2,48 @@ from pathlib import Path from inopyutils import ino_is_err, InoUtilHelper -import folder_paths +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path, load_image -class InoS3DownloadImage: + +class InoS3DownloadImage(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": "input/example.png"}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "temp"}), - "folder": ("STRING", {"default": ""}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3DownloadImage", + display_name="Ino S3 Download Image", + category="InoS3Helper", + description="Downloads an image from S3 and returns it as an IMAGE tensor with mask.", + inputs=[ + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default="input/example.png"), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="temp"), + io.String.Input("folder", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.Image.Output(display_name="image"), + io.Mask.Output(display_name="mask"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "IMAGE", "MASK", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", "image", "mask", ) - FUNCTION = "function" - - async def function(self, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None): + @classmethod + async def execute(cls, enabled, s3_key, parent_folder, folder, s3_config=None, bucket_name=None) -> io.NodeOutput: from nodes import EmptyImage empty_image = EmptyImage().generate(512, 512)[0] if not enabled: - return (False, "not enabled", "", "", empty_image, None, ) + return io.NodeOutput(False, "not enabled", "", "", empty_image, None) validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", empty_image, None,) + return io.NodeOutput(False, validate_s3_key["msg"], "", "", empty_image, None) random_str = InoUtilHelper.get_date_time_utc_base64() file_name = f'{random_str}{Path(s3_key).suffix}' @@ -46,16 +53,12 @@ class InoS3DownloadImage: s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "", empty_image, None,) + return io.NodeOutput(False, s3_instance["msg"], "", "", empty_image, None) s3_instance = s3_instance["instance"] - downloaded = await s3_instance.download_file( - s3_key=s3_key, - local_file_path=abs_path - ) + downloaded = await s3_instance.download_file(s3_key=s3_key, local_file_path=abs_path) if not downloaded["success"]: - return (downloaded["success"], downloaded["msg"], rel_path, abs_path, empty_image, None, ) + return io.NodeOutput(downloaded["success"], downloaded["msg"], rel_path, abs_path, empty_image, None) output_image, output_mask = load_image(downloaded["local_file"]) - - return (downloaded["success"], downloaded["msg"], rel_path, abs_path, output_image, output_mask, ) + return io.NodeOutput(downloaded["success"], downloaded["msg"], rel_path, abs_path, output_image, output_mask) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_get_download_url.py b/src/comfyui_ino_nodes/s3_helper/s3_get_download_url.py index 9c35944..39175f7 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_get_download_url.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_get_download_url.py @@ -1,51 +1,48 @@ -from pathlib import Path -import folder_paths +from inopyutils import ino_is_err -from inopyutils import ino_ok, ino_err, ino_is_err +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -class InoS3GetDownloadURL: +class InoS3GetDownloadURL(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": "input/example.png"}), - "expires_in": ("INT", {"default": 3600, "min": 1, "max": 3600, "step": 1}), - "as_attachment": ("BOOLEAN", {"default": False}), - "filename": ("STRING", {"default": ""}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3GetDownloadURL", + display_name="Ino S3 Get Download URL", + category="InoS3Helper", + description="Generates a presigned download URL for an S3 object.", + inputs=[ + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default="input/example.png"), + io.Int.Input("expires_in", default=3600, min=1, max=3600, step=1), + io.Boolean.Input("as_attachment", default=False), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="download_url"), + io.String.Output(display_name="filename"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "STRING",) - RETURN_NAMES = ("success", "msg", "result", "download_url", "filename",) - FUNCTION = "function" - - async def function(self, enabled:bool, s3_key:str, expires_in:int = 3600, as_attachment:bool = False, filename:str = None, s3_config:str = "{}", bucket_name:str = "default"): + @classmethod + async def execute(cls, enabled, s3_key, expires_in=3600, as_attachment=False, filename=None, s3_config=None, bucket_name=None) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "", "",) + return io.NodeOutput(False, "not enabled", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", "",) + return io.NodeOutput(False, validate_s3_key["msg"], "", "") s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "", "",) + return io.NodeOutput(False, s3_instance["msg"], "", "") s3_instance = s3_instance["instance"] - s3_result = await s3_instance.get_download_link( - s3_key=s3_key, - #bucket_name=bucket_name, - expires_in=expires_in, - as_attachment=as_attachment, - filename=filename, - ) - return (s3_result["success"], s3_result["msg"], str(s3_result), s3_result["url"], s3_result["filename"],) + s3_result = await s3_instance.get_download_link(s3_key=s3_key, expires_in=expires_in, as_attachment=as_attachment, filename=filename) + return io.NodeOutput(s3_result["success"], s3_result["msg"], s3_result["url"], s3_result["filename"]) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_helper.py b/src/comfyui_ino_nodes/s3_helper/s3_helper.py index e5a2a1c..d736822 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_helper.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_helper.py @@ -3,6 +3,8 @@ from pathlib import Path from inopyutils import InoS3Helper, InoJsonHelper, ino_ok, ino_err, ino_is_err +from comfy_api.latest import io + S3_EMPTY_CONFIG = { "access_key_id": "", "access_key_secret": "", @@ -61,14 +63,14 @@ class S3Helper: if s3_config_dict.get("access_key_id", None) is None: return { "success": False, - "msg": f"S3 configuration missing access_key_id", + "msg": "S3 configuration missing access_key_id", "config": "" } if s3_config_dict.get("access_key_secret", None) is None: return { "success": False, - "msg": f"S3 configuration missing access_key_secret", + "msg": "S3 configuration missing access_key_secret", "config": "" } @@ -81,61 +83,19 @@ class S3Helper: @staticmethod def validate_s3_key(s3_key) -> dict: if not s3_key or not s3_key.strip(): - return { - "success": False, - "msg": "S3 key is required and cannot be empty" - } - return { - "success": True, - "msg": "S3 key is valid" - } + return {"success": False, "msg": "S3 key is required and cannot be empty"} + return {"success": True, "msg": "S3 key is valid"} @staticmethod def validate_local_path(local_path) -> dict: if not local_path: - return { - "success": False, - "msg": "Local path is required and cannot be empty" - } - + return {"success": False, "msg": "Local path is required and cannot be empty"} if not Path(local_path).is_file() and not Path(local_path).is_dir(): - return { - "success": False, - "msg": "Local path does not exist" - } - return { - "success": True, - "msg": "Local path is valid" - } + return {"success": False, "msg": "Local path does not exist"} + return {"success": True, "msg": "Local path is valid"} @staticmethod def get_save_path(s3_key: str, save_path: str): - """ - Generate a local file path for saving an S3 object, handling various path formats. - - This function intelligently determines the appropriate local file path based on the - provided save_path format and the S3 key. It handles three scenarios: - 1. Directory path - saves file with original name from S3 key - 2. Full file path with extension - replaces extension with S3 object's extension - 3. File path without extension - appends S3 object's extension - - Args: - s3_key (str): The S3 object key (path) from which to extract the filename and extension. - Example: "folder/subfolder/document.pdf" - save_path (str): The desired local save path. Can be: - - Directory path ending with '/' or '\\': "/local/dir/" - - Full file path with extension: "/local/dir/myfile.txt" - - File path without extension: "/local/dir/myfile" - - Returns: - Path: A pathlib.Path object representing the complete local file path where - the S3 object should be saved, including the appropriate file extension. - - Note: - - The function preserves the file extension from the S3 key in all cases - - Both forward slashes (/) and backslashes (\\) are recognized as directory indicators - - If save_path has an extension, it will be replaced with the S3 object's extension - """ save_path_obj = Path(save_path) s3_key_obj = Path(s3_key) @@ -151,26 +111,31 @@ class S3Helper: return local_file_path -class InoS3Config: + +class InoS3Config(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required":{ - "access_key_id": ("STRING", {"default": ""}), - "access_key_secret": ("STRING", {"default": ""}), - "endpoint_url": ("STRING", {"default": ""}), - "region_name": ("STRING", {"default": ""}), - "bucket_name": ("STRING", {"default": ""}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3Config", + display_name="Ino S3 Config", + category="InoS3Helper", + description="Creates an S3 configuration string from credentials.", + inputs=[ + io.String.Input("access_key_id", default=""), + io.String.Input("access_key_secret", default=""), + io.String.Input("endpoint_url", default=""), + io.String.Input("region_name", default=""), + io.String.Input("bucket_name", default=""), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="config"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", ) - RETURN_NAMES = ("success", "config", ) - FUNCTION = "function" - - async def function(self, access_key_id, access_key_secret, endpoint_url, region_name, bucket_name): - s3_config ={ + @classmethod + async def execute(cls, access_key_id, access_key_secret, endpoint_url, region_name, bucket_name) -> io.NodeOutput: + s3_config = { "access_key_id": access_key_id, "access_key_secret": access_key_secret, "endpoint_url": endpoint_url, @@ -179,6 +144,5 @@ class InoS3Config: } dict_to_string = InoJsonHelper.dict_to_string(s3_config) if not dict_to_string["success"]: - return (False, dict_to_string["msg"], ) - - return (True, dict_to_string["data"], ) + return io.NodeOutput(False, dict_to_string["msg"]) + return io.NodeOutput(True, dict_to_string["data"]) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_sync_folder_node.py b/src/comfyui_ino_nodes/s3_helper/s3_sync_folder_node.py index 2115145..0757bc0 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_sync_folder_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_sync_folder_node.py @@ -2,45 +2,53 @@ from pathlib import Path from inopyutils import ino_is_err -import folder_paths +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3SyncFolder: + +class InoS3SyncFolder(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS,), - "folder": ("STRING", {"default": "sync/"}), - "sync_local": ("BOOLEAN", {"default": True, "label_off": "Upload (local->S3)", "label_on": "Download (S3->local)"}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "concurrency": ("INT", {"default": 5, "min": 1, "max": 10}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3SyncFolder", + display_name="Ino S3 Sync Folder", + category="InoS3Helper", + description="Syncs a local folder with S3 bidirectionally (upload or download).", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default="sync/"), + io.Boolean.Input("sync_local", default=True, label_off="Upload (local->S3)", label_on="Download (S3->local)"), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Int.Input("concurrency", default=5, min=1, max=10, optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.Int.Output(display_name="downloaded"), + io.Int.Output(display_name="uploaded"), + io.Int.Output(display_name="skipped_unchanged"), + io.Int.Output(display_name="failed"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "INT", "INT", "INT", "INT",) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", "downloaded", "uploaded", "skipped_unchanged", "failed",) - FUNCTION = "function" - OUTPUT_NODE = True - - async def function(self, execute, enabled, s3_key, parent_folder, folder, sync_local, s3_config=None, concurrency=5): + @classmethod + async def execute(cls, execute, enabled, s3_key, parent_folder, folder, sync_local, s3_config=None, concurrency=5) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "", 0, 0, 0, 0,) - + return io.NodeOutput(False, "not enabled", "", "", 0, 0, 0, 0) if not execute: - return (False, "execute empty", "", "", 0, 0, 0, 0,) + return io.NodeOutput(False, "execute empty", "", "", 0, 0, 0, 0) validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", 0, 0, 0, 0,) + return io.NodeOutput(False, validate_s3_key["msg"], "", "", 0, 0, 0, 0) rel_path, abs_path = resolve_comfy_path(parent_folder, folder) @@ -50,23 +58,13 @@ class InoS3SyncFolder: s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], "", "", 0, 0, 0, 0,) + return io.NodeOutput(False, s3_instance["msg"], "", "", 0, 0, 0, 0) s3_instance = s3_instance["instance"] - s3_result = await s3_instance.sync_folder( - s3_key=s3_key, - local_folder_path=abs_path, - sync_local=sync_local, - concurrency=concurrency, - ) + s3_result = await s3_instance.sync_folder(s3_key=s3_key, local_folder_path=abs_path, sync_local=sync_local, concurrency=concurrency) - return ( - s3_result["success"], - s3_result["msg"], - rel_path, - abs_path, - s3_result.get("downloaded", 0), - s3_result.get("uploaded", 0), - s3_result.get("skipped_unchanged", 0), - s3_result.get("failed", 0), + return io.NodeOutput( + s3_result["success"], s3_result["msg"], rel_path, abs_path, + s3_result.get("downloaded", 0), s3_result.get("uploaded", 0), + s3_result.get("skipped_unchanged", 0), s3_result.get("failed", 0), ) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_audio_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_audio_node.py index ac0fcf6..c6ea3da 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_audio_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_audio_node.py @@ -3,94 +3,89 @@ from pathlib import Path from inopyutils import ino_is_err, InoUtilHelper import folder_paths -from comfy_api.latest import IO +from comfy_api.latest import io, IO from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadAudio: + +class InoS3UploadAudio(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "audio": ("AUDIO",), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "output"}), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}) - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "date_time_as_name": ("BOOLEAN", {"default": False}), - }, - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadAudio", + display_name="Ino S3 Upload Audio", + category="InoS3Helper", + description="Saves audio as MP3 locally then uploads it to S3.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.Audio.Input("audio"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="output"), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Boolean.Input("date_time_as_name", default=False, optional=True), + ], + outputs=[ + io.Audio.Output(display_name="audio"), + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.String.Output(display_name="file_name"), + io.String.Output(display_name="s3_audio_path"), + ], + ) - RETURN_TYPES = ("AUDIO", "BOOLEAN", "STRING", "STRING", "STRING", "STRING", "STRING",) - RETURN_NAMES = ("audio", "success", "message", "rel_path", "abs_path", "file_name", "s3_audio_path",) - FUNCTION = "function" - OUTPUT_NODE = True - CATEGORY = "InoS3Helper" - - async def function(self, execute, enabled, audio, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False): + @classmethod + async def execute(cls, execute, enabled, audio, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False) -> io.NodeOutput: if not enabled: - return (audio, False, "", "", "", "", "",) - + return io.NodeOutput(audio, False, "", "", "", "", "") if not execute: - return (audio, False, "", "", "", "", "",) + return io.NodeOutput(audio, False, "", "", "", "", "") if isinstance(filename, list): if len(filename) == 1: filename = str(filename[0]) else: - return (audio, False, "file name is list", "", "", "", "",) - elif isinstance(filename, str): - pass - else: - return (audio, False, "file name not string", "", "", "", "",) + return io.NodeOutput(audio, False, "file name is list", "", "", "", "") + elif not isinstance(filename, str): + return io.NodeOutput(audio, False, "file name not string", "", "", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (audio, False, validate_s3_key["msg"], "", "", "", "",) + return io.NodeOutput(audio, False, validate_s3_key["msg"], "", "", "", "") if date_time_as_name: filename = InoUtilHelper.get_date_time_utc_base64() rel_path, abs_path = resolve_comfy_path(parent_folder, folder) - - save_as = "mp3" - file = f"{filename}.{save_as}" + file = f"{filename}.mp3" from comfy_extras.nodes_audio import SaveAudioMP3 audio_saver = SaveAudioMP3() - save_audio: IO.NodeOutput = audio_saver.execute( - audio=audio, - filename_prefix=filename, - format="mp3", - quality="128k" - ) + save_audio = audio_saver.execute(audio=audio, filename_prefix=filename, format="mp3", quality="128k") try: saved_filename = save_audio.ui.as_dict()["audio"][0]["filename"] except: - return (audio, False, "Audio saved, but failed to get filename", rel_path, abs_path, "", "",) + return io.NodeOutput(audio, False, "Audio saved, but failed to get filename", rel_path, abs_path, "", "") parent_path = folder_paths.get_output_directory() - full_path:Path = Path(parent_path) / saved_filename + full_path = Path(parent_path) / saved_filename s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (audio, False, s3_instance["msg"], rel_path, abs_path, "", "",) + return io.NodeOutput(audio, False, s3_instance["msg"], rel_path, abs_path, "", "") s3_instance = s3_instance["instance"] s3_full_key = f"{s3_key.rstrip('/')}/{saved_filename}" - s3_result = await s3_instance.upload_file( - s3_key=s3_full_key, - local_file_path=str(full_path), - ) + s3_result = await s3_instance.upload_file(s3_key=s3_full_key, local_file_path=str(full_path)) if not s3_result["success"]: - return (audio, False, s3_result["msg"], rel_path, abs_path, "", "",) + return io.NodeOutput(audio, False, s3_result["msg"], rel_path, abs_path, "", "") - return (audio, True, "Success", rel_path, abs_path, file, s3_full_key, ) + return io.NodeOutput(audio, True, "Success", rel_path, abs_path, file, s3_full_key) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_file_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_file_node.py index 33d2f2f..b4deb2d 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_file_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_file_node.py @@ -1,66 +1,66 @@ import os from pathlib import Path -from inopyutils import ino_ok, ino_err, ino_is_err +from inopyutils import ino_is_err -import folder_paths +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadFile: + +class InoS3UploadFile(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required":{ - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS,), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}), - "delete_local": ("BOOLEAN", {"default": True}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING , "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadFile", + display_name="Ino S3 Upload File", + category="InoS3Helper", + description="Uploads a local file to S3 with optional local deletion.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.Boolean.Input("delete_local", default=True), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", ) - FUNCTION = "function" - OUTPUT_NODE = True - - async def function(self, execute, enabled:bool, s3_key:str, parent_folder:str, folder:str, filename:str, delete_local:bool, s3_config:str=None, bucket_name:str=None): + @classmethod + async def execute(cls, execute, enabled, s3_key, parent_folder, folder, filename, delete_local, s3_config=None, bucket_name=None) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "",) - + return io.NodeOutput(False, "not enabled", "", "") if not execute: - return (False, "execute empty", "", "", ) + return io.NodeOutput(False, "execute empty", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", ) + return io.NodeOutput(False, validate_s3_key["msg"], "", "") rel_path, abs_path = resolve_comfy_path(parent_folder, folder, filename) - local_upload_path = Path(abs_path) - validate_local_path = S3Helper.validate_local_path(local_upload_path) + validate_local_path = S3Helper.validate_local_path(Path(abs_path)) if not validate_local_path["success"]: - return (False, validate_local_path["msg"], rel_path, abs_path, ) + return io.NodeOutput(False, validate_local_path["msg"], rel_path, abs_path) s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], rel_path, abs_path, ) + return io.NodeOutput(False, s3_instance["msg"], rel_path, abs_path) s3_instance = s3_instance["instance"] - s3_result = await s3_instance.upload_file( - s3_key=s3_key, - local_file_path=abs_path, - ) + s3_result = await s3_instance.upload_file(s3_key=s3_key, local_file_path=abs_path) if s3_result["success"] and delete_local: os.remove(abs_path) - return (s3_result["success"], s3_result["msg"], rel_path, abs_path, ) + return io.NodeOutput(s3_result["success"], s3_result["msg"], rel_path, abs_path) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_folder_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_folder_node.py index 079eedd..99809b2 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_folder_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_folder_node.py @@ -1,70 +1,71 @@ -import os import shutil from pathlib import Path -import folder_paths +from inopyutils import ino_is_err -from inopyutils import ino_ok, ino_err, ino_is_err +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadFolder: + +class InoS3UploadFolder(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required":{ - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS, ), - "folder": ("STRING", {"default": ""}), - "delete_local": ("BOOLEAN", {"default": True}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "bucket_name": ("STRING", {"default": "default"}), - "max_concurrent": ("INT", {"default": 5, "min": 1, "max": 10}), - "verify_with_s3": ("BOOLEAN", {"default": False}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadFolder", + display_name="Ino S3 Upload Folder", + category="InoS3Helper", + description="Uploads an entire local folder to S3 with concurrent uploads and optional verification.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default=""), + io.Boolean.Input("delete_local", default=True), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.String.Input("bucket_name", default="default", optional=True), + io.Int.Input("max_concurrent", default=5, min=1, max=10, optional=True), + io.Boolean.Input("verify_with_s3", default=False, optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.Int.Output(display_name="total_files"), + io.Int.Output(display_name="uploaded_successfully"), + io.Int.Output(display_name="failed_uploads"), + io.String.Output(display_name="errors"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "INT", "INT", "INT", "STRING", ) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", "total_files", "uploaded_successfully", "failed_uploads", "errors", ) - FUNCTION = "function" - OUTPUT_NODE = True - - async def function(self, execute, enabled, s3_key, parent_folder, folder, delete_local, s3_config=None, bucket_name=None, max_concurrent=5, verify_with_s3=False): + @classmethod + async def execute(cls, execute, enabled, s3_key, parent_folder, folder, delete_local, s3_config=None, bucket_name=None, max_concurrent=5, verify_with_s3=False) -> io.NodeOutput: if not enabled: - return (False, "", "", "", 0, 0, 0, "", ) - + return io.NodeOutput(False, "", "", "", 0, 0, 0, "") if not execute: - return (False, "", "", "", 0, 0, 0, "", ) + return io.NodeOutput(False, "", "", "", 0, 0, 0, "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", 0, 0, 0, "", ) + return io.NodeOutput(False, validate_s3_key["msg"], "", "", 0, 0, 0, "") rel_path, abs_path = resolve_comfy_path(parent_folder, folder) - local_upload_path = Path(abs_path) - validate_local_path = S3Helper.validate_local_path(local_upload_path) + validate_local_path = S3Helper.validate_local_path(Path(abs_path)) if not validate_local_path["success"]: - return (False, validate_local_path["msg"], rel_path, abs_path, 0, 0, 0, "", ) + return io.NodeOutput(False, validate_local_path["msg"], rel_path, abs_path, 0, 0, 0, "") s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], rel_path, abs_path, 0, 0, 0, "", ) + return io.NodeOutput(False, s3_instance["msg"], rel_path, abs_path, 0, 0, 0, "") s3_instance = s3_instance["instance"] - s3_result = await s3_instance.upload_folder( - s3_folder_key=s3_key, - local_folder_path=abs_path, - max_concurrent=max_concurrent, - verify=verify_with_s3 - ) + s3_result = await s3_instance.upload_folder(s3_folder_key=s3_key, local_folder_path=abs_path, max_concurrent=max_concurrent, verify=verify_with_s3) if s3_result["success"] and delete_local: - shutil.rmtree(local_upload_path) + shutil.rmtree(Path(abs_path)) - return (s3_result["success"], s3_result["msg"], rel_path, abs_path, s3_result["total_files"], s3_result["uploaded_successfully"], s3_result["failed_uploads"], str(s3_result["errors"]), ) + return io.NodeOutput(s3_result["success"], s3_result["msg"], rel_path, abs_path, s3_result["total_files"], s3_result["uploaded_successfully"], s3_result["failed_uploads"], str(s3_result["errors"])) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_image_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_image_node.py index 5f725fa..35d939a 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_image_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_image_node.py @@ -1,62 +1,69 @@ import os from pathlib import Path -from datetime import datetime -from PIL import Image, ImageOps, ImageSequence +from PIL import Image from PIL.PngImagePlugin import PngInfo import numpy as np -from inopyutils import InoJsonHelper, ino_ok, ino_err, ino_is_err, InoUtilHelper +from inopyutils import InoJsonHelper, ino_is_err, InoUtilHelper import folder_paths from comfy.cli_args import args +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadImage: + +class InoS3UploadImage(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "images": ("IMAGE",), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "temp"}), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}) - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "compress_level": ("INT", {"default": 4, "min": 1, "max": 9}), - "date_time_as_name": ("BOOLEAN", {"default": False}), - }, - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadImage", + display_name="Ino S3 Upload Image", + category="InoS3Helper", + description="Saves images locally as PNG then uploads them to S3.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.Image.Input("images"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="temp"), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Int.Input("compress_level", default=4, min=1, max=9, optional=True), + io.Boolean.Input("date_time_as_name", default=False, optional=True), + ], + outputs=[ + io.Image.Output(display_name="images"), + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.String.Output(display_name="file_names"), + io.String.Output(display_name="s3_image_paths"), + ], + ) - RETURN_TYPES = ("IMAGE", "BOOLEAN", "STRING", "STRING", "STRING", "STRING", "STRING",) - RETURN_NAMES = ("images", "success", "message", "rel_path", "abs_path", "file_names", "s3_image_paths",) - FUNCTION = "function" - OUTPUT_NODE = True - CATEGORY = "InoS3Helper" - - async def function(self, execute, enabled, images, s3_key, parent_folder, folder, filename, s3_config=None, compress_level=4, date_time_as_name=False): + @classmethod + async def execute(cls, execute, enabled, images, s3_key, parent_folder, folder, filename, s3_config=None, compress_level=4, date_time_as_name=False) -> io.NodeOutput: if not enabled: - return (images, False, "", "", "", "", "",) - + return io.NodeOutput(images, False, "", "", "", "", "") if not execute: - return (images, False, "", "", "", "", "",) + return io.NodeOutput(images, False, "", "", "", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (images, False, validate_s3_key["msg"], "", "", "", "") + return io.NodeOutput(images, False, validate_s3_key["msg"], "", "", "", "") if date_time_as_name: filename = InoUtilHelper.get_date_time_utc_base64() rel_path, abs_path = resolve_comfy_path(parent_folder, folder) - full_output_folder, file_prefix, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename, abs_path, images[0].shape[1], images[0].shape[0]) - results:dict = {} + full_output_folder, file_prefix, counter, subfolder, _ = folder_paths.get_save_image_path(filename, abs_path, images[0].shape[1], images[0].shape[0]) + results = {} for (batch_number, image) in enumerate(images): i = 255. * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) @@ -69,31 +76,22 @@ class InoS3UploadImage: file_w_ext = file + ".png" full_path = os.path.join(full_output_folder, file_w_ext) img.save(full_path, pnginfo=metadata, compress_level=compress_level) - results[batch_number] = { - "filename": file, - "filename_w_ext": file_w_ext, - "full_path": full_path, - } + results[batch_number] = {"filename": file, "filename_w_ext": file_w_ext, "full_path": full_path} counter += 1 img.close() s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (images, False, s3_instance["msg"], rel_path, abs_path, "", "") + return io.NodeOutput(images, False, s3_instance["msg"], rel_path, abs_path, "", "") s3_instance = s3_instance["instance"] for index in results: s3_full_key = s3_key + "/" + results[index]["filename_w_ext"] - s3_result = await s3_instance.upload_file( - s3_key=s3_full_key, - local_file_path=results[index]["full_path"], - ) + s3_result = await s3_instance.upload_file(s3_key=s3_full_key, local_file_path=results[index]["full_path"]) results[index]["s3_success"] = s3_result["success"] results[index]["s3_msg"] = s3_result["msg"] results[index]["s3_key"] = s3_full_key - result_str = InoJsonHelper.dict_to_string(results)['data'] - final_success = True final_message = "" for index in results: @@ -103,12 +101,9 @@ class InoS3UploadImage: break if not final_success: - return (images, final_success, final_message, rel_path, abs_path, "", "") + return io.NodeOutput(images, final_success, final_message, rel_path, abs_path, "", "") - s3_paths = [] - file_names = [] - for index in results: - s3_paths.append(results[index]["s3_key"]) - file_names.append(results[index]["filename"]) + s3_paths = [results[i]["s3_key"] for i in results] + file_names = [results[i]["filename"] for i in results] - return (images, True, "Success", rel_path, abs_path, str(file_names), str(s3_paths), ) + return io.NodeOutput(images, True, "Success", rel_path, abs_path, str(file_names), str(s3_paths)) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_string_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_string_node.py index 1513cbe..8aa7bdc 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_string_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_string_node.py @@ -1,75 +1,76 @@ import os from pathlib import Path -from datetime import datetime -from PIL import Image, ImageOps, ImageSequence -from PIL.PngImagePlugin import PngInfo -import numpy as np -from inopyutils import InoJsonHelper, InoFileHelper, ino_ok, ino_err, ino_is_err, InoUtilHelper +from inopyutils import InoJsonHelper, InoFileHelper, ino_is_err, InoUtilHelper import folder_paths -from comfy.cli_args import args +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadString: + +class InoS3UploadString(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "string": ("STRING",), - "save_as": (["txt", "json", "ini"], ), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "temp"}), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}) - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "date_time_as_name": ("BOOLEAN", {"default": False}), - }, - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadString", + display_name="Ino S3 Upload String", + category="InoS3Helper", + description="Saves a string as a file (txt/json/ini) locally then uploads it to S3.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("string"), + io.Combo.Input("save_as", options=["txt", "json", "ini"]), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="temp"), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Boolean.Input("date_time_as_name", default=False, optional=True), + ], + outputs=[ + io.String.Output(display_name="string"), + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.String.Output(display_name="file_name"), + io.String.Output(display_name="s3_string_path"), + ], + ) - RETURN_TYPES = ("STRING", "BOOLEAN", "STRING", "STRING", "STRING", "STRING", "STRING",) - RETURN_NAMES = ("STRING", "success", "message", "rel_path", "abs_path", "file_name", "s3_string_path",) - FUNCTION = "function" - OUTPUT_NODE = True - CATEGORY = "InoS3Helper" - - async def function(self, execute, enabled, string, save_as, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False): + @classmethod + async def execute(cls, execute, enabled, string, save_as, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False) -> io.NodeOutput: if not enabled: - return (string, False, "", "", "", "", "",) - + return io.NodeOutput(string, False, "", "", "", "", "") if not execute: - return (string, False, "", "", "", "", "",) + return io.NodeOutput(string, False, "", "", "", "", "") if isinstance(filename, list): if len(filename) == 1: filename = str(filename[0]) else: - return (string, False, "file name is list", "", "", "", "",) - elif isinstance(filename, str): - pass - else: - return (string, False, "file name not string", "", "", "", "",) + return io.NodeOutput(string, False, "file name is list", "", "", "", "") + elif not isinstance(filename, str): + return io.NodeOutput(string, False, "file name not string", "", "", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (string, False, validate_s3_key["msg"], "", "", "", "",) + return io.NodeOutput(string, False, validate_s3_key["msg"], "", "", "", "") if date_time_as_name: filename = InoUtilHelper.get_date_time_utc_base64() rel_path, abs_path = resolve_comfy_path(parent_folder, folder) - full_output_folder, file_prefix, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename, abs_path, 0, 0) + full_output_folder, file_prefix, counter, subfolder, _ = folder_paths.get_save_image_path(filename, abs_path, 0, 0) filename_with_batch_num = file_prefix.replace("%batch_num%", "0") file = f"{filename_with_batch_num}_{counter:05}" - file_w_ext= f"{file}.{save_as}" + file_w_ext = f"{file}.{save_as}" full_path = os.path.join(full_output_folder, file_w_ext) if save_as == "json": @@ -78,19 +79,16 @@ class InoS3UploadString: save_file = await InoFileHelper.save_string_as_file(string, full_path) if not save_file["success"]: - return (string, False, save_file["msg"], rel_path, abs_path, "", "", ) + return io.NodeOutput(string, False, save_file["msg"], rel_path, abs_path, "", "") s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (string, False, s3_instance["msg"], rel_path, abs_path, "", "", ) + return io.NodeOutput(string, False, s3_instance["msg"], rel_path, abs_path, "", "") s3_instance = s3_instance["instance"] s3_full_key = f"{s3_key.rstrip('/')}/{filename}.{save_as}" - s3_result = await s3_instance.upload_file( - s3_key=s3_full_key, - local_file_path=full_path, - ) + s3_result = await s3_instance.upload_file(s3_key=s3_full_key, local_file_path=full_path) if not s3_result["success"]: - return (string, False, s3_result["msg"], rel_path, abs_path, "", "",) + return io.NodeOutput(string, False, s3_result["msg"], rel_path, abs_path, "", "") - return (string, True, "Success", rel_path, abs_path, file_w_ext, s3_full_key, ) + return io.NodeOutput(string, True, "Success", rel_path, abs_path, file_w_ext, s3_full_key) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_upload_video_node.py b/src/comfyui_ino_nodes/s3_helper/s3_upload_video_node.py index d9e737d..4c7568f 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_upload_video_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_upload_video_node.py @@ -1,56 +1,58 @@ import os from pathlib import Path -from datetime import datetime -from PIL import Image, ImageOps, ImageSequence -from PIL.PngImagePlugin import PngInfo -import numpy as np -from inopyutils import InoJsonHelper, ino_ok, ino_err, ino_is_err, InoUtilHelper +from inopyutils import ino_is_err, InoUtilHelper import folder_paths -from comfy.cli_args import args -from comfy_api.latest import ComfyExtension, io, ui, Input, InputImpl, Types +from comfy_api.latest import io, Input, Types from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3UploadVideo: + +class InoS3UploadVideo(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "video": ("VIDEO",), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS, {"default": "temp"}), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}) - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "date_time_as_name": ("BOOLEAN", {"default": False}), - "video_format": (("mp4", "auto"), {}), - "video_codec": (("h264", "auto"), {}), - }, - } + def define_schema(cls): + return io.Schema( + node_id="InoS3UploadVideo", + display_name="Ino S3 Upload Video", + category="InoS3Helper", + description="Saves video locally then uploads it to S3.", + is_output_node=True, + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.Video.Input("video"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS, default="temp"), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Boolean.Input("date_time_as_name", default=False, optional=True), + io.Combo.Input("video_format", options=["mp4", "auto"], optional=True), + io.Combo.Input("video_codec", options=["h264", "auto"], optional=True), + ], + outputs=[ + io.Video.Output(display_name="video"), + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.String.Output(display_name="file_name"), + io.String.Output(display_name="s3_video_path"), + ], + ) - RETURN_TYPES = ("Video", "BOOLEAN", "STRING", "STRING", "STRING", "STRING", "STRING",) - RETURN_NAMES = ("video", "success", "message", "rel_path", "abs_path", "file_name", "s3_video_path",) - FUNCTION = "function" - OUTPUT_NODE = True - CATEGORY = "InoS3Helper" - - async def function(self, execute, enabled, video: Input.Video, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False, video_format="mp4", video_codec="h264"): + @classmethod + async def execute(cls, execute, enabled, video: Input.Video, s3_key, parent_folder, folder, filename, s3_config=None, date_time_as_name=False, video_format="mp4", video_codec="h264") -> io.NodeOutput: if not enabled: - return (video, False, "", "", "", "", "",) - + return io.NodeOutput(video, False, "", "", "", "", "") if not execute: - return (video, False, "", "", "", "", "",) + return io.NodeOutput(video, False, "", "", "", "", "") validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (video, False, validate_s3_key["msg"], "", "", "", "") + return io.NodeOutput(video, False, validate_s3_key["msg"], "", "", "", "") if date_time_as_name: filename = InoUtilHelper.get_date_time_utc_base64() @@ -58,36 +60,19 @@ class InoS3UploadVideo: rel_path, abs_path = resolve_comfy_path(parent_folder, folder) width, height = video.get_dimensions() - full_output_folder, file_prefix, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( - filename, - abs_path, - width, - height - ) - - saved_metadata = None + full_output_folder, file_prefix, counter, subfolder, _ = folder_paths.get_save_image_path(filename, abs_path, width, height) filename_w_ext = f"{file_prefix}.{Types.VideoContainer.get_extension(video_format)}" file_path = os.path.join(full_output_folder, filename_w_ext) - video.save_to( - file_path, - format=Types.VideoContainer(video_format), - codec=video_codec, - metadata=saved_metadata - ) + video.save_to(file_path, format=Types.VideoContainer(video_format), codec=Types.VideoCodec(video_codec)) s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (video, False, s3_instance["msg"], rel_path, abs_path, "", "") + return io.NodeOutput(video, False, s3_instance["msg"], rel_path, abs_path, "", "") s3_instance = s3_instance["instance"] s3_full_key = s3_key + "/" + filename_w_ext - s3_result = await s3_instance.upload_file( - s3_key=s3_full_key, - local_file_path=file_path, - ) - if not s3_result["success"]: - return (video, s3_result["success"], s3_result["msg"], rel_path, abs_path, filename, s3_full_key,) + s3_result = await s3_instance.upload_file(s3_key=s3_full_key, local_file_path=file_path) - return (video, s3_result["success"], s3_result["msg"], rel_path, abs_path, filename, s3_full_key,) + return io.NodeOutput(video, s3_result["success"], s3_result["msg"], rel_path, abs_path, filename, s3_full_key) diff --git a/src/comfyui_ino_nodes/s3_helper/s3_verify_file_node.py b/src/comfyui_ino_nodes/s3_helper/s3_verify_file_node.py index 4b96b8d..42a5682 100644 --- a/src/comfyui_ino_nodes/s3_helper/s3_verify_file_node.py +++ b/src/comfyui_ino_nodes/s3_helper/s3_verify_file_node.py @@ -2,70 +2,66 @@ from pathlib import Path from inopyutils import ino_is_err -import folder_paths +from comfy_api.latest import io from .s3_helper import S3Helper, S3_EMPTY_CONFIG_STRING -from ..node_helper import any_type, PARENT_FOLDER_OPTIONS, resolve_comfy_path +from ..node_helper import PARENT_FOLDER_OPTIONS, resolve_comfy_path -class InoS3VerifyFile: + +class InoS3VerifyFile(io.ComfyNode): @classmethod - def INPUT_TYPES(s): - return { - "required": { - "execute": (any_type,), - "enabled": ("BOOLEAN", {"default": True, "label_off": "OFF", "label_on": "ON"}), - "s3_key": ("STRING", {"default": ""}), - "parent_folder": (PARENT_FOLDER_OPTIONS,), - "folder": ("STRING", {"default": ""}), - "filename": ("STRING", {"default": ""}), - }, - "optional": { - "s3_config": ("STRING", {"default": S3_EMPTY_CONFIG_STRING, "tooltip": "you can leave it empty and pass it with env vars"}), - "use_md5": ("BOOLEAN", {"default": False}), - "use_sha256": ("BOOLEAN", {"default": False}), - } - } + def define_schema(cls): + return io.Schema( + node_id="InoS3VerifyFile", + display_name="Ino S3 Verify File", + category="InoS3Helper", + description="Verifies a local file exists in S3 and optionally checks integrity via hash.", + inputs=[ + io.AnyType.Input("execute"), + io.Boolean.Input("enabled", default=True, label_off="OFF", label_on="ON"), + io.String.Input("s3_key", default=""), + io.Combo.Input("parent_folder", options=PARENT_FOLDER_OPTIONS), + io.String.Input("folder", default=""), + io.String.Input("filename", default=""), + io.String.Input("s3_config", default=S3_EMPTY_CONFIG_STRING, optional=True, tooltip="you can leave it empty and pass it with env vars"), + io.Boolean.Input("use_md5", default=False, optional=True), + io.Boolean.Input("use_sha256", default=False, optional=True), + ], + outputs=[ + io.Boolean.Output(display_name="success"), + io.String.Output(display_name="message"), + io.String.Output(display_name="rel_path"), + io.String.Output(display_name="abs_path"), + io.Boolean.Output(display_name="exists_remote"), + io.Boolean.Output(display_name="sizes_match"), + ], + ) - CATEGORY = "InoS3Helper" - RETURN_TYPES = ("BOOLEAN", "STRING", "STRING", "STRING", "BOOLEAN", "BOOLEAN",) - RETURN_NAMES = ("success", "message", "rel_path", "abs_path", "exists_remote", "sizes_match",) - FUNCTION = "function" - - async def function(self, execute, enabled, s3_key, parent_folder, folder, filename, s3_config=None, use_md5=False, use_sha256=False): + @classmethod + async def execute(cls, execute, enabled, s3_key, parent_folder, folder, filename, s3_config=None, use_md5=False, use_sha256=False) -> io.NodeOutput: if not enabled: - return (False, "not enabled", "", "", False, False,) - + return io.NodeOutput(False, "not enabled", "", "", False, False) if not execute: - return (False, "execute empty", "", "", False, False,) + return io.NodeOutput(False, "execute empty", "", "", False, False) validate_s3_key = S3Helper.validate_s3_key(s3_key) if not validate_s3_key["success"]: - return (False, validate_s3_key["msg"], "", "", False, False,) + return io.NodeOutput(False, validate_s3_key["msg"], "", "", False, False) rel_path, abs_path = resolve_comfy_path(parent_folder, folder, filename) - local_file_path = Path(abs_path) - validate_local_path = S3Helper.validate_local_path(local_file_path) + validate_local_path = S3Helper.validate_local_path(Path(abs_path)) if not validate_local_path["success"]: - return (False, validate_local_path["msg"], rel_path, abs_path, False, False,) + return io.NodeOutput(False, validate_local_path["msg"], rel_path, abs_path, False, False) s3_instance = S3Helper.get_instance(s3_config) if ino_is_err(s3_instance): - return (False, s3_instance["msg"], rel_path, abs_path, False, False,) + return io.NodeOutput(False, s3_instance["msg"], rel_path, abs_path, False, False) s3_instance = s3_instance["instance"] - s3_result = await s3_instance.verify_file( - local_file_path=abs_path, - s3_key=s3_key, - use_md5=use_md5, - use_sha256=use_sha256, - ) + s3_result = await s3_instance.verify_file(local_file_path=abs_path, s3_key=s3_key, use_md5=use_md5, use_sha256=use_sha256) - return ( - s3_result["success"], - s3_result["msg"], - rel_path, - abs_path, - s3_result.get("exists_remote", False), - s3_result.get("sizes_match", False), + return io.NodeOutput( + s3_result["success"], s3_result["msg"], rel_path, abs_path, + s3_result.get("exists_remote", False), s3_result.get("sizes_match", False), )