update allowed paths handling and document torch versions

Signed-off-by: Vladimir Mandic <[email protected]>
This commit is contained in:
Vladimir Mandic
2026-10-04 13:52:24 +02:00
parent b36bf3e943
commit 3b0e5a8ccf
9 changed files with 100 additions and 45 deletions
+23 -4
View File
@@ -2,10 +2,29 @@
## Update for 2026-10-04
- sdnq: support double-quant, including `nvfp4`
- schedulers: update init signature for new configuration options
- rocm: remove incompatible env flags
- test: update `--test` workflow
- **Torch**
update all torch versions to best known options:
- **cuda**: `torch==2.14.1+cu132`
- **rocm/linux**: `torch==2.14.1+rocm7.14`
- **rocm/windows**: `torch==2.12.0+rocm10.0.0`
- **xpu**: `torch==2.14.1+xpu`
- **openvino**: `torch==2.11.0+cpu` with `openvino==2026.4.1`
- **zluda**: `torch==2.7.1+cu118`
notes:
- if you want to manually install `torch`, sdnext will not interfere/change it
- sdnext only updates `torch` if its not already installed
for best experience use `--reinstall` flags to force updates
- if you want to try cutting-edge versions of torch
use the `--use-nightly` flag
- zluda should only be used on older amd gpus not supported by rocm/windows
- **Compute**
- sdnq: support double-quant, including `nvfp4`
- **Fixes**
- schedulers: update init signature for new configuration options
- rocm: remove incompatible env flags
- test: update `--test` workflow
- paths: improve handling of allowed paths and add settings option to extend them
## Update for 2026-10-01
+1 -1
View File
@@ -677,7 +677,7 @@ def install_cuda():
if args.use_nightly:
cmd = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/cu132 --extra-index-url https://download.pytorch.org/whl/nightly/cu130')
else:
cmd = os.environ.get('TORCH_COMMAND', 'torch==2.14.0+cu132 torchvision==0.29.0+cu132 --index-url https://download.pytorch.org/whl/cu132')
cmd = os.environ.get('TORCH_COMMAND', 'torch==2.14.1+cu132 torchvision==0.29.1+cu132 --index-url https://download.pytorch.org/whl/cu132')
return cmd
+12 -4
View File
@@ -176,10 +176,13 @@ def register_api(api): # register api
# Build list of resolved output paths with labels
folders = []
if base_samples:
folders.append(make_folder(base_samples, os.path.basename(base_samples.rstrip('/\\'))))
if base_grids and base_grids != base_samples:
folders.append(make_folder(base_grids, os.path.basename(base_grids.rstrip('/\\'))))
base_samples = base_samples.rstrip('/\\')
folders.append(make_folder(resolve_output_path(base_samples), "Folders"))
if base_grids:
base_grids = base_grids.rstrip('/\\')
folders.append(make_folder(resolve_output_path(base_grids), "Folders"))
# Use the specific folder setting values as labels (e.g., "outputs/text" -> "outputs/text")
folders.append(make_folder(reference_dir, "Reference"))
folders.append(make_folder(resolve_output_path(base_samples, shared.opts.outdir_txt2img_samples), shared.opts.outdir_txt2img_samples))
folders.append(make_folder(resolve_output_path(base_samples, shared.opts.outdir_img2img_samples), shared.opts.outdir_img2img_samples))
folders.append(make_folder(resolve_output_path(base_samples, shared.opts.outdir_control_samples), shared.opts.outdir_control_samples))
@@ -190,6 +193,7 @@ def register_api(api): # register api
folders.append(make_folder(resolve_output_path(base_grids, shared.opts.outdir_txt2img_grids), shared.opts.outdir_txt2img_grids))
folders.append(make_folder(resolve_output_path(base_grids, shared.opts.outdir_img2img_grids), shared.opts.outdir_img2img_grids))
folders.append(make_folder(resolve_output_path(base_grids, shared.opts.outdir_control_grids), shared.opts.outdir_control_grids))
# Custom browser folders and reference dir
for f in shared.opts.browser_folders.split(','):
f = f.strip()
@@ -204,9 +208,11 @@ def register_api(api): # register api
if path and path not in seen_paths and os.path.isdir(path):
seen_paths.add(path)
unique_folders.append(f)
if shared.demo is not None and path not in shared.demo.allowed_paths:
if (shared.demo is not None) and (path not in shared.demo.allowed_paths):
debug(f'Browser folders allow: {path}')
shared.demo.allowed_paths.append(path)
if os.environ.get('SD_PATH_DEBUG', None):
log.trace(f'Path allowed: why=gallery allowed={shared.demo.allowed_paths}')
debug(f'Browser folders: {unique_folders}')
return unique_folders
@@ -275,3 +281,5 @@ def register_api(api): # register api
except Exception as e:
debug(f'WS error: {e}')
manager.disconnect(ws)
get_folders() # initialize all folders
+7 -1
View File
@@ -13,6 +13,7 @@ from modules.logger import log
_upload_store_getter = None
MAX_B64_BYTES = 256 * 1024 * 1024 # base64 expands ~4/3 and the response is built in memory; larger artifacts are fetched by path instead
debug = os.environ.get('SD_PATH_DEBUG', None) is not None
def register_upload_store(getter_fn):
@@ -204,15 +205,20 @@ def validate_path(fn: str, allowed_dirs: list[str] | None = None, allowed_folder
decoded = unquote(fn).replace('%3A', ':')
sanitized = sanitize_filename(decoded)
resolved = Path(sanitized).resolve()
# log.trace(f'API validate: fn="{fn}" sanitized="{sanitized}" resolved="{resolved}" parents={resolved.parents} allowed={allowed}')
if not any(folder in resolved.parents for folder in allowed):
if debug:
log.error(f'API validate allowed: fn="{fn}" sanitized="{sanitized}" resolved="{resolved}" parents={resolved.parents} allowed={allowed}')
raise HTTPException(status_code=403, detail=f"file not allowed: {resolved}")
if resolved.is_dir():
if allowed_folder:
return str(resolved)
if debug:
log.error(f'API validate folder: fn="{fn}" sanitized="{sanitized}" resolved="{resolved}" parents={resolved.parents} allowed={allowed}')
raise HTTPException(status_code=403, detail=f"directory not allowed: {resolved}")
if not resolved.is_file():
if allowed_file:
return str(resolved)
if debug:
log.error(f'API validate file:: fn="{fn}" sanitized="{sanitized}" resolved="{resolved}" parents={resolved.parents} allowed={allowed}')
raise HTTPException(status_code=404, detail=f"file not found: {resolved}")
return str(resolved)
+20 -22
View File
@@ -45,7 +45,7 @@ sd_configs_path = os.path.join(script_path, "configs")
sd_default_config = os.path.join(sd_configs_path, "v1-inference.yaml")
sd_model_file = cli.ckpt or os.path.join(script_path, 'model.safetensors') # not used
default_sd_model_file = sd_model_file # not used
debug = log.trace if os.environ.get('SD_PATH_DEBUG', None) is not None else lambda *args, **kwargs: None
debug = log.debug if os.environ.get('SD_PATH_DEBUG', None) is not None else lambda *args, **kwargs: None
debug('Trace: PATH')
paths = {}
@@ -65,16 +65,15 @@ def create_path(folder):
log.error(f'Create failed: folder="{folder}" {e}')
def resolve_output_path(base_path: str, specific_path: str) -> str:
def resolve_output_path(base_path: str, specific_path: str | None = None) -> str:
"""
Resolve output path by combining base and specific paths.
- If specific_path is absolute, return it directly (base is ignored)
- If base_path is set and specific_path is relative, join them
- If base_path is empty/None, return specific_path as-is
"""
if not specific_path:
return base_path or ''
return os.path.normpath(base_path)
if os.path.isabs(specific_path):
return specific_path
if base_path:
@@ -123,29 +122,16 @@ def create_paths(opts):
create_path(fix_path('onnx_temp_dir'))
create_path(fix_path('outdir_samples'))
create_path(fix_path('outdir_grids'))
# per-type output dirs resolve against data_path via fix_path(), so create them bare
# only when no base folder is set; with a base configured, the resolved base+specific
# paths below are the real targets and the bare versions would just litter data_path
base_samples = opts.data.get('outdir_samples', '')
base_grids = opts.data.get('outdir_grids', '')
if not base_samples:
create_path(fix_path('outdir_txt2img_samples'))
create_path(fix_path('outdir_img2img_samples'))
create_path(fix_path('outdir_control_samples'))
create_path(fix_path('outdir_extras_samples'))
create_path(fix_path('outdir_init_images'))
create_path(fix_path('outdir_save'))
create_path(fix_path('outdir_video'))
if not base_grids:
create_path(fix_path('outdir_txt2img_grids'))
create_path(fix_path('outdir_img2img_grids'))
create_path(fix_path('outdir_control_grids'))
create_path(fix_path('styles_dir'))
create_path(fix_path('yolo_dir'))
create_path(fix_path('wildcards_dir'))
create_path(fix_path('autocomplete_dir'))
# Create resolved output paths (base + specific)
# per-type output dirs resolve against data_path via fix_path(), so create them bare
# only when no base folder is set; with a base configured, the resolved base+specific
# paths below are the real targets and the bare versions would just litter data_path
base_samples = opts.data.get('outdir_samples', '').rstrip('/\\')
base_grids = opts.data.get('outdir_grids', '').rstrip('/\\')
if base_samples:
create_path(resolve_output_path(base_samples, opts.data.get('outdir_txt2img_samples', '')))
create_path(resolve_output_path(base_samples, opts.data.get('outdir_img2img_samples', '')))
@@ -154,10 +140,22 @@ def create_paths(opts):
create_path(resolve_output_path(base_samples, opts.data.get('outdir_save', '')))
create_path(resolve_output_path(base_samples, opts.data.get('outdir_video', '')))
create_path(resolve_output_path(base_samples, opts.data.get('outdir_init_images', '')))
else:
create_path(fix_path('outdir_txt2img_samples'))
create_path(fix_path('outdir_img2img_samples'))
create_path(fix_path('outdir_control_samples'))
create_path(fix_path('outdir_extras_samples'))
create_path(fix_path('outdir_init_images'))
create_path(fix_path('outdir_save'))
create_path(fix_path('outdir_video'))
if base_grids:
create_path(resolve_output_path(base_grids, opts.data.get('outdir_txt2img_grids', '')))
create_path(resolve_output_path(base_grids, opts.data.get('outdir_img2img_grids', '')))
create_path(resolve_output_path(base_grids, opts.data.get('outdir_control_grids', '')))
else:
create_path(fix_path('outdir_txt2img_grids'))
create_path(fix_path('outdir_img2img_grids'))
create_path(fix_path('outdir_control_grids'))
class Prioritize:
+6 -2
View File
@@ -476,9 +476,9 @@ def create_settings(cmd_opts):
# --- System Paths ---
options_templates.update(options_section(('system-paths', "System Paths"), {
"models_paths_sep_options": OptionInfo("<h2>Models Paths</h2>", "", gr.HTML),
"models_paths_sep_options": OptionInfo("<h2>Models root folder</h2>", "", gr.HTML),
"models_dir": OptionInfo('models', "Root model folder", folder=True),
"model_paths_sep_options": OptionInfo("<h2>Paths for specific models</h2>", "", gr.HTML),
"model_paths_sep_options": OptionInfo("<h2>Folders for specific models</h2>", "", gr.HTML),
"ckpt_dir": OptionInfo(os.path.join(paths.models_path, 'Stable-diffusion'), "Folder with stable diffusion models", folder=True),
"diffusers_dir": OptionInfo(os.path.join(paths.models_path, 'Diffusers'), "Folder with Huggingface models", folder=True),
"hfcache_dir": OptionInfo(default_hfcache_dir, "Folder for Huggingface cache", folder=True),
@@ -500,6 +500,10 @@ def create_settings(cmd_opts):
"scunet_models_path": OptionInfo(os.path.join(paths.models_path, 'SCUNet'), "Folder with SCUNet models", folder=True),
"swinir_models_path": OptionInfo(os.path.join(paths.models_path, 'SwinIR'), "Folder with SwinIR models", folder=True),
"clip_models_path": OptionInfo(os.path.join(paths.models_path, 'CLIP'), "Folder with CLIP models", folder=True),
"allowed_paths_sep_options": OptionInfo("<h2>Allowed folders</h2>", "", gr.HTML),
"allowed_paths": OptionInfo('', "Additional folders with access permissions", folder=True),
"other_paths_sep_options": OptionInfo("<h2>Cache folders</h2>", "", gr.HTML),
"clean_temp_dir_at_start": OptionInfo(True, "Cleanup temporary folder on startup"),
"temp_dir": OptionInfo("", "Directory for temporary files; leave empty for default", folder=True),
+1 -1
View File
@@ -1181,7 +1181,7 @@
{"id": "video_params_generic", "label": "Models", "localized": "", "hint": "Download, convert or merge your models and manage models metadata", "ui": "video"},
{"id": "", "label": "Models & Networks", "localized": "", "hint": "View lists of all available models and networks"},
{"id": "", "label": "Models List", "localized": "", "hint": "Table listing all discovered local model checkpoints, paths, and architectures"},
{"id": "", "label": "Models Paths", "localized": "", "hint": "Configure directory locations for models, networks, and upscalers", "ui": "settings_system-paths"},
{"id": "", "label": "Models root folder", "localized": "", "hint": "Configure directory locations for models, networks, and upscalers", "ui": "settings_system-paths"},
{"id": "", "label": "Modern", "localized": "", "hint": "ModernUI responsive theme with dynamic pane layouts", "ui": "settings_ui"},
{"id": "", "label": "ModernUI", "localized": "", "hint": "Modern responsive user interface theme for SD.Next"},
{"id": "", "label": "Modular Pipelines", "localized": "", "hint": "Modular Diffusers pipeline architecture settings", "ui": "settings_model_options"},
+29 -9
View File
@@ -323,6 +323,14 @@ def mount_subpath(app):
log.info(f'Mounted: subpath="{shared.opts.subpath}"')
def resolve_paths(allowed_paths):
folders = [modules.paths.resolve_output_path(f) for f in allowed_paths]
folders = [f for f in folders if os.path.isdir(f)]
folders = sorted(set(folders)) # remove duplicates
log.debug(f'Paths: {folders}')
return folders
def start_server(
blocks,
server_name: str | None = None, # pylint: disable=redefined-outer-name
@@ -357,6 +365,23 @@ def start_server(
path_to_local_server = f"http://{url_host_name}:{server_port}/"
return server_name, server_port, path_to_local_server, app, server
def get_paths():
allowed_paths = [os.path.dirname(__file__)]
if shared.cmd_opts.data_dir is not None and os.path.isdir(shared.cmd_opts.data_dir):
allowed_paths.append(shared.cmd_opts.data_dir)
if shared.cmd_opts.models_dir is not None and os.path.isdir(shared.cmd_opts.models_dir):
allowed_paths.append(shared.cmd_opts.models_dir)
if shared.cmd_opts.allowed_paths is not None:
allowed_paths += [p for p in shared.cmd_opts.allowed_paths if os.path.isdir(p)]
if os.environ.get('SD_PATH_DEBUG', None):
log.trace(f'Path allowed: why=args allowed={shared.demo.allowed_paths}')
if shared.opts.allowed_paths:
folders = [f.strip() for f in shared.opts.allowed_paths.split(',') if f.strip()]
allowed_paths = [f for f in folders if os.path.isdir(f)]
if os.environ.get('SD_PATH_DEBUG', None):
log.trace(f'Path allowed: why=opts allowed={shared.demo.allowed_paths}')
return allowed_paths
def start_ui():
log.debug('UI start sequence')
@@ -396,14 +421,8 @@ def start_ui():
global local_url # pylint: disable=global-statement
stdout = io.StringIO()
allowed_paths = [os.path.dirname(__file__)]
if shared.cmd_opts.data_dir is not None and os.path.isdir(shared.cmd_opts.data_dir):
allowed_paths.append(shared.cmd_opts.data_dir)
if shared.cmd_opts.models_dir is not None and os.path.isdir(shared.cmd_opts.models_dir):
allowed_paths.append(shared.cmd_opts.models_dir)
if shared.cmd_opts.allowed_paths is not None:
allowed_paths += [p for p in shared.cmd_opts.allowed_paths if os.path.isdir(p)]
log.info(f'Server: name={server_name} port={shared.cmd_opts.port} paths={allowed_paths} ssl={shared.cmd_opts.tls_keyfile}:{shared.cmd_opts.tls_certfile} auth={len(auth_pairs)}')
log.info(f'Server: name={server_name} port={shared.cmd_opts.port} ssl={shared.cmd_opts.tls_keyfile}:{shared.cmd_opts.tls_certfile} auth={len(auth_pairs)}')
with contextlib.redirect_stdout(stdout):
import gradio.networking
gradio.networking.start_server = start_server
@@ -421,7 +440,7 @@ def start_ui():
show_api=False,
quiet=True,
favicon_path='ui/assets/favicon.svg',
allowed_paths=allowed_paths,
allowed_paths=get_paths(),
app_kwargs=fastapi_args,
_frontend=True and shared.cmd_opts.share,
)
@@ -490,6 +509,7 @@ def webui(restart=False, _exit=False, profiler=None):
load_model()
mount_subpath(app)
shared.demo.allowed_paths = resolve_paths(shared.demo.allowed_paths)
shared.opts.save()
if shared.cmd_opts.profile: