From e4ec6b40edadce7e9010c17b07267c516f8a69a7 Mon Sep 17 00:00:00 2001 From: Ratul Hasan <34002411+RaSan147@users.noreply.github.com> Date: Thu, 21 Nov 2024 03:11:56 +0600 Subject: [PATCH] hotfix to 0.9.7 --- CHANGELOG.MD | 10 ++++-- README.md | 41 ++++++++++++++++-------- SECURITY PLAN.MD | 2 +- VERSION | 2 +- dev_src/_arg_parser.py | 15 ++++++--- dev_src/_fs_utils.py | 37 ++++++++++++++++++--- dev_src/_sub_extractor.py | 14 ++++---- dev_src/_zipfly_manager.py | 4 +-- dev_src/clone.py | 18 +++++++---- dev_src/pyroboxCore.py | 28 ++++++++-------- dev_src/pyrobox_ServerHost.py | 6 ++-- dev_src/server.py | 4 +-- dev_src/tools.py | 10 ++++-- setup.cfg | 4 +-- setup.py | 3 +- src/__init__.py | 6 ++-- src/__main__.py | 5 ++- src/_arg_parser.py | 15 ++++++--- src/_fs_utils.py | 35 ++++++++++++++++++-- src/_sub_extractor.py | 14 ++++---- src/_zipfly_manager.py | 4 +-- src/clone.py | 18 +++++++---- src/pyroDB.py | 60 +++++++++++++++++------------------ src/pyroboxCore.py | 28 ++++++++-------- src/pyrobox_ServerHost.py | 8 ++--- src/server.py | 28 ++++++++-------- src/tools.py | 20 +++++++----- 27 files changed, 275 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 66e254a..533837a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,9 @@ +# Version 0.9.7 + ## HOTFIX: + * Fixed requests requirement in the list + * **Server** In init and main, removed the import of the submodules + * **Server** Added folder size limit for zip (default 6GB) + # Version 0.9.6 ## Client-side Changes: * Fixed Logout button in account (No more Sword Art Online) @@ -13,7 +19,7 @@ * **Server:** Improved security (naming files and renaming) * **Server:** Added QR support (needs implementation) * **Server:** Updated and improved database package (pyroDB) with improved speed and application - * **Server:** Added 7z support (if installed in device) + * **Server:** Added 7z support (if installed in device) (searches for common locations) * **Server:** Added QR code for easy access to the server (from terminal -q) ## Fixes: @@ -198,7 +204,7 @@ * `TODO`: To change log level, use `--log-level` or `-l` flag * To change log level programmatically, use `pyroboxCore.logger.setLevel(logging.DEBUG|INFO|WARN|ERROR)` * REMOVED `pyrobox.clone` module (stil available in `dev_src` folder) - * The Entire server is now using `@SimpleHTTPRequestHandler.on_req` decorator importing from pyroboxCore to handle requests + * The Entire server is now using `@SimpleHTTPRequestHandler.on_req` decorator importing from pyroboxCore to handle requests ### check v0.6.1 for more info * added send_file function to pyroboxCore * improved POST request handling diff --git a/README.md b/README.md index 6a762a6..2020efd 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ * 🔜 More comming soon * All of these without the need of any internet connection (having connection will provide better experience) + ## Server side requirement * Python 3.7 or higher. Older support available.[^1] @@ -81,15 +82,15 @@ https://github.com/RaSan147/pyrobox/assets/34002411/eb2ac313-f95a-4334-a265-c3fe 1. Simply running the code on will create a server on `CURRENT WORKING DIRECTORY` on `Port: 6969` 1. On browser (on device under same router/wifi network), go to `deviceIP:port_number` to see the output like this: `http://192.168.0.101:6969/` * you must allow python in firewall to access network, check [FAQ](#faq) for more help -1. To change the server running directory, - * i) either edit the code (see `config` class at top) - * ii) or add `-d` or `--directory` command line argument when launching the program +1. To change the server running directory, + * (i) either edit the code (see `config` class at top) + * (ii) or add `-d` or `--directory` command line argument when launching the program * `pyrobox -d .` to launch the server in current directory (where the file is) * `pyrobox -d "D:\Server\Public folder\"` (Use Double-Quotation while directory has space) * `pyrobox -d "D:/Server/Public folder"` (Forward or backward slash really doesn't matter, unless your terminal thinks otherwise) 1. To change port number - * i) just edit the code for permanent change (see `config` class at top) - * ii) or add the port number at the end of the command line arg + * (i) just edit the code for permanent change (see `config` class at top) + * (ii) or add the port number at the end of the command line arg * `pyrobox 45678` # will run on port 45678 * `pyrobox -d . 45678` # will run on port 45678 in current directory @@ -97,11 +98,11 @@ https://github.com/RaSan147/pyrobox/assets/34002411/eb2ac313-f95a-4334-a265-c3fe * Add bind add `-bind {address}` # ie: `-bind 127.0.0.2` or `-bind 127.0.0.99` 1. To change upload password - * i) or add `-k` or `--password` command line argument when launching the program + * (i) or add `-k` or `--password` command line argument when launching the program * `pyrobox -k "my new password"` to launch the server with new password * `pyrobox -k ""` to launch the server without password * `pyrobox` to launch the server with default password (SECret) - * ii) just edit the code for permanent change (see `config` class at top) + * (ii) just edit the code for permanent change (see `config` class at top) 1. Optional configurations @@ -117,12 +118,13 @@ usage: `pyrobox [--password PASSWORD] [--no-upload] [--no-zip] [--no-update] [-- | Flags/Arg `value` | Description | | --------------------- | ------------| - |--password `PASSWORD`, -k `PASSWORD` | Upload Password (GUESTS users and Nameless server users must use it to upload files)(default: SECret)| + |--password `PASSWORD`, -k `PASSWORD` | Upload Password (GUESTS users and Nameless server users must use it to upload files)(default: `SECret`)| |--directory `DIRECTORY`, -d `DIRECTORY` | Specify alternative directory [default: current directory] - |--bind `ADDRESS`, -b `ADDRESS` | Specify alternate bind address [default: all interfaces]| + |--bind `ADDRESS`, -b `ADDRESS` | Specify alternate bind address [default: all interfaces (`0.0.0.0`)]| |--version, -v | show program's version number and exit| |-h, --help | show this help message and exit| |--no-extra-log | Disable file path and [= + - #] based logs (default: False)| + |--zip-limit, -zl `ZIP_LIMIT` | Set the limit of zip file size in bytes/KB/MB/GB/TB (default: `6GB`)| ## Customization Options @@ -155,12 +157,23 @@ usage: `pyrobox [--password PASSWORD] [--no-upload] [--no-zip] [--no-update] [-- 1. `pyrobox -n "My Server3" -aid "admin" -ak "admin123" -ng -ns` # Only admin can access the server. No guest allowed, no signup allowed. Admin can add user from admin page +## Clone Directory + + 1. Make a python file with the following code + ``` python + from pyrobox import clone + clone.main() + ``` + 1. Run the file. Follow the instructions. + 1. For advanced use, check the source code of `clone.py` in the `pyrobox` module. + + ## TODO * https://github.com/RaSan147/pyrobox/issues/33 Show thumbnails, for png and jpg (how to do with just standard library?), For others, just show extension. * https://github.com/RaSan147/pyrobox/issues/34 Copy stream URL for videos to play with any video player * https://github.com/RaSan147/pyrobox/issues/36 Add side bar to do something 🤔 -* check output ip and port accuracy on multiple os +* check output ip and port accuracy on multiple os * https://github.com/RaSan147/pyrobox/issues/37 Backup code if Reload causes unhandled issue and can't be accessed * Add more flags to disable specific features @@ -177,7 +190,7 @@ usage: `pyrobox [--password PASSWORD] [--no-upload] [--no-zip] [--no-update] [-- Using WSL, "PIP not found" > Run this to install `pip3` and add `pip` to path - + ``` bash sudo apt -y purge python3-pip sudo python3 -m pip uninstall pip @@ -206,7 +219,7 @@ usage: `pyrobox [--password PASSWORD] [--no-upload] [--no-zip] [--no-update] [--
Deleted (Move to Recycle), But WHERE ARE THEY?? [on LINUX & WSL] - + > Actually the feature is working fine, unfortunately NO-GUI mode linux and WSL don't recycle bin, so you can't find it! > And to make things worse, **you need to manually clear the recyle bin** from `~/.local/share/Trash` > **SO I'D RECOMMAND USING DELETE PARMANENTLY** @@ -214,11 +227,11 @@ usage: `pyrobox [--password PASSWORD] [--no-upload] [--no-zip] [--no-update] [--
Running on WINDOWS, but can't access with other device [FIREWALL] - + > You probably have **FireWall ON** and not configured. > For your safety, I'd recommend you to allow Python on private network and run the server when your network is Private. > IN SHORT: ALLOW PYTHON ON FIREWALL, RUN THE SERVER - + > *note: allowed on private but using public network on firewall will cause similar issue, you gotta make both same or allow python both on public and private*
diff --git a/SECURITY PLAN.MD b/SECURITY PLAN.MD index efb2bbe..78d03a3 100644 --- a/SECURITY PLAN.MD +++ b/SECURITY PLAN.MD @@ -9,7 +9,7 @@ ------ ## stream-url: -Users can create stream-url . This will **allow nonblocking access to the file** and file only. So, anyone with force flag need to use stream-url to download file with IDM or stream .mkv file on VLC . +Users can create stream-url . This will **allow nonblocking access to the file** and file only. So, anyone with force flag need to use stream-url to download file with IDM or stream .mkv file on VLC . **Format:** `/stream?id=(5alphanumerical)` [60M links] diff --git a/VERSION b/VERSION index 85b7c69..c81aa44 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.6 +0.9.7 diff --git a/dev_src/_arg_parser.py b/dev_src/_arg_parser.py index d2cfa0d..01ed0a9 100644 --- a/dev_src/_arg_parser.py +++ b/dev_src/_arg_parser.py @@ -1,14 +1,16 @@ # add additional arguments to the parser - +config = None # the config must be imported from pyroboxCore -# from pyroboxCore import config +if __name__ == "__main__": + from pyroboxCore import config + -def main(config): +def main(config=config): config.parser.add_argument('--password', '-k', default=config.PASSWORD, type=str, help='[Value] Upload Password (default: %(default)s)') - + config.parser.add_argument('--qr', '-q', @@ -90,4 +92,7 @@ def main(config): default=False, help="[Flag] Only allowed to see file list, nothing else (default: %(default)s)") - + config.parser.add_argument('--zip-limit', '-zl', + default="6GB", + type=str, + help='[Value] Max size of zip file allowed to download (default: %(default)s) [can be bytes without suffix or suffixes like 1KB, 1MB, 1GB, 1TB]') diff --git a/dev_src/_fs_utils.py b/dev_src/_fs_utils.py index 1231538..fccc015 100644 --- a/dev_src/_fs_utils.py +++ b/dev_src/_fs_utils.py @@ -22,7 +22,7 @@ import time import traceback import urllib.parse -from tools import os_scan_walk, xpath +from tools import os_scan_walk_gen, xpath from _exceptions import LimitExceed @@ -77,7 +77,7 @@ def walk_dir(*path, yield_dir=False): yield_dir (bool): if True yields directories too (default: False) """ - for f in os_scan_walk(*path, allow_dir=yield_dir): + for f in os_scan_walk_gen(*path, allow_dir=yield_dir): yield f @@ -281,6 +281,35 @@ def humanbytes(B: int): return ret +def reverse_humanbytes(human_str:str): + """ + Converts human readable size to bytes + """ + human_str = human_str.strip().lower() + + if human_str.endswith('bytes'): + return int(human_str[:-5].strip()) + if human_str.endswith('byte'): + return int(human_str[:-4].strip()) + + if human_str.endswith('b'): + human_str = human_str[:-1].strip() + + if human_str.endswith('k'): + return int(float(human_str[:-1].strip()) * 1024) + + if human_str.endswith('m'): + return int(float(human_str[:-1].strip()) * 1024**2) + + if human_str.endswith('g'): + return int(float(human_str[:-1].strip()) * 1024**3) + + if human_str.endswith('t'): + return int(float(human_str[:-1].strip()) * 1024**4) + + return int(human_str) + + def get_dir_m_time(path): """ Get the last modified time of a directory and all its subdirectories. @@ -492,7 +521,7 @@ def _start(self, server:"ServerHost"): self.err(f"Failed to upload {os_fn}") break - + else: self.waited += 1 self.sleep() @@ -503,7 +532,7 @@ def _start(self, server:"ServerHost"): def kill(self): self.active = False self.done = True - + for f in tuple(self.serial_io.queue): name = f[0].name if not f[0].closed: diff --git a/dev_src/_sub_extractor.py b/dev_src/_sub_extractor.py index efe9571..dfbcaaf 100644 --- a/dev_src/_sub_extractor.py +++ b/dev_src/_sub_extractor.py @@ -19,14 +19,14 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None """ output_paths = [] sub_names = [] - + if not os.path.isfile(input_file): raise FileNotFoundError(f"The file '{input_file}' does not exist.") if not FFMPEG: # we don't want to raise an exception here, just return an empty list return [] - + try: # Run ffmpeg to analyze the file process = subprocess.run( @@ -36,7 +36,7 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None text=True ) output = process.stderr # ffmpeg logs info in stderr - + # Extract stream information (Subtitle streams) # Example: Stream #0:1(eng): Subtitle: dvd_subtitle stream_pattern = re.compile( @@ -52,7 +52,7 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None sub_name = f"{sub_name}_{n}" else: sub_name = f"subtitle_{n}" - + sub_names.append(sub_name) n += 1 @@ -77,16 +77,16 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None os.makedirs(output_dir, exist_ok=True) # Create the output directory if it doesn't exist output_filename = f"{os.path.splitext(input_file)[0]}_{sub_name}.{output_format}" - + # Generate output path output_paths.append((sub_name, output_filename)) - + # Use ffmpeg to extract the audio stream subprocess.run( [FFMPEG, "-i", input_file, "-map", stream_index, '-y', output_filename], check=True ) - + return output_paths except subprocess.CalledProcessError as e: diff --git a/dev_src/_zipfly_manager.py b/dev_src/_zipfly_manager.py index ceaf8a1..7747178 100644 --- a/dev_src/_zipfly_manager.py +++ b/dev_src/_zipfly_manager.py @@ -19,7 +19,7 @@ -from _fs_utils import get_dir_m_time, _get_tree_path_n_size +from _fs_utils import get_dir_m_time, _get_tree_path_n_size, humanbytes from _exceptions import LimitExceed @@ -535,7 +535,7 @@ def err(msg): try: fs = _get_tree_path_n_size(path, must_read=True, path_type="both", limit=self.size_limit, add_dirs=True) except LimitExceed as e: - return err("DIRECTORY SIZE LIMIT EXCEED") + return err(f"DIRECTORY SIZE LIMIT EXCEED [CURRENT LIMIT: {humanbytes(self.size_limit)}]") source_size = sum(i[1] for i in fs) fm = [i[0] for i in fs] source_m_time = get_dir_m_time(path) diff --git a/dev_src/clone.py b/dev_src/clone.py index 6a9fd7f..34a07bf 100644 --- a/dev_src/clone.py +++ b/dev_src/clone.py @@ -69,7 +69,7 @@ def check_exist(url, path, check_method): if local_last_modify == original_modify and check_method == "date": return True - + if local_last_modify >= original_modify and check_method == "date+": return True @@ -126,7 +126,7 @@ def dl(url, path, overwrite, check_method): CANCEL = True os.remove(local_filename) return False - + except Exception: traceback.print_exc() print("ALERT: [dl] Server is probably down") @@ -159,7 +159,7 @@ def clone(self, url, path = "./", overwrite = False, check_exist = "date", delet """ if url[-1] != "/": url += "/" - + Q = Queue() def get_json(url): @@ -178,7 +178,7 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr if path[-1] != "/": path += "/" - + os.makedirs(path, exist_ok=True) # make sure the directory exists even if it's empty json = get_json(url) @@ -236,7 +236,7 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr -if __name__ == "__main__": +def main(): url = input("URL: ") path = input("Save to: ") @@ -248,7 +248,11 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr check_exist = "date+" o = None while not o: - o = input("Check exist? (date/date+/size) [Default: date+]: ") + o = input("""Check exist? [Default: date+] +date: download if remote file is update time same as local file +date+: download if remote file is newer or same as local file (ignored if local file is newer) +size: download if remote file size is different from local file +>>> """) if o in ["date", "date+", "size"]: check_exist = o else: @@ -271,3 +275,5 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr for future in as_completed(cloner.futures): bool(future.result()) +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/dev_src/pyroboxCore.py b/dev_src/pyroboxCore.py index 06b794d..f00f404 100644 --- a/dev_src/pyroboxCore.py +++ b/dev_src/pyroboxCore.py @@ -515,7 +515,7 @@ def allowed_CORS(self, method) -> Union[str, None]: @classmethod def allow_CORS(self, method, origin): """Add a method to the allowed list - + `method` = `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH`\n `origin` = `*`, `http://example.com`, `https://example.com`, `http://example.com:8080`, `https://example.com:8080` """ @@ -895,7 +895,7 @@ def send_header(self, keyword, value): def end_headers(self): """Send the blank line ending the MIME headers.""" - + CORS_POLICY = self.allowed_CORS(self.method) if self.allowed_CORS(self.method): self.send_header('Access-Control-Allow-Origin', CORS_POLICY) @@ -1165,7 +1165,7 @@ def alt_directory(dir, method='', url='', hasQ=(), QV={}, fragent='', url_regex= alternative directory handler (only handles GET and HEAD request for files) """ self = __class__ - + @self.on_req(method=method, url=url, hasQ=hasQ, QV=QV, fragent=fragent, url_regex=url_regex) def alt_dir_function(self: Type[__class__], *args, **kwargs): """ @@ -1173,7 +1173,7 @@ def alt_dir_function(self: Type[__class__], *args, **kwargs): """ file = self.url_path.split("/")[-1] - + if not os.path.exists(tools.xpath(dir, file)): self.send_error(HTTPStatus.NOT_FOUND, "File not found") return None @@ -1430,7 +1430,7 @@ def return_file(self, path, filename=None, download=False, cache_control="", coo if cache_control: self.send_header("Cache-Control", cache_control) - + self.send_header('Accept-Ranges', 'bytes') @@ -1479,7 +1479,7 @@ def return_file(self, path, filename=None, download=False, cache_control="", coo def send_file(self, path, filename=None, download=False, cache_control='', cookie:Union[SimpleCookie, str]=None): '''sends the head and file to client''' file = self.return_file(path, filename, download, cache_control, cookie=cookie) - if not file: + if not file: return # already flushed (with error/unchanged) if self.command == "HEAD": return # to avoid sending file on get request @@ -1545,11 +1545,11 @@ def get_displaypath(self, url_path): def get_rel_path(self, filename): """Return the relative path to the file, FOR WEB.""" return urllib.parse.unquote(posixpath.join(self.url_path, filename), errors='surrogatepass') - + def get_web_path(self, path:str, times=1): """replace current directory with /""" return path.replace(self.directory, "/", times) - + def path_safety_check(self, paths:Union[str, List], *more_paths:Union[str, List]): """check if path is safe paths: list of paths to check""" @@ -1569,11 +1569,11 @@ def path_safety_check(self, paths:Union[str, List], *more_paths:Union[str, List] for path in paths: if path.startswith(('../', '..\\', '/../', '\\..\\')) or '/../' in path or '\\..\\' in path or path.endswith(('/..', '\\..')): return False - + return True - + def translate_path(self, path): """Translate a /-separated PATH to the local filename syntax. @@ -2163,7 +2163,7 @@ def _get_details(): -def _log_details(force): +def _log_details(force): data = _get_details() port = data['port'] # same as config.port @@ -2186,8 +2186,8 @@ def _log_details(force): f"Serving HTTP on port {port} \n", f"Server is probably running on\n", (f"[over NETWORK] {network_address}\n" if on_network else ""), - f"[on DEVICE] http://localhost:{port} & http://{local_ip}:{port}", - + f"[on DEVICE] http://localhost:{port} & http://{local_ip}:{port}", + style="star", sep="" ) ) @@ -2316,7 +2316,7 @@ def stop(self): def runner(port=0, directory="", bind="", arg_parse=True, handler=SimpleHTTPRequestHandler, force_log_server_details=False) -> EasyServerRunner: - + EasyServer = EasyServerRunner( port=port, directory=directory, diff --git a/dev_src/pyrobox_ServerHost.py b/dev_src/pyrobox_ServerHost.py index 57c9c2e..fb2bd93 100644 --- a/dev_src/pyrobox_ServerHost.py +++ b/dev_src/pyrobox_ServerHost.py @@ -3,7 +3,7 @@ import os from typing import Union -from _fs_utils import get_titles, dir_navigator +from _fs_utils import get_titles, dir_navigator, reverse_humanbytes from tools import xpath from pyroDB import PickleTable import user_mgmt as u_mgmt @@ -42,12 +42,12 @@ def __init__(self, cli_args:Union[dict, argparse.Namespace]): self.init_account() # Max size a zip file will be made - self.max_zip_size = 6*1024*1024*1024 # 6GB + self.max_zip_size = reverse_humanbytes(cli_args.zip_limit) # 6GB by default # Max Buffer size for writing files self.max_buffer_size = 1024*1024 # 1MB - + self.temp_dir = CoreConfig.temp_dir self.subtitles_dir = xpath(self.temp_dir, "subtitles") self.allow_subtitle = True diff --git a/dev_src/server.py b/dev_src/server.py index f4614a7..c987987 100644 --- a/dev_src/server.py +++ b/dev_src/server.py @@ -36,7 +36,7 @@ -__version__ = pyroboxCore_version +__version__ = '0.9.7' true = T = True false = F = False enc = "utf-8" @@ -636,7 +636,7 @@ def get_zip_id(self: SH, *args, **kwargs): status = True except LimitExceed: - message = 'Directory size limit exceed' + message = f"DIRECTORY SIZE LIMIT EXCEED [CURRENT LIMIT: {humanbytes(Sconfig.max_zip_size)}]" except Exception: self.log_error(traceback.format_exc()) diff --git a/dev_src/tools.py b/dev_src/tools.py index 4676369..c39472b 100644 --- a/dev_src/tools.py +++ b/dev_src/tools.py @@ -5,7 +5,7 @@ import subprocess import sys from typing import Generator, List, Union - +import mimetypes def get_codec(): # return "libx264" @@ -121,7 +121,7 @@ def os_scan_walk_gen(*path, allow_dir=False) -> Generator[os.DirEntry, None, Non def os_scan_walk(*path, allow_dir=False) -> List[os.DirEntry]: """ Iterate through a directory and its subdirectories - and `yield` the filePath object of each Files and Folders*. + and return a list of the filePath object of each Files and Folders*. path: path to the directory allow_dir: if True, will also yield directories @@ -134,12 +134,16 @@ def os_scan_walk(*path, allow_dir=False) -> List[os.DirEntry]: return files - def is_file(*path): return os.path.isfile(xpath(*path)) +def is_filetype(*path, ext_type): + path = xpath(*path) + mime = mimetypes.guess_type(path)[0] + return is_file(path) and mime and mime.split('/')[0] == ext_type + class Text_Box: def __init__(self): self.styles = { diff --git a/setup.cfg b/setup.cfg index d563d6b..04d09ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyrobox -version = 0.9.6 +version = 0.9.7 author = Rasan author_email= wwwqweasd147@gmail.com description = Personal DropBox for Private Network @@ -15,7 +15,7 @@ project_urls = Release Notes = https://github.com/RaSan147/pyrobox/releases -classifiers = +classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 diff --git a/setup.py b/setup.py index 70b7bd5..afcd67c 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,12 @@ "msgpack", "tabulate2", "pyqrcode", + "requests", ], entry_points=''' [console_scripts] pyrobox=pyrobox:server.run ''', - + ) \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index be55a41..7e8af6f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,5 @@ __all__ = ["server", "pyroboxCore", "clone"] -from . import server -from . import pyroboxCore -from . import clone +# from . import server +# from . import pyroboxCore +# from . import clone diff --git a/src/__main__.py b/src/__main__.py index 5caa0ab..3f75309 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,2 +1,5 @@ from . import server -from . import clone +# from . import clone + +if __name__ == "__main__": + server.run() diff --git a/src/_arg_parser.py b/src/_arg_parser.py index d2cfa0d..01ed0a9 100644 --- a/src/_arg_parser.py +++ b/src/_arg_parser.py @@ -1,14 +1,16 @@ # add additional arguments to the parser - +config = None # the config must be imported from pyroboxCore -# from pyroboxCore import config +if __name__ == "__main__": + from pyroboxCore import config + -def main(config): +def main(config=config): config.parser.add_argument('--password', '-k', default=config.PASSWORD, type=str, help='[Value] Upload Password (default: %(default)s)') - + config.parser.add_argument('--qr', '-q', @@ -90,4 +92,7 @@ def main(config): default=False, help="[Flag] Only allowed to see file list, nothing else (default: %(default)s)") - + config.parser.add_argument('--zip-limit', '-zl', + default="6GB", + type=str, + help='[Value] Max size of zip file allowed to download (default: %(default)s) [can be bytes without suffix or suffixes like 1KB, 1MB, 1GB, 1TB]') diff --git a/src/_fs_utils.py b/src/_fs_utils.py index b4f94da..c427efd 100644 --- a/src/_fs_utils.py +++ b/src/_fs_utils.py @@ -22,7 +22,7 @@ import time import traceback import urllib.parse -from .tools import os_scan_walk, os_scan_walk_gen, xpath +from .tools import os_scan_walk_gen, xpath from ._exceptions import LimitExceed @@ -281,6 +281,35 @@ def humanbytes(B: int): return ret +def reverse_humanbytes(human_str:str): + """ + Converts human readable size to bytes + """ + human_str = human_str.strip().lower() + + if human_str.endswith('bytes'): + return int(human_str[:-5].strip()) + if human_str.endswith('byte'): + return int(human_str[:-4].strip()) + + if human_str.endswith('b'): + human_str = human_str[:-1].strip() + + if human_str.endswith('k'): + return int(float(human_str[:-1].strip()) * 1024) + + if human_str.endswith('m'): + return int(float(human_str[:-1].strip()) * 1024**2) + + if human_str.endswith('g'): + return int(float(human_str[:-1].strip()) * 1024**3) + + if human_str.endswith('t'): + return int(float(human_str[:-1].strip()) * 1024**4) + + return int(human_str) + + def get_dir_m_time(path): """ Get the last modified time of a directory and all its subdirectories. @@ -492,7 +521,7 @@ def _start(self, server:"ServerHost"): self.err(f"Failed to upload {os_fn}") break - + else: self.waited += 1 self.sleep() @@ -503,7 +532,7 @@ def _start(self, server:"ServerHost"): def kill(self): self.active = False self.done = True - + for f in tuple(self.serial_io.queue): name = f[0].name if not f[0].closed: diff --git a/src/_sub_extractor.py b/src/_sub_extractor.py index 8d63b30..175ae5a 100644 --- a/src/_sub_extractor.py +++ b/src/_sub_extractor.py @@ -19,14 +19,14 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None """ output_paths = [] sub_names = [] - + if not os.path.isfile(input_file): raise FileNotFoundError(f"The file '{input_file}' does not exist.") if not FFMPEG: # we don't want to raise an exception here, just return an empty list return [] - + try: # Run ffmpeg to analyze the file process = subprocess.run( @@ -36,7 +36,7 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None text=True ) output = process.stderr # ffmpeg logs info in stderr - + # Extract stream information (Subtitle streams) # Example: Stream #0:1(eng): Subtitle: dvd_subtitle stream_pattern = re.compile( @@ -52,7 +52,7 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None sub_name = f"{sub_name}_{n}" else: sub_name = f"subtitle_{n}" - + sub_names.append(sub_name) n += 1 @@ -77,16 +77,16 @@ def extract_subtitles_from_file(input_file, output_format="vtt", output_dir=None os.makedirs(output_dir, exist_ok=True) # Create the output directory if it doesn't exist output_filename = f"{os.path.splitext(input_file)[0]}_{sub_name}.{output_format}" - + # Generate output path output_paths.append((sub_name, output_filename)) - + # Use ffmpeg to extract the audio stream subprocess.run( [FFMPEG, "-i", input_file, "-map", stream_index, '-y', output_filename], check=True ) - + return output_paths except subprocess.CalledProcessError as e: diff --git a/src/_zipfly_manager.py b/src/_zipfly_manager.py index 7ce0393..d4bb7ff 100644 --- a/src/_zipfly_manager.py +++ b/src/_zipfly_manager.py @@ -19,7 +19,7 @@ -from ._fs_utils import get_dir_m_time, _get_tree_path_n_size +from ._fs_utils import get_dir_m_time, _get_tree_path_n_size, humanbytes from ._exceptions import LimitExceed @@ -535,7 +535,7 @@ def err(msg): try: fs = _get_tree_path_n_size(path, must_read=True, path_type="both", limit=self.size_limit, add_dirs=True) except LimitExceed as e: - return err("DIRECTORY SIZE LIMIT EXCEED") + return err(f"DIRECTORY SIZE LIMIT EXCEED [CURRENT LIMIT: {humanbytes(self.size_limit)}]") source_size = sum(i[1] for i in fs) fm = [i[0] for i in fs] source_m_time = get_dir_m_time(path) diff --git a/src/clone.py b/src/clone.py index 6a9fd7f..34a07bf 100644 --- a/src/clone.py +++ b/src/clone.py @@ -69,7 +69,7 @@ def check_exist(url, path, check_method): if local_last_modify == original_modify and check_method == "date": return True - + if local_last_modify >= original_modify and check_method == "date+": return True @@ -126,7 +126,7 @@ def dl(url, path, overwrite, check_method): CANCEL = True os.remove(local_filename) return False - + except Exception: traceback.print_exc() print("ALERT: [dl] Server is probably down") @@ -159,7 +159,7 @@ def clone(self, url, path = "./", overwrite = False, check_exist = "date", delet """ if url[-1] != "/": url += "/" - + Q = Queue() def get_json(url): @@ -178,7 +178,7 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr if path[-1] != "/": path += "/" - + os.makedirs(path, exist_ok=True) # make sure the directory exists even if it's empty json = get_json(url) @@ -236,7 +236,7 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr -if __name__ == "__main__": +def main(): url = input("URL: ") path = input("Save to: ") @@ -248,7 +248,11 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr check_exist = "date+" o = None while not o: - o = input("Check exist? (date/date+/size) [Default: date+]: ") + o = input("""Check exist? [Default: date+] +date: download if remote file is update time same as local file +date+: download if remote file is newer or same as local file (ignored if local file is newer) +size: download if remote file size is different from local file +>>> """) if o in ["date", "date+", "size"]: check_exist = o else: @@ -271,3 +275,5 @@ def run_Q(url, path = "./", overwrite = False, check_exist = "date", delete_extr for future in as_completed(cloner.futures): bool(future.result()) +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/pyroDB.py b/src/pyroDB.py index 91e5197..59d885c 100644 --- a/src/pyroDB.py +++ b/src/pyroDB.py @@ -674,7 +674,7 @@ def unlink(self): def delete_file(self): """ - Delete the file from the disk + Delete the file from the disk """ self._pk.delete_file() @@ -712,11 +712,11 @@ def __str__(self, limit:int=None): x += "\n" x += "\t|\t".join(str(self.row(i).values())) - else: + else: x = tabulate( - # self[:min(self.height, limit)], + # self[:min(self.height, limit)], self.rows(start=0, end=min(self.height, limit)), - headers="keys", + headers="keys", tablefmt= "simple_grid", #"orgtbl", maxcolwidths=60 @@ -783,7 +783,7 @@ def column(self, name, rescan=True) -> list: """ self.rescan(rescan=rescan) return self._pk.db[name].copy() - + def get_column(self, name) -> list: """ Return the list pointer to the column (unsafe) @@ -1018,7 +1018,7 @@ def search_iter(self, kw, column=None , row=None, full_match=False, return_obj=T - return_obj: return cell object instead of value (default: `True`) - return: cell object - ie: + ie: ```python for cell in db.search_iter("abc"): print(cell.value) @@ -1099,7 +1099,7 @@ def search(self, kw, column=None , row=None, full_match=False, return_obj=True, - return_row: return row object instead of cell object (default: `False`) - return: cell object - ie: + ie: ```python for cell in db.search("abc"): print(cell.value) @@ -1142,7 +1142,7 @@ def find_1st(self, kw, column=None , row=None, full_match=False, return_obj=True for cell in self.search_iter(kw, column=column , row=row, full_match=full_match, return_obj=return_obj, rescan=rescan): return cell - + def find_1st_row(self, kw, column=None , row=None, full_match=False, return_obj=True, rescan=True) -> Union["_PickleTRow", None]: """ search a keyword in a cell/row/column/entire sheet and return the 1st matched row object @@ -1335,7 +1335,7 @@ def copy(self, location=None, auto_dump=True, sig=True) -> "PickleTable": - sig: Add signal handler for graceful shutdown (default: `True`) - return: new PickleTable object """ - + new = PickleTable(location, auto_dump=auto_dump, sig=sig) new.add_column(*self.column_names) new._pk.db = datacopy.deepcopy(self.__db__()) @@ -1633,7 +1633,7 @@ def load_csv(self, filename, header=True, ignore_none=False, ignore_new_headers= """ Load a csv file to the table - WILL OVERWRITE THE EXISTING DATA (To append, make a new table and extend) - - header: + - header: * if True, the first row will be considered as column names * if False, the columns will be named as "Unnamed-1", "Unnamed-2", ... * if "auto", the columns will be named as "A", "B", "C", ..., "Z", "AA", "AB", ... @@ -1654,7 +1654,7 @@ def add_row(row, columns): new_row = {k: v for k, v in zip(columns, row)} # print(new_row) - + self.add_row(new_row, AD=False) @@ -1712,7 +1712,7 @@ def add_row(row, columns): self.add_column(new_columns, exist_ok=True, AD=False, rescan=False) columns = new_columns - + add_row(row, columns) else: # count the columns, and name them as "Unnamed-1", "Unnamed-2", ... @@ -1742,7 +1742,7 @@ def add_row(row, columns): self.auto_dump(AD=AD) - + def extend(self, other: "PickleTable", add_extra_columns=None, AD=True): """ Extend the table with another table @@ -1762,7 +1762,7 @@ def extend(self, other: "PickleTable", add_extra_columns=None, AD=True): keys = other.column_names this_keys = self.column_names - + if add_extra_columns: self.add_column(*keys, exist_ok=True, AD=False) else: @@ -1820,7 +1820,7 @@ def add(self, table:Union["PickleTable", dict], add_extra_columns=None, AD=True) else: self.extend(table, ) - + self.auto_dump(AD=AD) @@ -1911,7 +1911,7 @@ def row_index(self): @property def row(self): """ - returns a COPY row dict of the cell + returns a COPY row dict of the cell """ return self.source.row_by_id(self.id) @@ -1949,7 +1949,7 @@ def __init__(self, source:PickleTable, uid, CC): self.id = uid self.CC = CC self.deleted = False - + def is_deleted(self): """ return True if the row is deleted, else False @@ -2028,7 +2028,7 @@ def del_item(self, name, AD=True): self.source.raise_source(self.CC) self.source.set_cell_by_id(name, self.id, None, AD=AD) - + def __delitem__(self, name): # Auto dump @@ -2126,7 +2126,7 @@ def __eq__(self, other): def __ne__(self, other): self.raise_deleted() return not self.__eq__(other) - + class _PickleTColumn(list): @@ -2345,7 +2345,7 @@ def __str__(self): def __repr__(self): self.raise_deleted() - + return repr(self.source.get_column(self.name)) def del_column(self): @@ -2559,7 +2559,7 @@ def test(): except Exception as e: print("❌ TEST [ADD] (Raise Exception Invalid type): Failed") raise e - + try: tb.add({"x":[1,2,3], "Y":[4,5,6,7], "Z":[1,2,3]}) @@ -2571,7 +2571,7 @@ def test(): print("❌ TEST [ADD dict] (Raise Exception extra column) (F): Failed") raise e - + try: tb.add({"x":[1,2,3], "Y":[4,5,6,7]}, add_extra_columns=True) print("✅ TEST [ADD dict] (Add extra column): Passed") @@ -2604,21 +2604,21 @@ def _update_table(tb:PickleTable): print("\n\n📝 Sort test 1 (sort by x)") - + _update_table(tb) tb.sort("x") if tb.column("x") != [1, 3, 5, 7]: raise Exception("❌ TEST [SORT] (sort by x): Failed") - + print("✅ TEST [SORT] (sort by x): Passed") print("="*50) print("\n\n📝 Sort test 2 (sort by Y reverse)") _update_table(tb) - + tb.sort("Y", reverse=True) if tb.column("Y") != [8, 6, 4, 2]: @@ -2636,7 +2636,7 @@ def _update_table(tb:PickleTable): tb.add_column("x+Y", exist_ok=True) - + print("📝 APPLYING COLUMN FUNCTION with row_func=True") try: tb["x+Y"].apply(func=lambda x: x["x"]+x["Y"], row_func=True) @@ -2649,7 +2649,7 @@ def _update_table(tb:PickleTable): raise Exception("❌ TEST [SORT] (sort by key function) {x+y}: Failed") print("✅ TEST [SORT] (sort by key function) {x+y}: Passed") - + print("="*50) print("\n\n📝 Sort test 4 (sort by key function) {x+y} (copy)") @@ -2685,7 +2685,7 @@ def _update_table(tb:PickleTable): print("="*50) - + print("\n\n📝 Remove duplicates test [selective column]") tb.clear() @@ -2794,7 +2794,7 @@ def _update_table(tb:PickleTable): raise e - + print("\t📝 NoFile Load ['warn']") # 2nd error mode on file not found try: @@ -2811,7 +2811,7 @@ def _update_table(tb:PickleTable): print("\t❌ TEST [LOAD CSV][NoFile Load][on_file_not_found='warn']: Failed (Other error)") raise e - + print("\t📝 NoFile Load ['no_warning']") # 4th error mode on file not found try: diff --git a/src/pyroboxCore.py b/src/pyroboxCore.py index 9f25655..b0ba711 100644 --- a/src/pyroboxCore.py +++ b/src/pyroboxCore.py @@ -515,7 +515,7 @@ def allowed_CORS(self, method) -> Union[str, None]: @classmethod def allow_CORS(self, method, origin): """Add a method to the allowed list - + `method` = `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH`\n `origin` = `*`, `http://example.com`, `https://example.com`, `http://example.com:8080`, `https://example.com:8080` """ @@ -895,7 +895,7 @@ def send_header(self, keyword, value): def end_headers(self): """Send the blank line ending the MIME headers.""" - + CORS_POLICY = self.allowed_CORS(self.method) if self.allowed_CORS(self.method): self.send_header('Access-Control-Allow-Origin', CORS_POLICY) @@ -1165,7 +1165,7 @@ def alt_directory(dir, method='', url='', hasQ=(), QV={}, fragent='', url_regex= alternative directory handler (only handles GET and HEAD request for files) """ self = __class__ - + @self.on_req(method=method, url=url, hasQ=hasQ, QV=QV, fragent=fragent, url_regex=url_regex) def alt_dir_function(self: Type[__class__], *args, **kwargs): """ @@ -1173,7 +1173,7 @@ def alt_dir_function(self: Type[__class__], *args, **kwargs): """ file = self.url_path.split("/")[-1] - + if not os.path.exists(tools.xpath(dir, file)): self.send_error(HTTPStatus.NOT_FOUND, "File not found") return None @@ -1430,7 +1430,7 @@ def return_file(self, path, filename=None, download=False, cache_control="", coo if cache_control: self.send_header("Cache-Control", cache_control) - + self.send_header('Accept-Ranges', 'bytes') @@ -1479,7 +1479,7 @@ def return_file(self, path, filename=None, download=False, cache_control="", coo def send_file(self, path, filename=None, download=False, cache_control='', cookie:Union[SimpleCookie, str]=None): '''sends the head and file to client''' file = self.return_file(path, filename, download, cache_control, cookie=cookie) - if not file: + if not file: return # already flushed (with error/unchanged) if self.command == "HEAD": return # to avoid sending file on get request @@ -1545,11 +1545,11 @@ def get_displaypath(self, url_path): def get_rel_path(self, filename): """Return the relative path to the file, FOR WEB.""" return urllib.parse.unquote(posixpath.join(self.url_path, filename), errors='surrogatepass') - + def get_web_path(self, path:str, times=1): """replace current directory with /""" return path.replace(self.directory, "/", times) - + def path_safety_check(self, paths:Union[str, List], *more_paths:Union[str, List]): """check if path is safe paths: list of paths to check""" @@ -1569,11 +1569,11 @@ def path_safety_check(self, paths:Union[str, List], *more_paths:Union[str, List] for path in paths: if path.startswith(('../', '..\\', '/../', '\\..\\')) or '/../' in path or '\\..\\' in path or path.endswith(('/..', '\\..')): return False - + return True - + def translate_path(self, path): """Translate a /-separated PATH to the local filename syntax. @@ -2163,7 +2163,7 @@ def _get_details(): -def _log_details(force): +def _log_details(force): data = _get_details() port = data['port'] # same as config.port @@ -2186,8 +2186,8 @@ def _log_details(force): f"Serving HTTP on port {port} \n", f"Server is probably running on\n", (f"[over NETWORK] {network_address}\n" if on_network else ""), - f"[on DEVICE] http://localhost:{port} & http://{local_ip}:{port}", - + f"[on DEVICE] http://localhost:{port} & http://{local_ip}:{port}", + style="star", sep="" ) ) @@ -2316,7 +2316,7 @@ def stop(self): def runner(port=0, directory="", bind="", arg_parse=True, handler=SimpleHTTPRequestHandler, force_log_server_details=False) -> EasyServerRunner: - + EasyServer = EasyServerRunner( port=port, directory=directory, diff --git a/src/pyrobox_ServerHost.py b/src/pyrobox_ServerHost.py index ede1c4b..701adc5 100644 --- a/src/pyrobox_ServerHost.py +++ b/src/pyrobox_ServerHost.py @@ -3,9 +3,9 @@ import os from typing import Union -from ._fs_utils import get_titles, dir_navigator -from .pyroDB import PickleTable +from ._fs_utils import get_titles, dir_navigator, reverse_humanbytes from .tools import xpath +from .pyroDB import PickleTable from . import user_mgmt as u_mgmt from .user_mgmt import User @@ -42,12 +42,12 @@ def __init__(self, cli_args:Union[dict, argparse.Namespace]): self.init_account() # Max size a zip file will be made - self.max_zip_size = 6*1024*1024*1024 # 6GB + self.max_zip_size = reverse_humanbytes(cli_args.zip_limit) # 6GB by default # Max Buffer size for writing files self.max_buffer_size = 1024*1024 # 1MB - + self.temp_dir = CoreConfig.temp_dir self.subtitles_dir = xpath(self.temp_dir, "subtitles") self.allow_subtitle = True diff --git a/src/server.py b/src/server.py index 97a2b6d..2ac0996 100644 --- a/src/server.py +++ b/src/server.py @@ -36,7 +36,7 @@ -__version__ = pyroboxCore_version +__version__ = '0.9.7' true = T = True false = F = False enc = "utf-8" @@ -615,14 +615,14 @@ def get_zip_id(self: SH, *args, **kwargs): if not user.ZIP: return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) - + if not (user.DOWNLOAD and user.VIEW): return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) if CoreConfig.disabled_func["zip"]: return self.return_txt("ERROR: ZIP FEATURE IS UNAVAILABLE !", HTTPStatus.INTERNAL_SERVER_ERROR, cookie=cookie) - - + + os_path = kwargs.get('path', '') spathsplit = kwargs.get('spathsplit', '') filename = spathsplit[-2] + ".zip" @@ -630,14 +630,14 @@ def get_zip_id(self: SH, *args, **kwargs): zid = None status = False message = '' - + try: zid = zip_manager.get_id(os_path) status = True except LimitExceed: - message = 'Directory size limit exceed' - + message = f"DIRECTORY SIZE LIMIT EXCEED [CURRENT LIMIT: {humanbytes(Sconfig.max_zip_size)}]" + except Exception: self.log_error(traceback.format_exc()) message = 'Failed to create zip' @@ -663,7 +663,7 @@ def create_zip(self: SH, *args, **kwargs): if not user.ZIP: return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) - + if not (user.DOWNLOAD and user.VIEW): return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) @@ -691,7 +691,7 @@ def create_zip(self: SH, *args, **kwargs): data = pt.directory_explorer_header().safe_substitute(PY_PAGE_TITLE=title, PY_PUBLIC_URL=CoreConfig.address(), PY_DIR_TREE_NO_JS=dir_navigator(displaypath)) - + return self.return_txt(data, cookie=cookie) @@ -707,7 +707,7 @@ def get_zip(self: SH, *args, **kwargs): if not user.ZIP: return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) - + if not (user.DOWNLOAD and user.VIEW): return self.send_error(HTTPStatus.UNAUTHORIZED, "You are not authorized to perform this action", cookie=cookie) @@ -1290,7 +1290,7 @@ def remove_from_temp(temp_fn): rltv_path = xpath(url_path, fn) # relative path (must be url path with / separator) - + if not self.path_safety_check(fn, rltv_path): return self.send_txt("Invalid Path: " + rltv_path, HTTPStatus.BAD_REQUEST, cookie=cookie) @@ -1390,10 +1390,10 @@ def del_2_recycle(self: SH, *args, **kwargs): rel_path = self.get_rel_path(filename) - + if not self.path_safety_check(filename, rel_path): return self.send_json({"head": "Failed", "body": "Invalid Path: " + rel_path}, cookie=cookie) - + os_f_path = self.translate_path(xpath(url_path, filename)) self.log_warning(f'<-send2trash-> {os_f_path} by {[uid]}') @@ -1772,7 +1772,7 @@ def run(*args, **kwargs): print(url.terminal('black', 'white', quiet_zone=1)) except Exception as e: logger.error(f"Error generating QR code: {e}") - + runner.run() diff --git a/src/tools.py b/src/tools.py index 4676369..72c8614 100644 --- a/src/tools.py +++ b/src/tools.py @@ -5,7 +5,7 @@ import subprocess import sys from typing import Generator, List, Union - +import mimetypes def get_codec(): # return "libx264" @@ -81,7 +81,7 @@ def xpath(*path: Union[str, bytes], realpath=False, posix=True, win=False): def EXT(path): """ Returns the extension of the file - + ie. EXT("file.txt") -> "txt" """ return path.rsplit('.', 1)[-1] @@ -121,7 +121,7 @@ def os_scan_walk_gen(*path, allow_dir=False) -> Generator[os.DirEntry, None, Non def os_scan_walk(*path, allow_dir=False) -> List[os.DirEntry]: """ Iterate through a directory and its subdirectories - and `yield` the filePath object of each Files and Folders*. + and return a list of the filePath object of each Files and Folders*. path: path to the directory allow_dir: if True, will also yield directories @@ -134,12 +134,16 @@ def os_scan_walk(*path, allow_dir=False) -> List[os.DirEntry]: return files - def is_file(*path): return os.path.isfile(xpath(*path)) - - + + +def is_filetype(*path, ext_type): + path = xpath(*path) + mime = mimetypes.guess_type(path)[0] + return is_file(path) and mime and mime.split('/')[0] == ext_type + class Text_Box: def __init__(self): self.styles = { @@ -210,9 +214,9 @@ def str_comma(x): x = float(x) x = round(x, 2) - + return ("{:.2f}".format(x)).replace('.', ',') - + def str_comma_to_float(x): try: