Source code for ooodev.utils.file_io

# coding: utf-8
# Python conversion of FileIO.java by Andrew Davison, ad@fivedots.coe.psu.ac.th
# See Also: https://fivedots.coe.psu.ac.th/~ad/jlop/
# region Imports
from __future__ import annotations
import os
import re
import tempfile
import datetime
import zipfile
from pathlib import Path
from typing import Any, Generator, List, TYPE_CHECKING, overload

import uno
from com.sun.star.io import XActiveDataSink
from com.sun.star.io import XTextInputStream
from com.sun.star.packages.zip import XZipFileAccess
from com.sun.star.uno import Exception as UnoException
from com.sun.star.uno import RuntimeException as UnoRuntimeException

from ooo.dyn.util.the_macro_expander import theMacroExpander

from ooodev.loader import lo as mLo
from ooodev.exceptions import ex as mEx


if TYPE_CHECKING:
    from com.sun.star.container import XNameAccess
    from com.sun.star.io import XInputStream
    from ooodev.utils.type_var import PathOrStr
    from ooodev.utils.type_var import Table
else:
    PathOrStr = Any
    Table = Any

# if sys.version_info >= (3, 10):
#     from typing import Union
# else:
#     from typing_extensions import Union
# endregion imports

_UTIL_PATH = str(Path(__file__).parent)
_URI_FILE_RE = r"^(file:(?:/*))"


