2024-04-23 18:22:53 -04:00
|
|
|
#!/usr/bin/env python3 -B
|
|
|
|
# coding=utf-8
|
2022-04-01 10:43:22 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
"""
|
2024-04-23 18:22:53 -04:00
|
|
|
Copyright (C) 2022-2024 Plato Mavropoulos
|
2022-04-17 13:48:43 -04:00
|
|
|
"""
|
|
|
|
|
2022-04-01 10:43:22 -04:00
|
|
|
import os
|
|
|
|
import re
|
2022-04-17 13:48:43 -04:00
|
|
|
import shutil
|
2024-04-23 18:22:53 -04:00
|
|
|
import stat
|
|
|
|
import sys
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
from pathlib import Path, PurePath
|
2022-04-01 10:43:22 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
from common.system import get_os_ver
|
2022-04-30 18:33:43 -04:00
|
|
|
from common.text_ops import is_encased, to_string
|
2022-04-15 11:17:58 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
MAX_WIN_COMP_LEN = 255
|
|
|
|
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def safe_name(in_name):
|
2024-04-23 18:22:53 -04:00
|
|
|
"""
|
|
|
|
Fix illegal/reserved Windows characters
|
|
|
|
Can also be used to nuke dangerous paths
|
|
|
|
"""
|
|
|
|
|
2022-04-16 16:39:56 -04:00
|
|
|
name_repr = repr(in_name).strip("'")
|
2022-04-01 10:43:22 -04:00
|
|
|
|
2022-04-16 16:39:56 -04:00
|
|
|
return re.sub(r'[\\/:"*?<>|]+', '_', name_repr)
|
2022-04-01 10:43:22 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def safe_path(base_path, user_paths):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Check and attempt to fix illegal/unsafe OS path traversals """
|
|
|
|
|
2022-04-16 16:39:56 -04:00
|
|
|
# Convert base path to absolute path
|
2022-04-17 13:48:43 -04:00
|
|
|
base_path = real_path(base_path)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
# Merge user path(s) to string with OS separators
|
2022-04-15 11:17:58 -04:00
|
|
|
user_path = to_string(user_paths, os.sep)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
# Create target path from base + requested user path
|
2022-04-17 13:48:43 -04:00
|
|
|
target_path = norm_path(base_path, user_path)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
# Check if target path is OS illegal/unsafe
|
2022-04-16 16:39:56 -04:00
|
|
|
if is_safe_path(base_path, target_path):
|
2022-04-15 11:17:58 -04:00
|
|
|
return target_path
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
# Re-create target path from base + leveled/safe illegal "path" (now file)
|
2022-04-17 13:48:43 -04:00
|
|
|
nuked_path = norm_path(base_path, safe_name(user_path))
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
# Check if illegal path leveling worked
|
2022-04-16 16:39:56 -04:00
|
|
|
if is_safe_path(base_path, nuked_path):
|
2022-04-15 11:17:58 -04:00
|
|
|
return nuked_path
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
# Still illegal, raise exception to halt execution
|
2024-04-23 18:22:53 -04:00
|
|
|
raise OSError(f'Encountered illegal path traversal: {user_path}')
|
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
|
2022-04-16 16:39:56 -04:00
|
|
|
def is_safe_path(base_path, target_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Check for illegal/unsafe OS path traversal """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
base_path = real_path(base_path)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
target_path = real_path(target_path)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-16 16:39:56 -04:00
|
|
|
common_path = os.path.commonpath((base_path, target_path))
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
return base_path == common_path
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def norm_path(base_path, user_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Create normalized base path + OS separator + user path """
|
|
|
|
|
2022-04-15 11:17:58 -04:00
|
|
|
return os.path.normpath(base_path + os.sep + user_path)
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def real_path(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get absolute path, resolving any symlinks """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
return os.path.realpath(in_path)
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def agnostic_path(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get Windows/Posix OS agnostic path """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
return PurePath(in_path.replace('\\', os.sep))
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def path_parent(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get absolute parent of path """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
return Path(in_path).parent.absolute()
|
2022-04-16 16:39:56 -04:00
|
|
|
|
2022-08-28 13:02:55 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
def path_name(in_path, limit=False):
|
|
|
|
""" Get final path component, with suffix """
|
|
|
|
|
|
|
|
comp_name = PurePath(in_path).name
|
|
|
|
|
|
|
|
if limit and get_os_ver()[1]:
|
|
|
|
comp_name = comp_name[:MAX_WIN_COMP_LEN - len(extract_suffix())]
|
|
|
|
|
|
|
|
return comp_name
|
|
|
|
|
|
|
|
|
2022-06-21 07:23:08 -04:00
|
|
|
def path_stem(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get final path component, w/o suffix """
|
|
|
|
|
2022-06-21 07:23:08 -04:00
|
|
|
return PurePath(in_path).stem
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-06-28 17:44:42 -04:00
|
|
|
def path_suffixes(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get list of path file extensions """
|
|
|
|
|
2022-10-31 11:04:50 -04:00
|
|
|
return PurePath(in_path).suffixes or ['']
|
2022-06-28 17:44:42 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def is_path_absolute(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Check if path is absolute """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
return Path(in_path).is_absolute()
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def make_dirs(in_path, parents=True, exist_ok=False, delete=False):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Create folder(s), controlling parents, existence and prior deletion """
|
|
|
|
|
2022-07-06 10:54:17 -04:00
|
|
|
if delete:
|
|
|
|
del_dirs(in_path)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
Path.mkdir(Path(in_path), parents=parents, exist_ok=exist_ok)
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def del_dirs(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Delete folder(s), if present """
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
if Path(in_path).is_dir():
|
2024-04-23 18:22:53 -04:00
|
|
|
shutil.rmtree(in_path, onerror=clear_readonly_callback)
|
|
|
|
|
2022-07-13 18:32:25 -04:00
|
|
|
|
2022-08-28 13:02:55 -04:00
|
|
|
def copy_file(in_path, out_path, meta=False):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Copy file to path with or w/o metadata """
|
|
|
|
|
2022-08-28 13:02:55 -04:00
|
|
|
if meta:
|
|
|
|
shutil.copy2(in_path, out_path)
|
|
|
|
else:
|
|
|
|
shutil.copy(in_path, out_path)
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
|
|
|
def clear_readonly(in_path):
|
|
|
|
""" Clear read-only file attribute """
|
|
|
|
|
2022-07-13 18:32:25 -04:00
|
|
|
os.chmod(in_path, stat.S_IWRITE)
|
2024-04-23 18:22:53 -04:00
|
|
|
|
|
|
|
|
|
|
|
def clear_readonly_callback(in_func, in_path, _):
|
|
|
|
""" Clear read-only file attribute (on shutil.rmtree error) """
|
|
|
|
|
|
|
|
clear_readonly(in_path)
|
|
|
|
|
2022-07-13 18:32:25 -04:00
|
|
|
in_func(in_path)
|
2022-04-16 16:39:56 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-01 10:43:22 -04:00
|
|
|
def get_path_files(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Walk path to get all files """
|
|
|
|
|
2022-04-01 10:43:22 -04:00
|
|
|
path_files = []
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-01 10:43:22 -04:00
|
|
|
for root, _, files in os.walk(in_path):
|
|
|
|
for name in files:
|
|
|
|
path_files.append(os.path.join(root, name))
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-01 10:43:22 -04:00
|
|
|
return path_files
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-30 18:33:43 -04:00
|
|
|
def get_dequoted_path(in_path):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get path without leading/trailing quotes """
|
|
|
|
|
2022-04-30 18:33:43 -04:00
|
|
|
out_path = to_string(in_path).strip()
|
2024-04-23 18:22:53 -04:00
|
|
|
|
|
|
|
if len(out_path) >= 2 and is_encased(out_path, ("'", '"')):
|
2022-04-30 18:33:43 -04:00
|
|
|
out_path = out_path[1:-1]
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-30 18:33:43 -04:00
|
|
|
return out_path
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-09-12 16:09:12 -04:00
|
|
|
def extract_suffix():
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Set utility extraction stem """
|
|
|
|
|
2022-09-12 16:09:12 -04:00
|
|
|
return '_extracted'
|
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-09-12 16:09:12 -04:00
|
|
|
def get_extract_path(in_path, suffix=extract_suffix()):
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get utility extraction path """
|
|
|
|
|
2022-09-12 16:09:12 -04:00
|
|
|
return f'{in_path}{suffix}'
|
2022-04-06 18:13:07 -04:00
|
|
|
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
def project_root():
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get project's root directory """
|
|
|
|
|
|
|
|
return real_path(Path(__file__).parent.parent)
|
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
|
|
|
|
def runtime_root():
|
2024-04-23 18:22:53 -04:00
|
|
|
""" Get runtime's root directory """
|
|
|
|
|
2022-04-06 18:13:07 -04:00
|
|
|
if getattr(sys, 'frozen', False):
|
2022-04-17 13:48:43 -04:00
|
|
|
root = Path(sys.executable).parent
|
2022-04-06 18:13:07 -04:00
|
|
|
else:
|
2022-04-17 13:48:43 -04:00
|
|
|
root = project_root()
|
2024-04-23 18:22:53 -04:00
|
|
|
|
2022-04-17 13:48:43 -04:00
|
|
|
return real_path(root)
|