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