1 #!/usr/bin/env python 2 3 """ 4 Directory context functionality. 5 6 Copyright (C) 2018, 2019 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from os import makedirs, rename, walk 23 from os.path import abspath, commonprefix, exists, isdir, isfile, join 24 import fnmatch 25 26 # Get the directory with trailing path separator when assessing path prefixes 27 # in order to prevent sibling directory confusion. 28 29 def inside(filename, dirname): 30 31 "Return whether 'filename' is inside 'dirname'." 32 33 dirprefix = join(dirname, "") 34 return filename == dirname or commonprefix((filename, dirprefix)) == dirprefix 35 36 def within(filename, dirname): 37 38 "Return the part of 'filename' found within 'dirname'." 39 40 dirname = join(dirname, "") 41 prefix = commonprefix((filename, dirname)) 42 43 if prefix == dirname: 44 return filename[len(prefix):] 45 else: 46 return None 47 48 49 50 class Directory: 51 52 "A directory abstraction." 53 54 def __init__(self, filename): 55 56 "Initialise the abstraction with the given 'filename'." 57 58 self.filename = abspath(filename) 59 60 def get_filename(self, filename): 61 62 """ 63 Return the full path of a file with the given 'filename' found within 64 the directory. The full path is an absolute path. 65 """ 66 67 # Get the absolute path for the combination of directory and filename. 68 69 pathname = abspath(join(self.filename, filename)) 70 71 if inside(pathname, self.filename): 72 return pathname 73 else: 74 raise ValueError, filename 75 76 # File operations acting on relative filenames. 77 78 def _apply(self, fn, filename): 79 80 "Apply 'fn' to the relative 'filename'." 81 82 return fn(self.get_filename(filename)) 83 84 def ensure(self, filename=None): 85 86 """ 87 Ensure that this directory, or a directory 'filename' within it, exists. 88 """ 89 90 pathname = filename and self.get_filename(filename) or self.filename 91 92 if not exists(pathname): 93 makedirs(pathname) 94 95 def exists(self, filename): 96 97 "Return whether the relative 'filename' exists within the directory." 98 99 return self._apply(exists, filename) 100 101 def isdir(self, filename): 102 103 "Return whether the relative 'filename' is a directory." 104 105 return self._apply(isdir, filename) 106 107 def isfile(self, filename): 108 109 "Return whether the relative 'filename' is a file." 110 111 return self._apply(isfile, filename) 112 113 def makedirs(self, filename): 114 115 """ 116 Ensure that a directory having the given 'filename' exists by creating 117 it and any directories needed for it to be created. This filename is 118 relative to the directory. 119 """ 120 121 pathname = self.get_filename(filename) 122 123 if not exists(pathname): 124 makedirs(pathname) 125 126 def rename(self, old, new): 127 128 """ 129 Rename the file with the 'old' relative filename to the 'new' relative 130 filename. 131 """ 132 133 rename(self.get_filename(old), self.get_filename(new)) 134 135 def select_files(self, pattern, recursive=False, directories=False): 136 137 """ 138 Return a list of filenames found within the directory matching 139 'pattern'. These filenames are relative to the directory. If 'recursive' 140 is specified and is a true value, subdirectories are also searched. 141 142 If 'directories' is specified and is a true value, return a list of 143 directory names instead of filenames. 144 """ 145 146 selected = [] 147 148 # Obtain pathnames, directory names and filenames within the directory. 149 150 for dirpath, dirnames, filenames in walk(self.filename): 151 if not recursive and dirpath != self.filename: 152 continue 153 154 if directories: 155 objects = dirnames 156 else: 157 objects = filenames 158 159 for filename in objects: 160 161 # Qualify filenames with the directory path. 162 163 pathname = join(dirpath, filename) 164 165 # Obtain the local filename within the directory. 166 167 local_filename = self.within(pathname) 168 169 # Match filenames supporting the pattern. 170 171 if local_filename and fnmatch.fnmatch(local_filename, pattern): 172 selected.append(local_filename) 173 174 return selected 175 176 # File operations involving complete filenames. 177 178 def within(self, filename): 179 180 """ 181 Return the given complete 'filename' translated to be relative to this 182 directory, or return None if the filename describes a location outside 183 the directory. 184 """ 185 186 return within(filename, self.filename) 187 188 # vim: tabstop=4 expandtab shiftwidth=4