[docs]class FileIO: # region ------------- file path methods ---------------------------
[docs] @staticmethod def get_utils_folder() -> str: """ Gets path to utils folder Returns: str: folder path as str """ return _UTIL_PATH
[docs] @staticmethod def get_absolute_path(fnm: PathOrStr) -> Path: """ Gets Absolute path Args: fnm (PathOrStr): path as string Returns: Path: absolute path """ # windows path has no resolve method p = Path(fnm) return p if p.is_absolute() else p.absolute().resolve()
[docs] @staticmethod def get_ext(fnm: PathOrStr) -> str | None: """ Gets file extension without the ``.`` Args: fnm (PathOrStr): file path Raises: ValueError: If fnm is empty string Returns: str | None: Extension if Found; Otherwise, None """ if fnm == "": raise ValueError("Empty string") p = Path(fnm) # if not p.is_file(): # mLo.Lo.print(f"Not a file: {fnm}") # return None if not p.suffix: mLo.Lo.print(f"No extension found for '{fnm}'") return None return p.suffix[1:]
[docs] @classmethod def url_to_path(cls, url: str) -> Path: """ Converts url to path Args: url (str): url to convert Raises: Exception: If unable to parse url. Returns: Path: path as string """ # sourcery skip: raise-from-previous-error, raise-specific-error try: return cls.uri_to_path(uri_fnm=url) except Exception: raise Exception(f"Could not parse '{url}'")
[docs] @classmethod def fnm_to_url(cls, fnm: PathOrStr) -> str: """ Converts file path to url Args: fnm (PathOrStr): file path Raises: Exception: If unable to get url form fnm. Returns: str: Converted path. """ # sourcery skip: raise-specific-error try: p = cls.get_absolute_path(fnm) return p.as_uri() except Exception as e: raise Exception("Unable to convert '{fnm}'") from e
[docs] @staticmethod def uri_absolute(uri: str) -> str: """ Ensures a string ``uri`` is absolute. Args: uri (str): _description_ Returns: str: Absolute uri """ # converts # file:///C:/Program%20Files/LibreOffice/program/../share/gallery/sounds/apert2.wav # to # file:///C:/Program%20Files/LibreOffice/share/gallery/sounds/apert2.wav # https://tinyurl.com/2zryl94m result = os.path.normpath(uri) result = result.replace("\\", "/") # result may now start with file:/ and not file:/// # add proper file:/// again result = re.sub(pattern=_URI_FILE_RE, repl="file:///", string=result, count=1) return result
# region uri_to_path() @overload @classmethod def uri_to_path(cls, uri_fnm: PathOrStr) -> Path: ... @overload @classmethod def uri_to_path(cls, uri_fnm: PathOrStr, ensure_absolute: bool) -> Path: ...
[docs] @classmethod def uri_to_path(cls, uri_fnm: PathOrStr, ensure_absolute: bool = True) -> Path: """ Converts uri file to path. Args: uri_fnm (PathOrStr): URI to convert ensure_absolute (bool): If ``True`` then ensures that the return path is absolute. Default is ``True`` Raises: ConvertPathError: If unable to convert. Returns: Path: Converted URI as path. """ try: sys_path = uno.fileUrlToSystemPath(str(uri_fnm)) except UnoRuntimeException as e: raise mEx.ConvertPathError(f'Couldn\'t convert file url to a system path: uri_fnm="{uri_fnm}"') from e p = Path(sys_path) return cls.get_absolute_path(p) if ensure_absolute else p
# endregion uri_to_path()
[docs] @classmethod def get_file_names(cls, dir: PathOrStr) -> List[str]: """ Gets a list of filenames in a folder Args: dir (PathOrStr): Folder path Returns: List[str]: List of files. See Also: :py:meth:`~.file_io.FileIO.get_file_paths` """ # pattern .* includes hidden files whereas * does not. return [str(f) for f in cls.get_file_paths(dir)]
[docs] @classmethod def get_file_paths(cls, dir: PathOrStr) -> Generator[Path, None, None]: """ Gets a generator of file paths in a folder Args: dir (PathOrStr): Folder path Yields: Generator[Path, None, None]: Generator of Path objects See Also: :py:meth:`~.file_io.FileIO.get_file_paths` """ # pattern .* includes hidden files whereas * does not. p = cls.get_absolute_path(dir) return p.glob("*.*")
[docs] @staticmethod def get_fnm(path: PathOrStr) -> str: """ Gets last part of a file or dir such as ``myfile.txt`` Args: path (PathOrStr): file path Returns: str: file name portion """ if path == "": mLo.Lo.print("path is an empty string") return "" try: p = Path(path) return p.name except Exception as e: mLo.Lo.print(f"Unable to get name for '{path}'") mLo.Lo.print(f" {e}") return ""
[docs] @staticmethod def expand_macro(fnm: str) -> str: """ Expands macros in a file path if needed Args: fnm (str): file path Returns: str: expanded path Example: .. code-block:: python >>> print(FileIO.expand_macro("vnd.sun.star.expand:$BUNDLED_EXTENSIONS/wiki-publisher/templates")) 'file:///usr/lib/libreoffice/share/extensions/wiki-publisher/templates' .. versionadded:: 0.11.14 """ if not fnm: return "" if not fnm.startswith("vnd.sun.star.expand:"): return fnm # drop the prefix fnm = fnm[20:] return theMacroExpander().expandMacros(fnm)
# endregion ---------- file path methods --------------------------- # region ------------- file creation / deletion --------------------
[docs] @classmethod def is_openable(cls, fnm: PathOrStr) -> bool: """ Gets if a file can be opened Args: fnm (PathOrStr): file path Returns: bool: True if file can be opened; Otherwise, False """ try: p = cls.get_absolute_path(fnm) if not p.exists(): mLo.Lo.print(f"'{fnm}' does not exist") return False if not p.is_file(): mLo.Lo.print(f"'{fnm}' does is not a file") return False if not os.access(fnm, os.R_OK): mLo.Lo.print(f"'{fnm}' is not readable") return False return True except Exception as e: mLo.Lo.print(f"File is not openable: {e}") return False
[docs] @staticmethod def is_valid_path_or_str(fnm: PathOrStr) -> bool: """ Checks ``fnm`` it make sure it is a valid path or string. Args: fnm (PathOrStr): Input path Returns: bool: ``False`` if ``fnm`` is ``None`` or empty string; Otherwise; ``True`` """ # note when path is converted from empty string it becomes current dir such as PosixPath('.') return bool(fnm)
# region is_exist_file() @overload @classmethod def is_exist_file(cls, fnm: PathOrStr) -> bool: ... @overload @classmethod def is_exist_file(cls, fnm: PathOrStr, raise_err: bool) -> bool: ...
[docs] @classmethod def is_exist_file(cls, fnm: PathOrStr, raise_err: bool = False) -> bool: """ Gets is a file actually exist. Ensures that ``fnm`` is a valid ``PathOrStr`` format. Ensures that ``fnm`` is an existing file. Args: fnm (PathOrStr): File to check. Relative paths are accepted raise_err (bool, optional): Determines if an error is raised. Defaults to ``False``. Raises: ValueError: If ``raise_err`` is ``True`` and ``fnm`` is not a valid ``PathOrStr`` format. ValueError: If ``raise_err`` is ``True`` and ``fnm`` is not a file. FileNotFoundError: If ``raise_err`` is ``True`` and file is not found Returns: bool: ``True`` if file is valid; Otherwise, ``False``. """ if not cls.is_valid_path_or_str(fnm): if raise_err: raise ValueError(f'fnm is not a valid format for PathOrStr: "{fnm}"') else: return False p_fnm = cls.get_absolute_path(fnm) if not p_fnm.exists(): if raise_err: raise FileNotFoundError(f"File fnm does not exist: {p_fnm}") else: return False if not p_fnm.is_file(): if raise_err: raise ValueError(f'fnm is not a file: "{p_fnm}"') else: return False return True
# endregion is_exist_file() # region is_exist_dir() @overload @classmethod def is_exist_dir(cls, dnm: PathOrStr) -> bool: ... @overload @classmethod def is_exist_dir(cls, dnm: PathOrStr, raise_err: bool) -> bool: ...
[docs] @classmethod def is_exist_dir(cls, dnm: PathOrStr, raise_err: bool = False) -> bool: """ Gets is a directory actually exist. Ensures that ``dnm`` is a valid ``PathOrStr`` format. Ensures that ``dnm`` is an existing directory. Args: dnm (PathOrStr): directory to check. Relative paths are accepted raise_err (bool, optional): Determines if an error is raised. Defaults to ``False``. Raises: ValueError: If ``raise_err`` is ``True`` and ``dnm`` is not a valid ``PathOrStr`` format. FileNotFoundError: If ``raise_err`` is ``True`` and dir is not found. NotADirectoryError: If ``raise_err`` is ``True`` and ``dnm`` is not a directory. Returns: bool: ``True`` if file is valid; Otherwise, ``False``. """ if not cls.is_valid_path_or_str(dnm): if raise_err: raise ValueError(f'fnm is not a valid format for PathOrStr: "{dnm}"') else: return False p_fnm = cls.get_absolute_path(dnm) if not p_fnm.exists(): if raise_err: raise FileNotFoundError(f"Dir fnm does not exist: {p_fnm}") else: return False if not p_fnm.is_dir(): if raise_err: raise NotADirectoryError(f'fnm is not a directory: "{p_fnm}"') else: return False return True
# endregion is_exist_dir() # region make_directory() @overload @classmethod def make_directory(cls, dir: PathOrStr) -> Path: ... @overload @classmethod def make_directory(cls, fnm: PathOrStr) -> Path: ...
[docs] @classmethod def make_directory(cls, *args, **kwargs) -> Path: """ Creates path and sub paths they do not exist. Args: dir (PathOrStr): PathLike object to a directory fnm (PathOrStr): PathLike object to a directory or file. Returns: Path: Path of directory. Notes: If a file path is passed in then a directory is created for the file's parent. """ ordered_keys = (1,) kargs_len = len(kwargs) count = len(args) + kargs_len def get_kwargs() -> dict: ka = {} if kargs_len == 0: return ka valid_keys = ("dir", "fnm") check = all(key in valid_keys for key in kwargs) if not check: raise TypeError("make_directory() got an unexpected keyword argument") keys = ("doc", "slide") for key in keys: if key in kwargs: ka[1] = kwargs[key] break return ka if count != 1: raise TypeError("make_directory() got an invalid number of arguments") kargs = get_kwargs() for i, arg in enumerate(args): kargs[ordered_keys[i]] = arg # note that empty str converts to "." p = cls.get_absolute_path(kargs[1]) # test for suffix as p.is_file() will report false if the file does not exist. if p.suffix: p.parent.mkdir(parents=True, exist_ok=True) return p.parent else: p.mkdir(parents=True, exist_ok=True) return p
# endregion make_directory()
[docs] @staticmethod def create_temp_file(im_format: str) -> str: """ Creates a temporary file Args: im_format (str): File suffix such as txt or cfg Raises: ValueError: If ``im_format`` is empty. Exception: If creation of temp file fails. Returns: str: Path to temp file. Note: Temporary file is created in system temp directory. Caller of this method is responsible for deleting the file. """ # sourcery skip: raise-specific-error if not im_format: raise ValueError("im_format must not be empty value") try: # delete=False keeps the file after tmp.Close() tmp = tempfile.NamedTemporaryFile(prefix="loTemp", suffix=f".{im_format}", delete=False) name = tmp.name tmp.close() return name except Exception as e: raise Exception("Could not create temp file") from e
[docs] @classmethod def delete_file(cls, fnm: PathOrStr) -> bool: """ Deletes a file Args: fnm (PathOrStr): file to delete Returns: bool: True if delete is successful; Otherwise, False """ p = cls.get_absolute_path(fnm) os.remove(p) if p.exists(): mLo.Lo.print(f"'{p}' could not be deleted") return False else: mLo.Lo.print(f"'{p}' deleted") return True
[docs] @classmethod def delete_files(cls, *fnms: PathOrStr) -> bool: """ Deletes files Args: fnms (str): one or more files to delete Returns: bool: Returns True if all file are deleted; Otherwise, False """ if not fnms: return False mLo.Lo.print() result = True for s in fnms: result = result and cls.delete_file(s) return result
[docs] @classmethod def save_string(cls, fnm: PathOrStr, data: str) -> None: """ Writes string of data into given file. Args: fnm (PathOrStr): File to write string data (str): String to write in file Raises: Exception: If Error occurs Returns: None: """ # sourcery skip: raise-specific-error p = None if not data: mLo.Lo.print(f"No data to save in '{fnm}'") return try: p = cls.get_absolute_path(fnm) with open(p, "w") as file: file.write(data) mLo.Lo.print(f"Saved string to file: {p}") except Exception as e: if p is None: raise Exception("Could not save string to file") from e else: raise Exception(f"Could not save string to file: {p}") from e
[docs] @classmethod def save_bytes(cls, fnm: PathOrStr, b: bytes) -> None: """ Writes bytes data to a given file. Args: fnm (PathOrStr): File to write data b (bytes): data to write Raises: ValueError: If ``b`` is None Exception: If any other error occurs. """ # sourcery skip: raise-specific-error if b is None: raise ValueError(f"'b' is null. No data to save in '{fnm}'") try: p = cls.get_absolute_path(fnm) with open(p, "wb") as file: file.write(b) mLo.Lo.print(f"Saved bytes to file: {p}") except Exception as e: raise Exception(f"Could not save bytes to file: {fnm}") from e
[docs] @classmethod def save_array(cls, fnm: PathOrStr, arr: Table) -> None: """ Saves a 2d array to a file as tab delimited data. Args: fnm (PathOrStr): file to save data to arr (Table): 2d array of data. """ # sourcery skip: raise-specific-error if arr is None: raise ValueError("'arr' is null. No data to save in '{fnm}'") num_rows = len(arr) if num_rows == 0: mLo.Lo.print("No data to save in '{fnm}'") return try: p = cls.get_absolute_path(fnm) with open(p, "w") as file: for j in range(num_rows): line = "\t".join([str(v) for v in arr[j]]) file.write(line) file.write("\n") mLo.Lo.print(f"Save array to file: {p}") except Exception as e: raise Exception(f"Could not save array to file: {fnm}") from e
[docs] @classmethod def append_to(cls, fnm: PathOrStr, msg: str) -> None: """ Appends text to a file Args: fnm (PathOrStr): File to append text to. msg (str): Text to append. Raises: Exception: If unable to append text. """ # sourcery skip: raise-specific-error try: p = cls.get_absolute_path(fnm) with open(p, "a") as file: file.write(msg) file.write("\n") except Exception as e: raise Exception(f"unable to append to '{fnm}'") from e
# endregion ---------- file creation / deletion -------------------- # region ------------- zip access ----------------------------------
[docs] @classmethod def zip_access(cls, fnm: PathOrStr) -> XZipFileAccess: return mLo.Lo.create_instance_mcf( XZipFileAccess, "com.sun.star.packages.zip.ZipFileAccess", (cls.fnm_to_url(fnm),), raise_err=True )
[docs] @classmethod def zip_list_uno(cls, fnm: PathOrStr) -> None: """Use zip_list method""" # replaced by more detailed Java version; see below zfa = mLo.Lo.qi(XNameAccess, cls.zip_access(fnm), True) names = zfa.getElementNames() mLo.Lo.print(f"\nZipped Contents of '{fnm}'") mLo.Lo.print_names(names, 1)
[docs] @staticmethod def unzip_file(zfa: XZipFileAccess, fnm: PathOrStr) -> None: """ Unzip File. Not yet implemented Args: zfa (XZipFileAccess): Zip File Access fnm (PathOrStr): File path Raises: NotImplementedError: """ # TODO: implement unzip_file raise NotImplementedError
[docs] @staticmethod def read_lines(in_stream: XInputStream) -> List[str] | None: """ Converts a input stream to a list of strings. Args: in_stream (XInputStream): Input stream Returns: List[str] | None: If text was found in input stream the list of string; Otherwise, None """ lines = [] try: tis = mLo.Lo.create_instance_mcf(XTextInputStream, "com.sun.star.io.TextInputStream", raise_err=True) sink = mLo.Lo.qi(XActiveDataSink, tis, True) sink.setInputStream(in_stream) while tis.isEOF() is False: lines.append(tis.readLine()) tis.closeInput() except Exception as e: mLo.Lo.print(e) return lines or None
[docs] @classmethod def get_mime_type(cls, zfa: XZipFileAccess) -> str | None: """ Gets Mime-type for zip file access Args: zfa (XZipFileAccess): zip file access Raises: Exception: If error getting Mime-type Returns: str | None: Mime-type if found; Otherwise, None """ # sourcery skip: raise-specific-error try: in_stream = zfa.getStreamByPattern("mimetype") lines = cls.read_lines(in_stream) if lines is not None: return lines[0].strip() except UnoException as e: raise Exception("Unable to get mime type") from e mLo.Lo.print("No mimetype found") return None
# endregion ------------- zip access ------------------------------- # region ------------- switch to Python's zip APIs -----------------
[docs] @classmethod def zip_list(cls, fnm: PathOrStr) -> None: """ Prints info to console for a give zip file. Args: fnm (PathOrStr): Path to zip file. """ try: p = cls.get_absolute_path(fnm) with zipfile.ZipFile(p, "r") as zip: for file in zip.filelist: info = zip.getinfo(file.filename) mLo.Lo.print(info.filename) mLo.Lo.print("\tModified:\t" + str(datetime.datetime(*info.date_time))) mLo.Lo.print("\tSystem:\t\t" + str(info.create_system) + "(0 = Windows, 3 = Unix)") mLo.Lo.print("\tZIP version:\t" + str(info.create_version)) mLo.Lo.print("\tCompressed:\t" + str(info.compress_size) + " bytes") mLo.Lo.print("\tUncompressed:\t" + str(info.file_size) + " bytes") mLo.Lo.print() except Exception as e: mLo.Lo.print(e)
# endregion ---------- switch to Python's zip APIs -----------------