Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | #!/usr/bin/env python |
michael@0 | 2 | |
michael@0 | 3 | #tooltool is a lookaside cache implemented in Python |
michael@0 | 4 | #Copyright (C) 2011 John H. Ford <john@johnford.info> |
michael@0 | 5 | # |
michael@0 | 6 | #This program is free software; you can redistribute it and/or |
michael@0 | 7 | #modify it under the terms of the GNU General Public License |
michael@0 | 8 | #as published by the Free Software Foundation version 2 |
michael@0 | 9 | # |
michael@0 | 10 | #This program is distributed in the hope that it will be useful, |
michael@0 | 11 | #but WITHOUT ANY WARRANTY; without even the implied warranty of |
michael@0 | 12 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
michael@0 | 13 | #GNU General Public License for more details. |
michael@0 | 14 | # |
michael@0 | 15 | #You should have received a copy of the GNU General Public License |
michael@0 | 16 | #along with this program; if not, write to the Free Software |
michael@0 | 17 | #Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
michael@0 | 18 | |
michael@0 | 19 | # An manifest file specifies files in that directory that are stored |
michael@0 | 20 | # elsewhere. This file should only contain file in the directory |
michael@0 | 21 | # which the manifest file resides in and it should be called 'manifest.manifest' |
michael@0 | 22 | |
michael@0 | 23 | __version__ = '1' |
michael@0 | 24 | |
michael@0 | 25 | import json |
michael@0 | 26 | import os |
michael@0 | 27 | import optparse |
michael@0 | 28 | import logging |
michael@0 | 29 | import hashlib |
michael@0 | 30 | import urllib2 |
michael@0 | 31 | import ConfigParser |
michael@0 | 32 | |
michael@0 | 33 | log = logging.getLogger(__name__) |
michael@0 | 34 | |
michael@0 | 35 | class FileRecordJSONEncoderException(Exception): pass |
michael@0 | 36 | class InvalidManifest(Exception): pass |
michael@0 | 37 | class ExceptionWithFilename(Exception): |
michael@0 | 38 | def __init__(self, filename): |
michael@0 | 39 | Exception.__init__(self) |
michael@0 | 40 | self.filename = filename |
michael@0 | 41 | |
michael@0 | 42 | class DigestMismatchException(ExceptionWithFilename): pass |
michael@0 | 43 | class MissingFileException(ExceptionWithFilename): pass |
michael@0 | 44 | |
michael@0 | 45 | class FileRecord(object): |
michael@0 | 46 | def __init__(self, filename, size, digest, algorithm): |
michael@0 | 47 | object.__init__(self) |
michael@0 | 48 | self.filename = filename |
michael@0 | 49 | self.size = size |
michael@0 | 50 | self.digest = digest |
michael@0 | 51 | self.algorithm = algorithm |
michael@0 | 52 | log.debug("creating %s 0x%x" % (self.__class__.__name__, id(self))) |
michael@0 | 53 | |
michael@0 | 54 | def __eq__(self, other): |
michael@0 | 55 | if self is other: |
michael@0 | 56 | return True |
michael@0 | 57 | if self.filename == other.filename and \ |
michael@0 | 58 | self.size == other.size and \ |
michael@0 | 59 | self.digest == other.digest and \ |
michael@0 | 60 | self.algorithm == other.algorithm: |
michael@0 | 61 | return True |
michael@0 | 62 | else: |
michael@0 | 63 | return False |
michael@0 | 64 | |
michael@0 | 65 | def __ne__(self, other): |
michael@0 | 66 | return not self.__eq__(other) |
michael@0 | 67 | |
michael@0 | 68 | def __str__(self): |
michael@0 | 69 | return repr(self) |
michael@0 | 70 | |
michael@0 | 71 | def __repr__(self): |
michael@0 | 72 | return "%s.%s(filename='%s', size='%s', digest='%s', algorithm='%s')" % (__name__, |
michael@0 | 73 | self.__class__.__name__, |
michael@0 | 74 | self.filename, self.size, self.digest, self.algorithm) |
michael@0 | 75 | |
michael@0 | 76 | def present(self): |
michael@0 | 77 | # Doesn't check validity |
michael@0 | 78 | return os.path.exists(self.filename) |
michael@0 | 79 | |
michael@0 | 80 | def validate_size(self): |
michael@0 | 81 | if self.present(): |
michael@0 | 82 | return self.size == os.path.getsize(self.filename) |
michael@0 | 83 | else: |
michael@0 | 84 | log.debug("trying to validate size on a missing file, %s", self.filename) |
michael@0 | 85 | raise MissingFileException(filename=self.filename) |
michael@0 | 86 | |
michael@0 | 87 | def validate_digest(self): |
michael@0 | 88 | if self.present(): |
michael@0 | 89 | with open(self.filename, 'rb') as f: |
michael@0 | 90 | return self.digest == digest_file(f, self.algorithm) |
michael@0 | 91 | else: |
michael@0 | 92 | log.debug("trying to validate digest on a missing file, %s', self.filename") |
michael@0 | 93 | raise MissingFileException(filename=self.filename) |
michael@0 | 94 | |
michael@0 | 95 | def validate(self): |
michael@0 | 96 | if self.validate_size(): |
michael@0 | 97 | if self.validate_digest(): |
michael@0 | 98 | return True |
michael@0 | 99 | return False |
michael@0 | 100 | |
michael@0 | 101 | def describe(self): |
michael@0 | 102 | if self.present() and self.validate(): |
michael@0 | 103 | return "'%s' is present and valid" % self.filename |
michael@0 | 104 | elif self.present(): |
michael@0 | 105 | return "'%s' is present and invalid" % self.filename |
michael@0 | 106 | else: |
michael@0 | 107 | return "'%s' is absent" % self.filename |
michael@0 | 108 | |
michael@0 | 109 | |
michael@0 | 110 | def create_file_record(filename, algorithm): |
michael@0 | 111 | fo = open(filename, 'rb') |
michael@0 | 112 | stored_filename = os.path.split(filename)[1] |
michael@0 | 113 | fr = FileRecord(stored_filename, os.path.getsize(filename), digest_file(fo, algorithm), algorithm) |
michael@0 | 114 | fo.close() |
michael@0 | 115 | return fr |
michael@0 | 116 | |
michael@0 | 117 | |
michael@0 | 118 | class FileRecordJSONEncoder(json.JSONEncoder): |
michael@0 | 119 | def encode_file_record(self, obj): |
michael@0 | 120 | if not issubclass(type(obj), FileRecord): |
michael@0 | 121 | err = "FileRecordJSONEncoder is only for FileRecord and lists of FileRecords, not %s" % obj.__class__.__name__ |
michael@0 | 122 | log.warn(err) |
michael@0 | 123 | raise FileRecordJSONEncoderException(err) |
michael@0 | 124 | else: |
michael@0 | 125 | return {'filename': obj.filename, 'size': obj.size, 'algorithm': obj.algorithm, 'digest': obj.digest} |
michael@0 | 126 | |
michael@0 | 127 | def default(self, f): |
michael@0 | 128 | if issubclass(type(f), list): |
michael@0 | 129 | record_list = [] |
michael@0 | 130 | for i in f: |
michael@0 | 131 | record_list.append(self.encode_file_record(i)) |
michael@0 | 132 | return record_list |
michael@0 | 133 | else: |
michael@0 | 134 | return self.encode_file_record(f) |
michael@0 | 135 | |
michael@0 | 136 | |
michael@0 | 137 | class FileRecordJSONDecoder(json.JSONDecoder): |
michael@0 | 138 | """I help the json module materialize a FileRecord from |
michael@0 | 139 | a JSON file. I understand FileRecords and lists of |
michael@0 | 140 | FileRecords. I ignore things that I don't expect for now""" |
michael@0 | 141 | # TODO: make this more explicit in what it's looking for |
michael@0 | 142 | # and error out on unexpected things |
michael@0 | 143 | def process_file_records(self, obj): |
michael@0 | 144 | if isinstance(obj, list): |
michael@0 | 145 | record_list = [] |
michael@0 | 146 | for i in obj: |
michael@0 | 147 | record = self.process_file_records(i) |
michael@0 | 148 | if issubclass(type(record), FileRecord): |
michael@0 | 149 | record_list.append(record) |
michael@0 | 150 | return record_list |
michael@0 | 151 | if isinstance(obj, dict) and \ |
michael@0 | 152 | len(obj.keys()) == 4 and \ |
michael@0 | 153 | obj.has_key('filename') and \ |
michael@0 | 154 | obj.has_key('size') and \ |
michael@0 | 155 | obj.has_key('algorithm') and \ |
michael@0 | 156 | obj.has_key('digest'): |
michael@0 | 157 | rv = FileRecord(obj['filename'], obj['size'], obj['digest'], obj['algorithm']) |
michael@0 | 158 | log.debug("materialized %s" % rv) |
michael@0 | 159 | return rv |
michael@0 | 160 | return obj |
michael@0 | 161 | |
michael@0 | 162 | def decode(self, s): |
michael@0 | 163 | decoded = json.JSONDecoder.decode(self, s) |
michael@0 | 164 | rv = self.process_file_records(decoded) |
michael@0 | 165 | return rv |
michael@0 | 166 | |
michael@0 | 167 | |
michael@0 | 168 | class Manifest(object): |
michael@0 | 169 | |
michael@0 | 170 | valid_formats = ('json',) |
michael@0 | 171 | |
michael@0 | 172 | def __init__(self, file_records=[]): |
michael@0 | 173 | self.file_records = file_records |
michael@0 | 174 | |
michael@0 | 175 | def __eq__(self, other): |
michael@0 | 176 | if self is other: |
michael@0 | 177 | return True |
michael@0 | 178 | if len(self.file_records) != len(other.file_records): |
michael@0 | 179 | log.debug('Manifests differ in number of files') |
michael@0 | 180 | return False |
michael@0 | 181 | #TODO: Lists in a different order should be equal |
michael@0 | 182 | for record in range(0,len(self.file_records)): |
michael@0 | 183 | if self.file_records[record] != other.file_records[record]: |
michael@0 | 184 | log.debug('FileRecords differ, %s vs %s' % (self.file_records[record], |
michael@0 | 185 | other.file_records[record])) |
michael@0 | 186 | return False |
michael@0 | 187 | return True |
michael@0 | 188 | |
michael@0 | 189 | def __deepcopy__(self, memo): |
michael@0 | 190 | # This is required for a deep copy |
michael@0 | 191 | return Manifest(self.file_records[:]) |
michael@0 | 192 | |
michael@0 | 193 | def __copy__(self): |
michael@0 | 194 | return Manifest(self.file_records) |
michael@0 | 195 | |
michael@0 | 196 | def copy(self): |
michael@0 | 197 | return Manifest(self.file_records[:]) |
michael@0 | 198 | |
michael@0 | 199 | def present(self): |
michael@0 | 200 | return all(i.present() for i in self.file_records) |
michael@0 | 201 | |
michael@0 | 202 | def validate_sizes(self): |
michael@0 | 203 | return all(i.validate_size() for i in self.file_records) |
michael@0 | 204 | |
michael@0 | 205 | def validate_digests(self): |
michael@0 | 206 | return all(i.validate_digest() for i in self.file_records) |
michael@0 | 207 | |
michael@0 | 208 | def validate(self): |
michael@0 | 209 | return all(i.validate() for i in self.file_records) |
michael@0 | 210 | |
michael@0 | 211 | def sort(self): |
michael@0 | 212 | #TODO: WRITE TESTS |
michael@0 | 213 | self.file_records.sort(key=lambda x: x.size) |
michael@0 | 214 | |
michael@0 | 215 | def load(self, data_file, fmt='json'): |
michael@0 | 216 | assert fmt in self.valid_formats |
michael@0 | 217 | if fmt == 'json': |
michael@0 | 218 | try: |
michael@0 | 219 | self.file_records.extend(json.load(data_file, cls=FileRecordJSONDecoder)) |
michael@0 | 220 | self.sort() |
michael@0 | 221 | except ValueError: |
michael@0 | 222 | raise InvalidManifest("trying to read invalid manifest file") |
michael@0 | 223 | |
michael@0 | 224 | def loads(self, data_string, fmt='json'): |
michael@0 | 225 | assert fmt in self.valid_formats |
michael@0 | 226 | if fmt == 'json': |
michael@0 | 227 | try: |
michael@0 | 228 | self.file_records.extend(json.loads(data_string, cls=FileRecordJSONDecoder)) |
michael@0 | 229 | self.sort() |
michael@0 | 230 | except ValueError: |
michael@0 | 231 | raise InvalidManifest("trying to read invalid manifest file") |
michael@0 | 232 | |
michael@0 | 233 | def dump(self, output_file, fmt='json'): |
michael@0 | 234 | assert fmt in self.valid_formats |
michael@0 | 235 | self.sort() |
michael@0 | 236 | if fmt == 'json': |
michael@0 | 237 | rv = json.dump(self.file_records, output_file, indent=0, cls=FileRecordJSONEncoder) |
michael@0 | 238 | print >> output_file, '' |
michael@0 | 239 | return rv |
michael@0 | 240 | |
michael@0 | 241 | def dumps(self, fmt='json'): |
michael@0 | 242 | assert fmt in self.valid_formats |
michael@0 | 243 | self.sort() |
michael@0 | 244 | if fmt == 'json': |
michael@0 | 245 | return json.dumps(self.file_records, cls=FileRecordJSONEncoder) |
michael@0 | 246 | |
michael@0 | 247 | |
michael@0 | 248 | def digest_file(f, a): |
michael@0 | 249 | """I take a file like object 'f' and return a hex-string containing |
michael@0 | 250 | of the result of the algorithm 'a' applied to 'f'.""" |
michael@0 | 251 | h = hashlib.new(a) |
michael@0 | 252 | chunk_size = 1024*10 |
michael@0 | 253 | data = f.read(chunk_size) |
michael@0 | 254 | while data: |
michael@0 | 255 | h.update(data) |
michael@0 | 256 | data = f.read(chunk_size) |
michael@0 | 257 | if hasattr(f, 'name'): |
michael@0 | 258 | log.debug('hashed %s with %s to be %s', f.name, a, h.hexdigest()) |
michael@0 | 259 | else: |
michael@0 | 260 | log.debug('hashed a file with %s to be %s', a, h.hexdigest()) |
michael@0 | 261 | return h.hexdigest() |
michael@0 | 262 | |
michael@0 | 263 | # TODO: write tests for this function |
michael@0 | 264 | def open_manifest(manifest_file): |
michael@0 | 265 | """I know how to take a filename and load it into a Manifest object""" |
michael@0 | 266 | if os.path.exists(manifest_file): |
michael@0 | 267 | manifest = Manifest() |
michael@0 | 268 | with open(manifest_file) as f: |
michael@0 | 269 | manifest.load(f) |
michael@0 | 270 | log.debug("loaded manifest from file '%s'" % manifest_file) |
michael@0 | 271 | return manifest |
michael@0 | 272 | else: |
michael@0 | 273 | log.debug("tried to load absent file '%s' as manifest" % manifest_file) |
michael@0 | 274 | raise InvalidManifest("manifest file '%s' does not exist" % manifest_file) |
michael@0 | 275 | |
michael@0 | 276 | # TODO: write tests for this function |
michael@0 | 277 | def list_manifest(manifest_file): |
michael@0 | 278 | """I know how print all the files in a location""" |
michael@0 | 279 | try: |
michael@0 | 280 | manifest = open_manifest(manifest_file) |
michael@0 | 281 | except InvalidManifest: |
michael@0 | 282 | log.error("failed to load manifest file at '%s'" % manifest_file) |
michael@0 | 283 | return False |
michael@0 | 284 | for f in manifest.file_records: |
michael@0 | 285 | print "%s\t%s\t%s" % ("P" if f.present() else "-", |
michael@0 | 286 | "V" if f.present() and f.validate() else "-", |
michael@0 | 287 | f.filename) |
michael@0 | 288 | return True |
michael@0 | 289 | |
michael@0 | 290 | def validate_manifest(manifest_file): |
michael@0 | 291 | """I validate that all files in a manifest are present and valid but |
michael@0 | 292 | don't fetch or delete them if they aren't""" |
michael@0 | 293 | try: |
michael@0 | 294 | manifest = open_manifest(manifest_file) |
michael@0 | 295 | except InvalidManifest: |
michael@0 | 296 | log.error("failed to load manifest file at '%s'" % manifest_file) |
michael@0 | 297 | return False |
michael@0 | 298 | invalid_files = [] |
michael@0 | 299 | absent_files = [] |
michael@0 | 300 | for f in manifest.file_records: |
michael@0 | 301 | if not f.present(): |
michael@0 | 302 | absent_files.append(f) |
michael@0 | 303 | else: |
michael@0 | 304 | if not f.validate(): |
michael@0 | 305 | invalid_files.append(f) |
michael@0 | 306 | if len(invalid_files + absent_files) == 0: |
michael@0 | 307 | return True |
michael@0 | 308 | else: |
michael@0 | 309 | return False |
michael@0 | 310 | |
michael@0 | 311 | # TODO: write tests for this function |
michael@0 | 312 | def add_files(manifest_file, algorithm, filenames): |
michael@0 | 313 | # returns True if all files successfully added, False if not |
michael@0 | 314 | # and doesn't catch library Exceptions. If any files are already |
michael@0 | 315 | # tracked in the manifest, return will be False because they weren't |
michael@0 | 316 | # added |
michael@0 | 317 | all_files_added = True |
michael@0 | 318 | # Create a old_manifest object to add to |
michael@0 | 319 | if os.path.exists(manifest_file): |
michael@0 | 320 | old_manifest = open_manifest(manifest_file) |
michael@0 | 321 | else: |
michael@0 | 322 | old_manifest = Manifest() |
michael@0 | 323 | log.debug("creating a new manifest file") |
michael@0 | 324 | new_manifest = Manifest() # use a different manifest for the output |
michael@0 | 325 | for filename in filenames: |
michael@0 | 326 | log.debug("adding %s" % filename) |
michael@0 | 327 | path, name = os.path.split(filename) |
michael@0 | 328 | new_fr = create_file_record(filename, algorithm) |
michael@0 | 329 | log.debug("appending a new file record to manifest file") |
michael@0 | 330 | add = True |
michael@0 | 331 | for fr in old_manifest.file_records: |
michael@0 | 332 | log.debug("manifest file has '%s'" % "', ".join([x.filename for x in old_manifest.file_records])) |
michael@0 | 333 | if new_fr == fr and new_fr.validate(): |
michael@0 | 334 | # TODO: Decide if this case should really cause a False return |
michael@0 | 335 | log.info("file already in old_manifest file and matches") |
michael@0 | 336 | add = False |
michael@0 | 337 | elif new_fr == fr and not new_fr.validate(): |
michael@0 | 338 | log.error("file already in old_manifest file but is invalid") |
michael@0 | 339 | add = False |
michael@0 | 340 | if filename == fr.filename: |
michael@0 | 341 | log.error("manifest already contains file named %s" % filename) |
michael@0 | 342 | add = False |
michael@0 | 343 | if add: |
michael@0 | 344 | new_manifest.file_records.append(new_fr) |
michael@0 | 345 | log.debug("added '%s' to manifest" % filename) |
michael@0 | 346 | else: |
michael@0 | 347 | all_files_added = False |
michael@0 | 348 | with open(manifest_file, 'wb') as output: |
michael@0 | 349 | new_manifest.dump(output, fmt='json') |
michael@0 | 350 | return all_files_added |
michael@0 | 351 | |
michael@0 | 352 | |
michael@0 | 353 | # TODO: write tests for this function |
michael@0 | 354 | def fetch_file(base_url, file_record, overwrite=False, grabchunk=1024*4): |
michael@0 | 355 | # A file which is requested to be fetched that exists locally will be hashed. |
michael@0 | 356 | # If the hash matches the requested file's hash, nothing will be done and the |
michael@0 | 357 | # function will return. If the function is told to overwrite and there is a |
michael@0 | 358 | # digest mismatch, the exiting file will be overwritten |
michael@0 | 359 | if file_record.present(): |
michael@0 | 360 | if file_record.validate(): |
michael@0 | 361 | log.info("existing '%s' is valid, not fetching" % file_record.filename) |
michael@0 | 362 | return True |
michael@0 | 363 | if overwrite: |
michael@0 | 364 | log.info("overwriting '%s' as requested" % file_record.filename) |
michael@0 | 365 | else: |
michael@0 | 366 | # All of the following is for a useful error message |
michael@0 | 367 | with open(file_record.filename, 'rb') as f: |
michael@0 | 368 | d = digest_file(f, file_record.algorithm) |
michael@0 | 369 | log.error("digest mismatch between manifest(%s...) and local file(%s...)" % \ |
michael@0 | 370 | (file_record.digest[:8], d[:8])) |
michael@0 | 371 | log.debug("full digests: manifest (%s) local file (%s)" % (file_record.digest, d)) |
michael@0 | 372 | # Let's bail! |
michael@0 | 373 | return False |
michael@0 | 374 | |
michael@0 | 375 | # Generate the URL for the file on the server side |
michael@0 | 376 | url = "%s/%s/%s" % (base_url, file_record.algorithm, file_record.digest) |
michael@0 | 377 | |
michael@0 | 378 | log.debug("fetching from '%s'" % url) |
michael@0 | 379 | |
michael@0 | 380 | # TODO: This should be abstracted to make generic retreival protocol handling easy |
michael@0 | 381 | # Well, the file doesn't exist locally. Lets fetch it. |
michael@0 | 382 | try: |
michael@0 | 383 | f = urllib2.urlopen(url) |
michael@0 | 384 | log.debug("opened %s for reading" % url) |
michael@0 | 385 | with open(file_record.filename, 'wb') as out: |
michael@0 | 386 | k = True |
michael@0 | 387 | size = 0 |
michael@0 | 388 | while k: |
michael@0 | 389 | # TODO: print statistics as file transfers happen both for info and to stop |
michael@0 | 390 | # buildbot timeouts |
michael@0 | 391 | indata = f.read(grabchunk) |
michael@0 | 392 | out.write(indata) |
michael@0 | 393 | size += len(indata) |
michael@0 | 394 | if indata == '': |
michael@0 | 395 | k = False |
michael@0 | 396 | if size != file_record.size: |
michael@0 | 397 | log.error("transfer from %s to %s failed due to a difference of %d bytes" % (url, |
michael@0 | 398 | file_record.filename, file_record.size - size)) |
michael@0 | 399 | return False |
michael@0 | 400 | log.info("fetched %s" % file_record.filename) |
michael@0 | 401 | except (urllib2.URLError, urllib2.HTTPError) as e: |
michael@0 | 402 | log.error("failed to fetch '%s': %s" % (file_record.filename, e), |
michael@0 | 403 | exc_info=True) |
michael@0 | 404 | return False |
michael@0 | 405 | except IOError: |
michael@0 | 406 | log.error("failed to write to '%s'" % file_record.filename, |
michael@0 | 407 | exc_info=True) |
michael@0 | 408 | return False |
michael@0 | 409 | return True |
michael@0 | 410 | |
michael@0 | 411 | |
michael@0 | 412 | # TODO: write tests for this function |
michael@0 | 413 | def fetch_files(manifest_file, base_url, overwrite, filenames=[]): |
michael@0 | 414 | # Lets load the manifest file |
michael@0 | 415 | try: |
michael@0 | 416 | manifest = open_manifest(manifest_file) |
michael@0 | 417 | except InvalidManifest: |
michael@0 | 418 | log.error("failed to load manifest file at '%s'" % manifest_file) |
michael@0 | 419 | return False |
michael@0 | 420 | # We want to track files that fail to be fetched as well as |
michael@0 | 421 | # files that are fetched |
michael@0 | 422 | failed_files = [] |
michael@0 | 423 | |
michael@0 | 424 | # Lets go through the manifest and fetch the files that we want |
michael@0 | 425 | fetched_files = [] |
michael@0 | 426 | for f in manifest.file_records: |
michael@0 | 427 | if f.filename in filenames or len(filenames) == 0: |
michael@0 | 428 | log.debug("fetching %s" % f.filename) |
michael@0 | 429 | if fetch_file(base_url, f, overwrite): |
michael@0 | 430 | fetched_files.append(f) |
michael@0 | 431 | else: |
michael@0 | 432 | failed_files.append(f.filename) |
michael@0 | 433 | else: |
michael@0 | 434 | log.debug("skipping %s" % f.filename) |
michael@0 | 435 | |
michael@0 | 436 | # Even if we get the file, lets ensure that it matches what the |
michael@0 | 437 | # manifest specified |
michael@0 | 438 | for localfile in fetched_files: |
michael@0 | 439 | if not localfile.validate(): |
michael@0 | 440 | log.error("'%s'" % localfile.describe()) |
michael@0 | 441 | |
michael@0 | 442 | # If we failed to fetch or validate a file, we need to fail |
michael@0 | 443 | if len(failed_files) > 0: |
michael@0 | 444 | log.error("The following files failed: '%s'" % "', ".join(failed_files)) |
michael@0 | 445 | return False |
michael@0 | 446 | return True |
michael@0 | 447 | |
michael@0 | 448 | |
michael@0 | 449 | # TODO: write tests for this function |
michael@0 | 450 | def process_command(options, args): |
michael@0 | 451 | """ I know how to take a list of program arguments and |
michael@0 | 452 | start doing the right thing with them""" |
michael@0 | 453 | cmd = args[0] |
michael@0 | 454 | cmd_args = args[1:] |
michael@0 | 455 | log.debug("processing '%s' command with args '%s'" % (cmd, '", "'.join(cmd_args))) |
michael@0 | 456 | log.debug("using options: %s" % options) |
michael@0 | 457 | if cmd == 'list': |
michael@0 | 458 | return list_manifest(options['manifest']) |
michael@0 | 459 | if cmd == 'validate': |
michael@0 | 460 | return validate_manifest(options['manifest']) |
michael@0 | 461 | elif cmd == 'add': |
michael@0 | 462 | return add_files(options['manifest'], options['algorithm'], cmd_args) |
michael@0 | 463 | elif cmd == 'fetch': |
michael@0 | 464 | if not options.has_key('base_url') or options.get('base_url') is None: |
michael@0 | 465 | log.critical('fetch command requires url option') |
michael@0 | 466 | return False |
michael@0 | 467 | return fetch_files(options['manifest'], options['base_url'], options['overwrite'], cmd_args) |
michael@0 | 468 | else: |
michael@0 | 469 | log.critical('command "%s" is not implemented' % cmd) |
michael@0 | 470 | return False |
michael@0 | 471 | |
michael@0 | 472 | # fetching api: |
michael@0 | 473 | # http://hostname/algorithm/hash |
michael@0 | 474 | # example: http://people.mozilla.org/sha1/1234567890abcedf |
michael@0 | 475 | # This will make it possible to have the server allow clients to |
michael@0 | 476 | # use different algorithms than what was uploaded to the server |
michael@0 | 477 | |
michael@0 | 478 | # TODO: Implement the following features: |
michael@0 | 479 | # -optimization: do small files first, justification is that they are faster |
michael@0 | 480 | # and cause a faster failure if they are invalid |
michael@0 | 481 | # -store permissions |
michael@0 | 482 | # -local renames i.e. call the file one thing on the server and |
michael@0 | 483 | # something different locally |
michael@0 | 484 | # -deal with the cases: |
michael@0 | 485 | # -local data matches file requested with different filename |
michael@0 | 486 | # -two different files with same name, different hash |
michael@0 | 487 | # -?only ever locally to digest as filename, symlink to real name |
michael@0 | 488 | # -?maybe deal with files as a dir of the filename with all files in that dir as the versions of that file |
michael@0 | 489 | # - e.g. ./python-2.6.7.dmg/0123456789abcdef and ./python-2.6.7.dmg/abcdef0123456789 |
michael@0 | 490 | |
michael@0 | 491 | def main(): |
michael@0 | 492 | # Set up logging, for now just to the console |
michael@0 | 493 | ch = logging.StreamHandler() |
michael@0 | 494 | cf = logging.Formatter("%(levelname)s - %(message)s") |
michael@0 | 495 | ch.setFormatter(cf) |
michael@0 | 496 | |
michael@0 | 497 | # Set up option parsing |
michael@0 | 498 | parser = optparse.OptionParser() |
michael@0 | 499 | # I wish there was a way to say "only allow args to be |
michael@0 | 500 | # sequential and at the end of the argv. |
michael@0 | 501 | # OH! i could step through sys.argv and check for things starting without -/-- before things starting with them |
michael@0 | 502 | parser.add_option('-q', '--quiet', default=False, |
michael@0 | 503 | dest='quiet', action='store_true') |
michael@0 | 504 | parser.add_option('-v', '--verbose', default=False, |
michael@0 | 505 | dest='verbose', action='store_true') |
michael@0 | 506 | parser.add_option('-m', '--manifest', default='manifest.tt', |
michael@0 | 507 | dest='manifest', action='store', |
michael@0 | 508 | help='specify the manifest file to be operated on') |
michael@0 | 509 | parser.add_option('-d', '--algorithm', default='sha512', |
michael@0 | 510 | dest='algorithm', action='store', |
michael@0 | 511 | help='openssl hashing algorithm to use') |
michael@0 | 512 | parser.add_option('-o', '--overwrite', default=False, |
michael@0 | 513 | dest='overwrite', action='store_true', |
michael@0 | 514 | help='if fetching, remote copy will overwrite a local copy that is different. ') |
michael@0 | 515 | parser.add_option('--url', dest='base_url', action='store', |
michael@0 | 516 | help='base url for fetching files') |
michael@0 | 517 | parser.add_option('--ignore-config-files', action='store_true', default=False, |
michael@0 | 518 | dest='ignore_cfg_files') |
michael@0 | 519 | (options_obj, args) = parser.parse_args() |
michael@0 | 520 | # Dictionaries are easier to work with |
michael@0 | 521 | options = vars(options_obj) |
michael@0 | 522 | |
michael@0 | 523 | |
michael@0 | 524 | # Use some of the option parser to figure out application |
michael@0 | 525 | # log level |
michael@0 | 526 | if options.get('verbose'): |
michael@0 | 527 | ch.setLevel(logging.DEBUG) |
michael@0 | 528 | elif options.get('quiet'): |
michael@0 | 529 | ch.setLevel(logging.ERROR) |
michael@0 | 530 | else: |
michael@0 | 531 | ch.setLevel(logging.INFO) |
michael@0 | 532 | log.addHandler(ch) |
michael@0 | 533 | |
michael@0 | 534 | cfg_file = ConfigParser.SafeConfigParser() |
michael@0 | 535 | if not options.get("ignore_cfg_files"): |
michael@0 | 536 | read_files = cfg_file.read(['/etc/tooltool', os.path.expanduser('~/.tooltool'), |
michael@0 | 537 | os.path.join(os.getcwd(), '.tooltool')]) |
michael@0 | 538 | log.debug("read in the config files '%s'" % '", '.join(read_files)) |
michael@0 | 539 | else: |
michael@0 | 540 | log.debug("skipping config files") |
michael@0 | 541 | |
michael@0 | 542 | for option in ('base_url', 'algorithm'): |
michael@0 | 543 | if not options.get(option): |
michael@0 | 544 | try: |
michael@0 | 545 | options[option] = cfg_file.get('general', option) |
michael@0 | 546 | log.debug("read '%s' as '%s' from cfg_file" % (option, options[option])) |
michael@0 | 547 | except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as e: |
michael@0 | 548 | log.debug("%s in config file" % e, exc_info=True) |
michael@0 | 549 | |
michael@0 | 550 | if not options.has_key('manifest'): |
michael@0 | 551 | parser.error("no manifest file specified") |
michael@0 | 552 | |
michael@0 | 553 | if len(args) < 1: |
michael@0 | 554 | parser.error('You must specify a command') |
michael@0 | 555 | exit(0 if process_command(options, args) else 1) |
michael@0 | 556 | |
michael@0 | 557 | if __name__ == "__main__": |
michael@0 | 558 | main() |
michael@0 | 559 | else: |
michael@0 | 560 | log.addHandler(logging.NullHandler()) |
michael@0 | 561 | #log.addHandler(logging.StreamHandler()) |