1 #!/usr/bin/env python 2 3 """ 4 Directory context functionality. 5 6 Copyright (C) 2018 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): 136 137 """ 138 Return a list of filenames found within the directory matching 139 'pattern'. These filenames are relative to the directory. 140 """ 141 142 selected = [] 143 144 # Obtain pathnames, directory names and filenames within the directory. 145 146 for dirpath, dirnames, filenames in walk(self.filename): 147 for filename in filenames: 148 149 # Qualify filenames with the directory path. 150 151 pathname = join(dirpath, filename) 152 153 # Obtain the local filename within the directory. 154 155 local_filename = self.within(pathname) 156 157 # Match filenames supporting the pattern. 158 159 if local_filename and fnmatch.fnmatch(local_filename, pattern): 160 selected.append(local_filename) 161 162 return selected 163 164 # File operations involving complete filenames. 165 166 def within(self, filename): 167 168 """ 169 Return the given complete 'filename' translated to be relative to this 170 directory, or return None if the filename describes a location outside 171 the directory. 172 """ 173 174 return within(filename, self.filename) 175 176 # vim: tabstop=4 expandtab shiftwidth=4