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): 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 143 selected = [] 144 145 # Obtain pathnames, directory names and filenames within the directory. 146 147 for dirpath, dirnames, filenames in walk(self.filename): 148 if not recursive and dirpath != self.filename: 149 continue 150 151 for filename in filenames: 152 153 # Qualify filenames with the directory path. 154 155 pathname = join(dirpath, filename) 156 157 # Obtain the local filename within the directory. 158 159 local_filename = self.within(pathname) 160 161 # Match filenames supporting the pattern. 162 163 if local_filename and fnmatch.fnmatch(local_filename, pattern): 164 selected.append(local_filename) 165 166 return selected 167 168 # File operations involving complete filenames. 169 170 def within(self, filename): 171 172 """ 173 Return the given complete 'filename' translated to be relative to this 174 directory, or return None if the filename describes a location outside 175 the directory. 176 """ 177 178 return within(filename, self.filename) 179 180 # vim: tabstop=4 expandtab shiftwidth=4