#!/usr/bin/env python3 #coding=utf-8 """ Copyright (C) 2022 Plato Mavropoulos """ import os import re import sys import shutil from pathlib import Path, PurePath from common.text_ops import is_encased, to_string # Fix illegal/reserved Windows characters def safe_name(in_name): name_repr = repr(in_name).strip("'") return re.sub(r'[\\/:"*?<>|]+', '_', name_repr) # Check and attempt to fix illegal/unsafe OS path traversals def safe_path(base_path, user_paths): # Convert base path to absolute path base_path = real_path(base_path) # Merge user path(s) to string with OS separators user_path = to_string(user_paths, os.sep) # Create target path from base + requested user path target_path = norm_path(base_path, user_path) # Check if target path is OS illegal/unsafe if is_safe_path(base_path, target_path): return target_path # Re-create target path from base + leveled/safe illegal "path" (now file) nuked_path = norm_path(base_path, safe_name(user_path)) # Check if illegal path leveling worked if is_safe_path(base_path, nuked_path): return nuked_path # Still illegal, raise exception to halt execution raise Exception(f'ILLEGAL_PATH_TRAVERSAL: {user_path}') # Check for illegal/unsafe OS path traversal def is_safe_path(base_path, target_path): base_path = real_path(base_path) target_path = real_path(target_path) common_path = os.path.commonpath((base_path, target_path)) return base_path == common_path # Create normalized base path + OS separator + user path def norm_path(base_path, user_path): return os.path.normpath(base_path + os.sep + user_path) # Get absolute path, resolving any symlinks def real_path(in_path): return os.path.realpath(in_path) # Get Windows/Posix OS agnostic path def agnostic_path(in_path): return PurePath(in_path.replace('\\', os.sep)) # Get absolute parent of path def path_parent(in_path): return Path(in_path).parent.absolute() # Get final path component, w/o suffix def path_stem(in_path): return PurePath(in_path).stem # Get list of path file extensions def path_suffixes(in_path): return PurePath(in_path).suffixes # Check if path is absolute def is_path_absolute(in_path): return Path(in_path).is_absolute() # Create folder(s), controlling parents, existence and prior deletion def make_dirs(in_path, parents=True, exist_ok=False, delete=False): if delete: del_dirs(in_path) Path.mkdir(Path(in_path), parents=parents, exist_ok=exist_ok) # Delete folder(s), if present def del_dirs(in_path): if Path(in_path).is_dir(): shutil.rmtree(in_path) # Walk path to get all files def get_path_files(in_path): path_files = [] for root, _, files in os.walk(in_path): for name in files: path_files.append(os.path.join(root, name)) return path_files # Get path without leading/trailing quotes def get_dequoted_path(in_path): out_path = to_string(in_path).strip() if len(out_path) >= 2 and is_encased(out_path, ("'",'"')): out_path = out_path[1:-1] return out_path # Get absolute file path of argparse object def get_argparse_path(argparse_path): if not argparse_path: # Use runtime directory if no user path is specified absolute_path = runtime_root() else: # Check if user specified path is absolute if is_path_absolute(argparse_path): absolute_path = argparse_path # Otherwise, make it runtime directory relative else: absolute_path = safe_path(runtime_root(), argparse_path) return absolute_path # Process input files (argparse object) def process_input_files(argparse_args, sys_argv=None): if sys_argv is None: sys_argv = [] if len(sys_argv) >= 2: # Drag & Drop or CLI if argparse_args.input_dir: input_path_user = argparse_args.input_dir input_path_full = get_argparse_path(input_path_user) if input_path_user else '' input_files = get_path_files(input_path_full) else: input_files = [file.name for file in argparse_args.files] # Set output fallback value for missing argparse Output and Input Path output_fallback = path_parent(input_files[0]) if input_files else None # Set output path via argparse Output path or argparse Input path or first input file path output_path = argparse_args.output_dir or argparse_args.input_dir or output_fallback else: # Script w/o parameters input_path_user = get_dequoted_path(input('\nEnter input directory path: ')) input_path_full = get_argparse_path(input_path_user) if input_path_user else '' input_files = get_path_files(input_path_full) output_path = get_dequoted_path(input('\nEnter output directory path: ')) output_path_final = get_argparse_path(output_path) return input_files, output_path_final # Get project's root directory def project_root(): root = Path(__file__).parent.parent return real_path(root) # Get runtime's root directory def runtime_root(): if getattr(sys, 'frozen', False): root = Path(sys.executable).parent else: root = project_root() return real_path(root)