diff -r 9c1f429cb56b -r b47dfa7a137d src/eric7/SystemUtilities/FileSystemUtilities.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/SystemUtilities/FileSystemUtilities.py Sun Dec 18 19:33:46 2022 +0100 @@ -0,0 +1,612 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing file system related utility functions. +""" + +import contextlib +import ctypes +import fnmatch +import os +import pathlib +import subprocess + +from eric7.SystemUtilities import OSUtilities + + +def toNativeSeparators(path): + """ + Function returning a path, that is using native separator characters. + + @param path path to be converted + @type str + @return path with converted separator characters + @rtype str + """ + return str(pathlib.PurePath(path)) if bool(path) else "" + + +def fromNativeSeparators(path): + """ + Function returning a path, that is using "/" separator characters. + + @param path path to be converted + @type str + @return path with converted separator characters + @rtype str + """ + return pathlib.PurePath(path).as_posix() if bool(path) else "" + + +def normcasepath(path): + """ + Function returning a path, that is normalized with respect to its case + and references. + + @param path file path (string) + @return case normalized path (string) + """ + return os.path.normcase(os.path.normpath(path)) + + +def normcaseabspath(path): + """ + Function returning an absolute path, that is normalized with respect to + its case and references. + + @param path file path (string) + @return absolute, normalized path (string) + """ + return os.path.normcase(os.path.abspath(path)) + + +def normjoinpath(a, *p): + """ + Function returning a normalized path of the joined parts passed into it. + + @param a first path to be joined (string) + @param p variable number of path parts to be joined (string) + @return normalized path (string) + """ + return os.path.normpath(os.path.join(a, *p)) + + +def normabsjoinpath(a, *p): + """ + Function returning a normalized, absolute path of the joined parts passed + into it. + + @param a first path to be joined (string) + @param p variable number of path parts to be joind (string) + @return absolute, normalized path (string) + """ + return os.path.abspath(os.path.join(a, *p)) + + +def isinpath(file): + """ + Function to check for an executable file. + + @param file filename of the executable to check (string) + @return flag to indicate, if the executable file is accessible + via the searchpath defined by the PATH environment variable. + """ + if os.path.isabs(file): + return os.access(file, os.X_OK) + + if os.path.exists(os.path.join(os.curdir, file)): + return os.access(os.path.join(os.curdir, file), os.X_OK) + + path = OSUtilities.getEnvironmentEntry("PATH") + + # environment variable not defined + if path is None: + return False + + dirs = path.split(os.pathsep) + return any(os.access(os.path.join(directory, file), os.X_OK) for directory in dirs) + + +def startswithPath(path, start): + """ + Function to check, if a path starts with a given start path. + + @param path path to be checked + @type str + @param start start path + @type str + @return flag indicating that the path starts with the given start + path + @rtype bool + """ + return bool(start) and ( + path == start or normcasepath(path).startswith(normcasepath(start + "/")) + ) + + +def relativeUniversalPath(path, start): + """ + Function to convert a file path to a path relative to a start path + with universal separators. + + @param path file or directory name to convert (string) + @param start start path (string) + @return relative path or unchanged path, if path does not start with + the start path with universal separators (string) + """ + return fromNativeSeparators(os.path.relpath(path, start)) + + +def absolutePath(path, start): + """ + Public method to convert a path relative to a start path to an + absolute path. + + @param path file or directory name to convert (string) + @param start start path (string) + @return absolute path (string) + """ + if not os.path.isabs(path): + path = os.path.normpath(os.path.join(start, path)) + return path + + +def absoluteUniversalPath(path, start): + """ + Public method to convert a path relative to a start path with + universal separators to an absolute path. + + @param path file or directory name to convert (string) + @param start start path (string) + @return absolute path with native separators (string) + """ + if not os.path.isabs(path): + path = toNativeSeparators(os.path.normpath(os.path.join(start, path))) + return path + + +def getExecutablePath(file): + """ + Function to build the full path of an executable file from the environment. + + @param file filename of the executable to check (string) + @return full executable name, if the executable file is accessible + via the searchpath defined by the PATH environment variable, or an + empty string otherwise. + """ + if os.path.isabs(file): + if os.access(file, os.X_OK): + return file + else: + return "" + + cur_path = os.path.join(os.curdir, file) + if os.path.exists(cur_path) and os.access(cur_path, os.X_OK): + return cur_path + + path = os.getenv("PATH") + + # environment variable not defined + if path is None: + return "" + + dirs = path.split(os.pathsep) + for directory in dirs: + exe = os.path.join(directory, file) + if os.access(exe, os.X_OK): + return exe + + return "" + + +def getExecutablePaths(file): + """ + Function to build all full path of an executable file from the environment. + + @param file filename of the executable (string) + @return list of full executable names (list of strings), if the executable + file is accessible via the searchpath defined by the PATH environment + variable, or an empty list otherwise. + """ + paths = [] + + if os.path.isabs(file): + if os.access(file, os.X_OK): + return [file] + else: + return [] + + cur_path = os.path.join(os.curdir, file) + if os.path.exists(cur_path) and os.access(cur_path, os.X_OK): + paths.append(cur_path) + + path = os.getenv("PATH") + + # environment variable not defined + if path is not None: + dirs = path.split(os.pathsep) + for directory in dirs: + exe = os.path.join(directory, file) + if os.access(exe, os.X_OK) and exe not in paths: + paths.append(exe) + + return paths + + +def getWindowsExecutablePath(file): + """ + Function to build the full path of an executable file from the environment + on Windows platforms. + + First an executable with the extension .exe is searched for, thereafter + such with the extensions .cmd or .bat and finally the given file name as + is. The first match is returned. + + @param file filename of the executable to check (string) + @return full executable name, if the executable file is accessible + via the searchpath defined by the PATH environment variable, or an + empty string otherwise. + """ + if os.path.isabs(file): + if os.access(file, os.X_OK): + return file + else: + return "" + + filenames = [file + ".exe", file + ".cmd", file + ".bat", file] + + for filename in filenames: + cur_path = os.path.join(os.curdir, filename) + if os.path.exists(cur_path) and os.access(cur_path, os.X_OK): + return os.path.abspath(cur_path) + + path = os.getenv("PATH") + + # environment variable not defined + if path is None: + return "" + + dirs = path.split(os.pathsep) + for directory in dirs: + for filename in filenames: + exe = os.path.join(directory, filename) + if os.access(exe, os.X_OK): + return exe + + return "" + + +def isExecutable(exe): + """ + Function to check, if a file is executable. + + @param exe filename of the executable to check (string) + @return flag indicating executable status (boolean) + """ + return os.access(exe, os.X_OK) + + +def isDrive(path): + """ + Function to check, if a path is a Windows drive. + + @param path path name to be checked + @type str + @return flag indicating a Windows drive + @rtype bool + """ + isWindowsDrive = False + drive, directory = os.path.splitdrive(path) + if ( + drive + and len(drive) == 2 + and drive.endswith(":") + and directory in ["", "\\", "/"] + ): + isWindowsDrive = True + + return isWindowsDrive + + +def samepath(f1, f2): + """ + Function to compare two paths. + + @param f1 first path for the compare (string) + @param f2 second path for the compare (string) + @return flag indicating whether the two paths represent the + same path on disk. + """ + if f1 is None or f2 is None: + return False + + if normcaseabspath(os.path.realpath(f1)) == normcaseabspath(os.path.realpath(f2)): + return True + + return False + + +def samefilepath(f1, f2): + """ + Function to compare two paths. Strips the filename. + + @param f1 first filepath for the compare (string) + @param f2 second filepath for the compare (string) + @return flag indicating whether the two paths represent the + same path on disk. + """ + if f1 is None or f2 is None: + return False + + if normcaseabspath(os.path.dirname(os.path.realpath(f1))) == normcaseabspath( + os.path.dirname(os.path.realpath(f2)) + ): + return True + + return False + + +try: + EXTSEP = os.extsep +except AttributeError: + EXTSEP = "." + + +def splitPath(name): + """ + Function to split a pathname into a directory part and a file part. + + @param name path name (string) + @return a tuple of 2 strings (dirname, filename). + """ + if os.path.isdir(name): + dn = os.path.abspath(name) + fn = "." + else: + dn, fn = os.path.split(name) + return (dn, fn) + + +def joinext(prefix, ext): + """ + Function to join a file extension to a path. + + The leading "." of ext is replaced by a platform specific extension + separator if necessary. + + @param prefix the basepart of the filename (string) + @param ext the extension part (string) + @return the complete filename (string) + """ + if ext[0] != ".": + ext = ".{0}".format(ext) + # require leading separator to match os.path.splitext + return prefix + EXTSEP + ext[1:] + + +def compactPath(path, width, measure=len): + """ + Function to return a compacted path fitting inside the given width. + + @param path path to be compacted (string) + @param width width for the compacted path (integer) + @param measure reference to a function used to measure the length of the + string + @return compacted path (string) + """ + if measure(path) <= width: + return path + + ellipsis = "..." + + head, tail = os.path.split(path) + mid = len(head) // 2 + head1 = head[:mid] + head2 = head[mid:] + while head1: + # head1 is same size as head2 or one shorter + path = os.path.join("{0}{1}{2}".format(head1, ellipsis, head2), tail) + if measure(path) <= width: + return path + head1 = head1[:-1] + head2 = head2[1:] + path = os.path.join(ellipsis, tail) + if measure(path) <= width: + return path + while tail: + path = "{0}{1}".format(ellipsis, tail) + if measure(path) <= width: + return path + tail = tail[1:] + return "" + + +def direntries( + path, filesonly=False, pattern=None, followsymlinks=True, checkStop=None +): + """ + Function returning a list of all files and directories. + + @param path root of the tree to check + @type str + @param filesonly flag indicating that only files are wanted + @type bool + @param pattern a filename pattern or list of filename patterns to check + against + @type str or list of str + @param followsymlinks flag indicating whether symbolic links + should be followed + @type bool + @param checkStop function to be called to check for a stop + @type function + @return list of all files and directories in the tree rooted + at path. The names are expanded to start with path. + @rtype list of strs + """ + patterns = pattern if isinstance(pattern, list) else [pattern] + files = [] if filesonly else [path] + try: + entries = os.listdir(path) + for entry in entries: + if checkStop and checkStop(): + break + + if entry in [ + ".svn", + ".hg", + ".git", + ".ropeproject", + ".eric7project", + ".jedi", + ]: + continue + + fentry = os.path.join(path, entry) + if ( + pattern + and not os.path.isdir(fentry) + and not any(fnmatch.fnmatch(entry, p) for p in patterns) + ): + # entry doesn't fit the given pattern + continue + + if os.path.isdir(fentry): + if os.path.islink(fentry) and not followsymlinks: + continue + files += direntries( + fentry, filesonly, pattern, followsymlinks, checkStop + ) + else: + files.append(fentry) + except OSError: + pass + except UnicodeDecodeError: + pass + return files + + +def getDirs(path, excludeDirs): + """ + Function returning a list of all directories below path. + + @param path root of the tree to check + @param excludeDirs basename of directories to ignore + @return list of all directories found + """ + try: + names = os.listdir(path) + except OSError: + return [] + + dirs = [] + for name in names: + if os.path.isdir(os.path.join(path, name)) and not os.path.islink( + os.path.join(path, name) + ): + exclude = 0 + for e in excludeDirs: + if name.split(os.sep, 1)[0] == e: + exclude = 1 + break + if not exclude: + dirs.append(os.path.join(path, name)) + + for name in dirs[:]: + if not os.path.islink(name): + dirs += getDirs(name, excludeDirs) + + return dirs + + +def findVolume(volumeName, findAll=False): + """ + Function to find the directory belonging to a given volume name. + + @param volumeName name of the volume to search for + @type str + @param findAll flag indicating to get the directories for all volumes + starting with the given name (defaults to False) + @type bool (optional) + @return directory path or list of directory paths for the given volume + name + @rtype str or list of str + """ + volumeDirectories = [] + volumeDirectory = None + + if OSUtilities.isWindowsPlatform(): + # we are on a Windows platform + def getVolumeName(diskName): + """ + Local function to determine the volume of a disk or device. + + Each disk or external device connected to windows has an + attribute called "volume name". This function returns the + volume name for the given disk/device. + + Code from http://stackoverflow.com/a/12056414 + """ + volumeNameBuffer = ctypes.create_unicode_buffer(1024) + ctypes.windll.kernel32.GetVolumeInformationW( + ctypes.c_wchar_p(diskName), + volumeNameBuffer, + ctypes.sizeof(volumeNameBuffer), + None, + None, + None, + None, + 0, + ) + return volumeNameBuffer.value + + # + # In certain circumstances, volumes are allocated to USB + # storage devices which cause a Windows popup to raise if their + # volume contains no media. Wrapping the check in SetErrorMode + # with SEM_FAILCRITICALERRORS (1) prevents this popup. + # + oldMode = ctypes.windll.kernel32.SetErrorMode(1) + try: + for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": + dirpath = "{0}:\\".format(disk) + if os.path.exists(dirpath): + if findAll: + if getVolumeName(dirpath).startswith(volumeName): + volumeDirectories.append(dirpath) + else: + if getVolumeName(dirpath) == volumeName: + volumeDirectory = dirpath + break + finally: + ctypes.windll.kernel32.SetErrorMode(oldMode) + else: + # we are on a Linux or macOS platform + for mountCommand in ["mount", "/sbin/mount", "/usr/sbin/mount"]: + with contextlib.suppress(FileNotFoundError): + mountOutput = subprocess.run( # secok + mountCommand, check=True, capture_output=True, text=True + ).stdout.splitlines() + mountedVolumes = [ + x.split(" type")[0].split(maxsplit=2)[2] for x in mountOutput + ] + if findAll: + for volume in mountedVolumes: + if volumeName in volume: + volumeDirectories.append(volume) + if volumeDirectories: + break + else: + for volume in mountedVolumes: + if volume.endswith(volumeName): + volumeDirectory = volume + break + if volumeDirectory: + break + + if findAll: + return volumeDirectories + else: + return volumeDirectory