添加国际化,修复安全问题,使用英文提示
This commit is contained in:
@@ -118,7 +118,7 @@ pip install -r requirements.txt
|
||||
### 文件命名选项
|
||||
- **文件名前缀**:自定义前缀
|
||||
- **添加时间戳**:在文件名中包含时间
|
||||
- **添加计数器**:批量保存时的序号
|
||||
- **添加计数器**:多张图片时的序号
|
||||
- **计数器设置**:起始值和位数
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"SmartFolderManager": "Smart Folder Manager",
|
||||
"SmartImageSaver": "Smart Image Saver"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"SmartFolderManager": {
|
||||
"title": "Smart Folder Manager",
|
||||
"inputs": {
|
||||
"images": "Input images for extracting size info and passing to save node",
|
||||
"base_folder": "Base folder path, can be relative or absolute path",
|
||||
"create_subfolders": "Whether to automatically create subfolders",
|
||||
"enable_date_folder": "Whether to create date folder",
|
||||
"enable_model_folder": "Whether to create model folder",
|
||||
"enable_seed_folder": "Whether to create seed folder",
|
||||
"enable_prompt_folder": "Whether to create prompt folder",
|
||||
"enable_custom_folder": "Whether to use custom folder",
|
||||
"date_format": "Date format: yyyy=year MM=month dd=day hh=hour mm=minute ss=second",
|
||||
"include_time": "Whether to include time in date",
|
||||
"model_source": "Model info source: auto=from metadata/external input, manual=manual selection",
|
||||
"manual_model_name": "Manual model selection (only effective when model_source=manual)",
|
||||
"model_input": "Input from model loader node (only effective when model_source=auto)",
|
||||
"seed": "Seed value (manual input or connect external node)",
|
||||
"positive_prompt": "Positive prompt (manual input or connect external node)",
|
||||
"negative_prompt": "Negative prompt (manual input or connect external node)",
|
||||
"custom_subfolder": "Custom subfolder name",
|
||||
"model_short_name": "Use model short name (remove extension)",
|
||||
"prompt_max_length": "Maximum length for prompt folder name",
|
||||
"sanitize_names": "Clean illegal characters in filenames"
|
||||
},
|
||||
"outputs": {
|
||||
"images": "Passed image data",
|
||||
"folder_path": "Generated folder path",
|
||||
"metadata_json": "Extracted metadata JSON"
|
||||
}
|
||||
},
|
||||
"SmartImageSaver": {
|
||||
"title": "Smart Image Saver",
|
||||
"inputs": {
|
||||
"images": "Images to save (usually from SmartFolderManager)",
|
||||
"folder_path": "Save path (from SmartFolderManager)",
|
||||
"metadata_json": "Metadata JSON (from SmartFolderManager)",
|
||||
"filename_prefix": "Filename prefix",
|
||||
"file_format": "Image format",
|
||||
"preview_mode": "Save mode",
|
||||
"add_timestamp": "Add timestamp to filename",
|
||||
"add_counter": "Add counter (for multiple images)",
|
||||
"counter_start": "Counter start value",
|
||||
"counter_padding": "Counter padding (zero-fill)",
|
||||
"jpeg_quality": "JPEG quality (1-100)",
|
||||
"webp_quality": "WebP quality (1-100)",
|
||||
"webp_lossless": "WebP lossless compression",
|
||||
"png_compression": "PNG compression level (0-9)",
|
||||
"embed_metadata": "Embed metadata into image",
|
||||
"embed_workflow": "Embed workflow information",
|
||||
"overwrite_existing": "Overwrite existing files",
|
||||
"create_backup": "Create backup for overwritten files",
|
||||
"optimize_size": "Optimize file size"
|
||||
},
|
||||
"outputs": {
|
||||
"images": "Saved images"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"SmartFolderManager": "智能文件夹管理",
|
||||
"SmartImageSaver": "智能图片保存"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"SmartFolderManager": {
|
||||
"title": "智能文件夹管理",
|
||||
"inputs": {
|
||||
"images": "输入图片,用于提取尺寸信息并传递给保存节点",
|
||||
"base_folder": "基础文件夹路径,可以是相对路径或绝对路径",
|
||||
"create_subfolders": "是否自动创建子文件夹",
|
||||
"enable_date_folder": "是否创建日期文件夹",
|
||||
"enable_model_folder": "是否创建模型文件夹",
|
||||
"enable_seed_folder": "是否创建种子文件夹",
|
||||
"enable_prompt_folder": "是否创建提示词文件夹",
|
||||
"enable_custom_folder": "是否使用自定义文件夹",
|
||||
"date_format": "日期格式: yyyy=年 MM=月 dd=日 hh=时 mm=分 ss=秒",
|
||||
"include_time": "是否在日期中包含时间",
|
||||
"model_source": "模型信息来源:auto=从元数据/外部输入获取,manual=手动选择",
|
||||
"manual_model_name": "手动选择模型(仅在model_source=manual时生效)",
|
||||
"model_input": "从模型加载器节点输入(仅在model_source=auto时生效)",
|
||||
"seed": "种子值(手动输入或连接外部节点)",
|
||||
"positive_prompt": "正向提示词(手动输入或连接外部节点)",
|
||||
"negative_prompt": "负向提示词(手动输入或连接外部节点)",
|
||||
"custom_subfolder": "自定义子文件夹名称",
|
||||
"model_short_name": "使用模型短名称(去除扩展名)",
|
||||
"prompt_max_length": "提示词文件夹名最大长度",
|
||||
"sanitize_names": "清理文件名中的非法字符"
|
||||
},
|
||||
"outputs": {
|
||||
"images": "传递的图片数据",
|
||||
"folder_path": "生成的文件夹路径",
|
||||
"metadata_json": "提取的元数据JSON"
|
||||
}
|
||||
},
|
||||
"SmartImageSaver": {
|
||||
"title": "智能图片保存",
|
||||
"inputs": {
|
||||
"images": "要保存的图片(通常来自SmartFolderManager)",
|
||||
"folder_path": "保存路径(来自SmartFolderManager)",
|
||||
"metadata_json": "元数据JSON(来自SmartFolderManager)",
|
||||
"filename_prefix": "文件名前缀",
|
||||
"file_format": "图片格式",
|
||||
"preview_mode": "保存模式",
|
||||
"add_timestamp": "在文件名中添加时间戳",
|
||||
"add_counter": "添加计数器(多张图片时)",
|
||||
"counter_start": "计数器起始值",
|
||||
"counter_padding": "计数器位数(补零)",
|
||||
"jpeg_quality": "JPEG质量(1-100)",
|
||||
"webp_quality": "WebP质量(1-100)",
|
||||
"webp_lossless": "WebP无损压缩",
|
||||
"png_compression": "PNG压缩级别(0-9)",
|
||||
"embed_metadata": "嵌入元数据到图片",
|
||||
"embed_workflow": "嵌入工作流信息",
|
||||
"overwrite_existing": "覆盖已存在的文件",
|
||||
"create_backup": "为覆盖的文件创建备份",
|
||||
"optimize_size": "优化文件大小"
|
||||
},
|
||||
"outputs": {
|
||||
"images": "保存的图片"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,19 @@ class PathManager:
|
||||
}
|
||||
|
||||
def sanitize_filename(self, name: str, max_length: int = 100) -> str:
|
||||
"""清理文件名,移除非法字符"""
|
||||
"""安全地清理文件名,移除非法字符和路径遍历攻击"""
|
||||
if not name:
|
||||
return "untitled"
|
||||
|
||||
name = str(name)
|
||||
|
||||
# 使用os.path.basename确保只获取文件名部分,防止路径遍历
|
||||
name = os.path.basename(name)
|
||||
|
||||
# 如果basename后为空(比如输入是路径分隔符),使用默认名称
|
||||
if not name or name in ('.', '..'):
|
||||
return "untitled"
|
||||
|
||||
# 移除或替换非法字符
|
||||
name = re.sub(self.illegal_chars, '_', name)
|
||||
|
||||
@@ -41,6 +48,16 @@ class PathManager:
|
||||
# 移除首尾的下划线和点
|
||||
name = name.strip('_.')
|
||||
|
||||
# 防止Windows保留名称
|
||||
reserved_names = [
|
||||
'CON', 'PRN', 'AUX', 'NUL',
|
||||
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
||||
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
|
||||
]
|
||||
name_upper = name.upper()
|
||||
if name_upper in reserved_names or name_upper.split('.')[0] in reserved_names:
|
||||
name = f"_{name}"
|
||||
|
||||
# 限制长度
|
||||
if len(name) > max_length:
|
||||
name = name[:max_length].rstrip('_.')
|
||||
@@ -143,13 +160,14 @@ class PathManager:
|
||||
return [seg for seg in path_segments if seg]
|
||||
|
||||
def resolve_base_path(self, base_folder: str, default_output_dir: str) -> str:
|
||||
"""解析基础路径"""
|
||||
"""安全地解析基础路径"""
|
||||
from ..utils.validators import InputValidator
|
||||
|
||||
if not base_folder or base_folder.lower() in ["", "output", "."]:
|
||||
return default_output_dir
|
||||
elif os.path.isabs(base_folder):
|
||||
return base_folder
|
||||
else:
|
||||
return os.path.join(default_output_dir, base_folder)
|
||||
|
||||
# 使用安全的路径连接方法
|
||||
return InputValidator.secure_path_join(base_folder, "")
|
||||
|
||||
def build_full_path(self, base_folder: str, structure_mode: str,
|
||||
metadata: Dict[str, Any], user_inputs: Dict[str, Any],
|
||||
|
||||
@@ -10,8 +10,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SmartFolderManager": "智能文件夹管理",
|
||||
"SmartImageSaver": "智能图片保存",
|
||||
"SmartFolderManager": "SmartFolderManager",
|
||||
"SmartImageSaver": "SmartImageSaver",
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -24,103 +24,103 @@ class SmartFolderManager:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", {"tooltip": "输入图片,用于提取尺寸信息并传递给保存节点"}),
|
||||
"images": ("IMAGE", {"tooltip": "Input images for extracting size info and passing to save node"}),
|
||||
"base_folder": ("STRING", {
|
||||
"default": "output",
|
||||
"multiline": False,
|
||||
"tooltip": "基础文件夹路径,可以是相对路径或绝对路径"
|
||||
"tooltip": "Base folder path, can be relative or absolute path"
|
||||
}),
|
||||
"create_subfolders": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "是否自动创建子文件夹"
|
||||
"tooltip": "Whether to automatically create subfolders"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# 文件夹层级开关
|
||||
"enable_date_folder": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "是否创建日期文件夹"
|
||||
"tooltip": "Whether to create date folder"
|
||||
}),
|
||||
"enable_model_folder": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "是否创建模型文件夹"
|
||||
"tooltip": "Whether to create model folder"
|
||||
}),
|
||||
"enable_seed_folder": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "是否创建种子文件夹"
|
||||
"tooltip": "Whether to create seed folder"
|
||||
}),
|
||||
"enable_prompt_folder": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "是否创建提示词文件夹"
|
||||
"tooltip": "Whether to create prompt folder"
|
||||
}),
|
||||
"enable_custom_folder": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "是否使用自定义文件夹"
|
||||
"tooltip": "Whether to use custom folder"
|
||||
}),
|
||||
|
||||
# 日期相关
|
||||
"date_format": ("STRING", {
|
||||
"default": "yyyy-MM-dd",
|
||||
"multiline": False,
|
||||
"tooltip": "日期格式: yyyy年 MM月 dd日 hh时 mm分 ss秒"
|
||||
"tooltip": "Date format: yyyy=year MM=month dd=day hh=hour mm=minute ss=second"
|
||||
}),
|
||||
"include_time": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "是否在日期中包含时间"
|
||||
"tooltip": "Whether to include time in date"
|
||||
}),
|
||||
|
||||
# 模型信息来源选择
|
||||
"model_source": (["auto", "manual"], {
|
||||
"default": "auto",
|
||||
"tooltip": "模型信息来源:auto=从元数据/外部输入获取,manual=手动选择"
|
||||
"tooltip": "Model info source: auto=from metadata/external input, manual=manual selection"
|
||||
}),
|
||||
"manual_model_name": (folder_paths.get_filename_list("checkpoints"), {
|
||||
"tooltip": "手动选择模型(仅在model_source=manual时生效)"
|
||||
"tooltip": "Manual model selection (only effective when model_source=manual)"
|
||||
}),
|
||||
"model_input": ("MODEL", {
|
||||
"tooltip": "从模型加载器节点输入(仅在model_source=auto时生效)"
|
||||
"tooltip": "Input from model loader node (only effective when model_source=auto)"
|
||||
}),
|
||||
|
||||
# 种子信息
|
||||
"seed": ("STRING", {
|
||||
"default": "0",
|
||||
"multiline": False,
|
||||
"tooltip": "种子值(手动输入或连接外部节点)"
|
||||
"tooltip": "Seed value (manual input or connect external node)"
|
||||
}),
|
||||
|
||||
# 提示词信息
|
||||
"positive_prompt": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "正向提示词(手动输入或连接外部节点)"
|
||||
"tooltip": "Positive prompt (manual input or connect external node)"
|
||||
}),
|
||||
"negative_prompt": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "负向提示词(手动输入或连接外部节点)"
|
||||
"tooltip": "Negative prompt (manual input or connect external node)"
|
||||
}),
|
||||
|
||||
# 自定义路径
|
||||
"custom_subfolder": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "自定义子文件夹名称"
|
||||
"tooltip": "Custom subfolder name"
|
||||
}),
|
||||
|
||||
# 显示选项
|
||||
"model_short_name": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "使用模型短名称(去除扩展名)"
|
||||
"tooltip": "Use model short name (remove extension)"
|
||||
}),
|
||||
"prompt_max_length": ("INT", {
|
||||
"default": 50,
|
||||
"min": 10,
|
||||
"max": 200,
|
||||
"tooltip": "提示词文件夹名最大长度"
|
||||
"tooltip": "Maximum length for prompt folder name"
|
||||
}),
|
||||
"sanitize_names": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "清理文件名中的非法字符"
|
||||
"tooltip": "Clean illegal characters in filenames"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
|
||||
@@ -28,52 +28,52 @@ class SmartImageSaver:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", {"tooltip": "要保存的图片(通常来自SmartFolderManager)"}),
|
||||
"images": ("IMAGE", {"tooltip": "Images to save (usually from SmartFolderManager)"}),
|
||||
"folder_path": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "保存路径(来自SmartFolderManager)"
|
||||
"tooltip": "Save path (from SmartFolderManager)"
|
||||
}),
|
||||
"metadata_json": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "元数据JSON(来自SmartFolderManager)"
|
||||
"tooltip": "Metadata JSON (from SmartFolderManager)"
|
||||
}),
|
||||
"filename_prefix": ("STRING", {
|
||||
"default": "image",
|
||||
"multiline": False,
|
||||
"tooltip": "文件名前缀"
|
||||
"tooltip": "Filename prefix"
|
||||
}),
|
||||
"file_format": (["png", "jpeg", "webp", "bmp", "tiff"], {
|
||||
"default": "png",
|
||||
"tooltip": "图片格式"
|
||||
"tooltip": "Image format"
|
||||
}),
|
||||
"preview_mode": (["save_and_preview", "preview_only", "save_only"], {
|
||||
"default": "save_and_preview",
|
||||
"tooltip": "保存模式"
|
||||
"tooltip": "Save mode"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# 文件名选项
|
||||
"add_timestamp": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "在文件名中添加时间戳"
|
||||
"tooltip": "Add timestamp to filename"
|
||||
}),
|
||||
"add_counter": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "添加计数器(批量保存时)"
|
||||
"tooltip": "Add counter (for multiple images)"
|
||||
}),
|
||||
"counter_start": ("INT", {
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 99999,
|
||||
"tooltip": "计数器起始值"
|
||||
"tooltip": "Counter start value"
|
||||
}),
|
||||
"counter_padding": ("INT", {
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"tooltip": "计数器位数(补零)"
|
||||
"tooltip": "Counter padding (zero-fill)"
|
||||
}),
|
||||
|
||||
# 图片质量选项
|
||||
@@ -81,47 +81,47 @@ class SmartImageSaver:
|
||||
"default": 95,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"tooltip": "JPEG质量(1-100)"
|
||||
"tooltip": "JPEG quality (1-100)"
|
||||
}),
|
||||
"webp_quality": ("INT", {
|
||||
"default": 90,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"tooltip": "WebP质量(1-100)"
|
||||
"tooltip": "WebP quality (1-100)"
|
||||
}),
|
||||
"webp_lossless": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "WebP无损压缩"
|
||||
"tooltip": "WebP lossless compression"
|
||||
}),
|
||||
"png_compression": ("INT", {
|
||||
"default": 6,
|
||||
"min": 0,
|
||||
"max": 9,
|
||||
"tooltip": "PNG压缩级别(0-9)"
|
||||
"tooltip": "PNG compression level (0-9)"
|
||||
}),
|
||||
|
||||
# 元数据选项
|
||||
"embed_metadata": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"tooltip": "嵌入元数据到图片"
|
||||
"tooltip": "Embed metadata into image"
|
||||
}),
|
||||
"embed_workflow": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "嵌入工作流信息"
|
||||
"default": True,
|
||||
"tooltip": "Embed workflow information"
|
||||
}),
|
||||
|
||||
# 高级选项
|
||||
"overwrite_existing": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "覆盖已存在的文件"
|
||||
"tooltip": "Overwrite existing files"
|
||||
}),
|
||||
"create_backup": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "为覆盖的文件创建备份"
|
||||
"tooltip": "Create backup for overwritten files"
|
||||
}),
|
||||
"optimize_size": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "优化文件大小"
|
||||
"tooltip": "Optimize file size"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
|
||||
@@ -9,8 +9,8 @@ class InputValidator:
|
||||
"""输入验证器"""
|
||||
|
||||
@staticmethod
|
||||
def validate_folder_path(path: str) -> bool:
|
||||
"""验证文件夹路径是否有效"""
|
||||
def validate_folder_path(path: str, allowed_base_paths: list = None) -> bool:
|
||||
"""验证文件夹路径是否有效且安全"""
|
||||
if not path:
|
||||
return False
|
||||
|
||||
@@ -23,8 +23,53 @@ class InputValidator:
|
||||
if re.search(illegal_chars, path):
|
||||
return False
|
||||
|
||||
# 检查路径遍历攻击
|
||||
if '..' in path or path.startswith('/') or '~' in path:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def secure_path_join(base_path: str, user_path: str = "") -> str:
|
||||
"""安全地连接路径,防止路径遍历攻击 - 使用GitHub建议的方法"""
|
||||
import folder_paths
|
||||
|
||||
# 获取ComfyUI的输出目录作为安全基础路径
|
||||
safe_base = folder_paths.get_output_directory()
|
||||
|
||||
try:
|
||||
# 使用realpath解析安全基础路径
|
||||
resolved_safe_base = os.path.realpath(safe_base)
|
||||
|
||||
# 处理base_path
|
||||
if os.path.isabs(base_path):
|
||||
# 绝对路径:直接解析
|
||||
candidate_path = os.path.realpath(base_path)
|
||||
else:
|
||||
# 相对路径:相对于安全基础路径
|
||||
candidate_path = os.path.realpath(os.path.join(safe_base, base_path))
|
||||
|
||||
# 如果有用户路径,继续连接
|
||||
if user_path:
|
||||
candidate_path = os.path.realpath(os.path.join(candidate_path, user_path))
|
||||
|
||||
# 使用commonpath确保路径在安全范围内
|
||||
try:
|
||||
common = os.path.commonpath([resolved_safe_base, candidate_path])
|
||||
# 检查公共路径是否就是安全基础路径
|
||||
if os.path.realpath(common) == resolved_safe_base:
|
||||
return candidate_path
|
||||
else:
|
||||
# 路径逃逸,返回安全基础路径
|
||||
return resolved_safe_base
|
||||
except ValueError:
|
||||
# commonpath失败(通常是不同驱动器),返回安全基础路径
|
||||
return resolved_safe_base
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
# 任何路径操作失败,返回安全基础路径
|
||||
return safe_base
|
||||
|
||||
@staticmethod
|
||||
def validate_filename(filename: str) -> bool:
|
||||
"""验证文件名是否有效"""
|
||||
|
||||
Reference in New Issue
Block a user