|
1 #!/usr/bin/python |
|
2 # |
|
3 # CDDL HEADER START |
|
4 # |
|
5 # The contents of this file are subject to the terms of the |
|
6 # Common Development and Distribution License (the "License"). |
|
7 # You may not use this file except in compliance with the License. |
|
8 # |
|
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
|
10 # or http://www.opensolaris.org/os/licensing. |
|
11 # See the License for the specific language governing permissions |
|
12 # and limitations under the License. |
|
13 # |
|
14 # When distributing Covered Code, include this CDDL HEADER in each |
|
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
|
16 # If applicable, add the following below this CDDL HEADER, with the |
|
17 # fields enclosed by brackets "[]" replaced with your own identifying |
|
18 # information: Portions Copyright [yyyy] [name of copyright owner] |
|
19 # |
|
20 # CDDL HEADER END |
|
21 # |
|
22 |
|
23 # |
|
24 # Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. |
|
25 # |
|
26 |
|
27 import atexit |
|
28 import collections |
|
29 import errno |
|
30 import tarfile as tf |
|
31 import pkg.pkggzip |
|
32 import pkg.pkgtarfile as ptf |
|
33 import os |
|
34 import pkg |
|
35 import pkg.client.api_errors as apx |
|
36 import pkg.client.publisher |
|
37 import pkg.fmri |
|
38 import pkg.misc |
|
39 import pkg.portable |
|
40 import pkg.p5i |
|
41 import shutil |
|
42 import tempfile |
|
43 import urllib |
|
44 |
|
45 |
|
46 class ArchiveErrors(apx.ApiException): |
|
47 """Base exception class for archive class errors.""" |
|
48 |
|
49 |
|
50 class InvalidArchiveIndex(ArchiveErrors): |
|
51 """Used to indicate that the specified index is in a format not |
|
52 supported or recognized by this version of the pkg(5) ArchiveIndex |
|
53 class.""" |
|
54 |
|
55 def __init__(self, arc_name): |
|
56 ArchiveErrors.__init__(self) |
|
57 self.__name = arc_name |
|
58 |
|
59 def __str__(self): |
|
60 return _("%s is not in a supported or recognizable archive " |
|
61 "index format.") % self.__name |
|
62 |
|
63 |
|
64 class ArchiveIndex(object): |
|
65 """Class representing a pkg(5) archive table of contents and a set of |
|
66 interfaces to populate and retrieve entries. |
|
67 |
|
68 Entries in this file are written in the following format: |
|
69 |
|
70 <name>NUL<offset>NUL<entry_size>NUL<size>NUL<typeflag>NULNL |
|
71 |
|
72 <name> is a string containing the pathname of the file in the |
|
73 archive. It can be up to 65,535 bytes in length. |
|
74 |
|
75 <offset> is an unsigned long long integer containing the relative |
|
76 offset in bytes of the first header block for the file in the |
|
77 archive. The offset is relative to the end of the last block of |
|
78 the first file in the archive. |
|
79 |
|
80 <entry_size> is an unsigned long long integer containing the size of |
|
81 the file's entry in bytes in the archive (including archive |
|
82 headers and trailers for the entry). |
|
83 |
|
84 <size> is an unsigned long long integer containing the size of the |
|
85 file in bytes in the archive. |
|
86 |
|
87 <typeflag> is a single character representing the type of the file |
|
88 in the archive. Possible values are: |
|
89 0 Regular File |
|
90 1 Hard Link |
|
91 2 Symbolic Link |
|
92 5 Directory or subdirectory""" |
|
93 |
|
94 version = None |
|
95 CURRENT_VERSION = 0 |
|
96 COMPATIBLE_VERSIONS = 0, |
|
97 ENTRY_FORMAT = "%s\0%d\0%d\0%d\0%c\0\n" |
|
98 |
|
99 def __init__(self, name, mode="r", version=None): |
|
100 """Open a pkg(5) archive table of contents file. |
|
101 |
|
102 'name' should be the absolute path of the file to use when |
|
103 reading or writing index data. |
|
104 |
|
105 'mode' indicates whether the index is being used for reading |
|
106 or writing, and can be 'r' or 'w'. Appending to or updating |
|
107 a table of contents file is not supported. |
|
108 |
|
109 'version' is an optional integer value specifying the version |
|
110 of the index to be read or written. If not specified, the |
|
111 current version is assumed. |
|
112 """ |
|
113 |
|
114 assert os.path.isabs(name) |
|
115 if version is None: |
|
116 version = self.CURRENT_VERSION |
|
117 if version not in self.COMPATIBLE_VERSIONS: |
|
118 raise InvalidArchiveIndex(name) |
|
119 |
|
120 self.__closed = False |
|
121 self.__name = name |
|
122 self.__mode = mode + "b" |
|
123 try: |
|
124 self.__file = pkg.pkggzip.PkgGzipFile(self.__name, |
|
125 self.__mode) |
|
126 except IOError, e: |
|
127 if e.errno: |
|
128 raise |
|
129 # Underlying gzip library raises this exception if the |
|
130 # file isn't a valid gzip file. So, assume that if |
|
131 # errno isn't set, this is a gzip error instead. |
|
132 raise InvalidArchiveIndex(name) |
|
133 |
|
134 self.version = version |
|
135 |
|
136 def __exit__(self, exc_type, exc_value, exc_tb): |
|
137 """Context handler that ensures archive is automatically closed |
|
138 in a non-error condition scenario. This enables 'with' usage. |
|
139 """ |
|
140 if exc_type or exc_value or exc_tb: |
|
141 # Only close filehandles in an error condition. |
|
142 self.__close_fh() |
|
143 else: |
|
144 # Close archive normally in all other cases. |
|
145 self.close() |
|
146 |
|
147 @property |
|
148 def pathname(self): |
|
149 """The absolute path of the archive index file.""" |
|
150 return self.__name |
|
151 |
|
152 def add(self, name, offset, entry_size, size, typeflag): |
|
153 """Add an entry for the given archive file to the table of |
|
154 contents.""" |
|
155 |
|
156 self.__file.write(self.ENTRY_FORMAT % (name, offset, entry_size, |
|
157 size, typeflag)) |
|
158 |
|
159 def offsets(self): |
|
160 """Returns a generator that yields tuples of the form (name, |
|
161 offset) for each file in the index.""" |
|
162 |
|
163 self.__file.seek(0) |
|
164 l = None |
|
165 try: |
|
166 for line in self.__file: |
|
167 if line[-2] != "\0": |
|
168 # Filename contained newline. |
|
169 if l is None: |
|
170 l = line |
|
171 else: |
|
172 l += "\n" |
|
173 l += line |
|
174 continue |
|
175 elif l is None: |
|
176 l = line |
|
177 |
|
178 name, offset, ignored = l.split("\0", 2) |
|
179 yield name, long(offset) |
|
180 l = None |
|
181 except ValueError: |
|
182 raise InvalidArchiveIndex(self.__name) |
|
183 except IOError, e: |
|
184 if e.errno: |
|
185 raise |
|
186 # Underlying gzip library raises this exception if the |
|
187 # file isn't a valid gzip file. So, assume that if |
|
188 # errno isn't set, this is a gzip error instead. |
|
189 raise InvalidArchiveIndex(self.__name) |
|
190 |
|
191 def close(self): |
|
192 """Close the index. No further operations can be performed |
|
193 using this object once closed.""" |
|
194 |
|
195 if self.__closed: |
|
196 return |
|
197 if self.__file: |
|
198 self.__file.close() |
|
199 self.__file = None |
|
200 self.__closed = True |
|
201 |
|
202 |
|
203 class InvalidArchive(ArchiveErrors): |
|
204 """Used to indicate that the specified archive is in a format not |
|
205 supported or recognized by this version of the pkg(5) Archive class. |
|
206 """ |
|
207 |
|
208 def __init__(self, arc_name): |
|
209 ArchiveErrors.__init__(self) |
|
210 self.arc_name = arc_name |
|
211 |
|
212 def __str__(self): |
|
213 return _("Archive %s is missing, unsupported, or corrupt.") % \ |
|
214 self.arc_name |
|
215 |
|
216 |
|
217 class CorruptArchiveFiles(ArchiveErrors): |
|
218 """Used to indicate that the specified file(s) could not be found in the |
|
219 archive. |
|
220 """ |
|
221 |
|
222 def __init__(self, arc_name, files): |
|
223 ArchiveErrors.__init__(self) |
|
224 self.arc_name = arc_name |
|
225 self.files = files |
|
226 |
|
227 def __str__(self): |
|
228 return _("Package archive %(arc_name)s contains corrupt " |
|
229 "entries for the requested package file(s):\n%(files)s.") % { |
|
230 "arc_name": self.arc_name, |
|
231 "files": "\n".join(self.files) } |
|
232 |
|
233 |
|
234 class UnknownArchiveFiles(ArchiveErrors): |
|
235 """Used to indicate that the specified file(s) could not be found in the |
|
236 archive. |
|
237 """ |
|
238 |
|
239 def __init__(self, arc_name, files): |
|
240 ArchiveErrors.__init__(self) |
|
241 self.arc_name = arc_name |
|
242 self.files = files |
|
243 |
|
244 def __str__(self): |
|
245 return _("Package archive %(arc_name)s does not contain the " |
|
246 "requested package file(s):\n%(files)s.") % { |
|
247 "arc_name": self.arc_name, |
|
248 "files": "\n".join(self.files) } |
|
249 |
|
250 |
|
251 class UnknownPackageManifest(ArchiveErrors): |
|
252 """Used to indicate that a manifest for the specified package could not |
|
253 be found in the archive. |
|
254 """ |
|
255 |
|
256 def __init__(self, arc_name, pfmri): |
|
257 ArchiveErrors.__init__(self) |
|
258 self.arc_name = arc_name |
|
259 self.pfmri = pfmri |
|
260 |
|
261 def __str__(self): |
|
262 return _("No package manifest for package '%(pfmri)s' exists " |
|
263 "in archive %(arc_name)s.") % self.__dict__ |
|
264 |
|
265 |
|
266 class Archive(object): |
|
267 """Class representing a pkg(5) archive and a set of interfaces to |
|
268 populate it and retrieve data from it. |
|
269 |
|
270 This class stores package data in pax archives in version 4 repository |
|
271 format. Encoding the structure of a repository into the archive is |
|
272 necessary to enable easy composition of package archive contents with |
|
273 existing repositories and to enable consumers to access the contents of |
|
274 a package archive the same as they would a repository. |
|
275 |
|
276 This class can be used to access or extract the contents of almost any |
|
277 tar archive, except for those that are compressed. |
|
278 """ |
|
279 |
|
280 __idx_pfx = "pkg5.index." |
|
281 __idx_sfx = ".gz" |
|
282 __idx_name = "pkg5.index.%s.gz" |
|
283 __idx_ver = ArchiveIndex.CURRENT_VERSION |
|
284 __index = None |
|
285 __arc_tfile = None |
|
286 __arc_file = None |
|
287 version = None |
|
288 |
|
289 # If the repository format changes, then the version of the package |
|
290 # archive format should be rev'd and this updated. (Although that isn't |
|
291 # strictly necessary, as the Repository class should remain backwards |
|
292 # compatible with this format.) |
|
293 CURRENT_VERSION = 0 |
|
294 COMPATIBLE_VERSIONS = (0,) |
|
295 |
|
296 def __init__(self, pathname, mode="r"): |
|
297 """'pathname' is the absolute path of the archive file to create |
|
298 or read from. |
|
299 |
|
300 'mode' is a string used to indicate whether the archive is being |
|
301 opened for reading or writing, which is indicated by 'r' and 'w' |
|
302 respectively. An archive opened for writing may not be used for |
|
303 any extraction operations, and must not already exist. |
|
304 """ |
|
305 |
|
306 assert os.path.isabs(pathname) |
|
307 self.__arc_name = pathname |
|
308 self.__closed = False |
|
309 self.__mode = mode |
|
310 self.__temp_dir = tempfile.mkdtemp() |
|
311 |
|
312 # Used to cache publisher objects. |
|
313 self.__pubs = None |
|
314 |
|
315 # Used to cache location of publisher catalog data. |
|
316 self.__catalogs = {} |
|
317 |
|
318 arc_mode = mode + "b" |
|
319 mode += ":" |
|
320 |
|
321 assert "r" in mode or "w" in mode |
|
322 assert "a" not in mode |
|
323 if "w" in mode: |
|
324 # Don't allow overwrite of existing archive. |
|
325 assert not os.path.exists(self.__arc_name) |
|
326 |
|
327 try: |
|
328 self.__arc_file = open(self.__arc_name, arc_mode, |
|
329 128*1024) |
|
330 except EnvironmentError, e: |
|
331 if e.errno in (errno.ENOENT, errno.EISDIR): |
|
332 raise InvalidArchive(self.__arc_name) |
|
333 raise apx._convert_error(e) |
|
334 |
|
335 self.__queue_offset = 0 |
|
336 self.__queue = collections.deque() |
|
337 |
|
338 # Ensure cleanup is performed on exit if the archive is not |
|
339 # explicitly closed. |
|
340 def arc_cleanup(): |
|
341 if not self.__closed: |
|
342 self.__close_fh() |
|
343 self.__cleanup() |
|
344 return |
|
345 atexit.register(arc_cleanup) |
|
346 |
|
347 # Open the pax archive for the package. |
|
348 try: |
|
349 self.__arc_tfile = ptf.PkgTarFile.open(mode=mode, |
|
350 fileobj=self.__arc_file, format=tf.PAX_FORMAT) |
|
351 except EnvironmentError, e: |
|
352 raise apx._convert_error(e) |
|
353 except Exception: |
|
354 # Likely not an archive or the archive is corrupt. |
|
355 raise InvalidArchive(self.__arc_name) |
|
356 |
|
357 self.__extract_offsets = {} |
|
358 if "r" in mode: |
|
359 # Opening the tarfile loaded the first member, which |
|
360 # should be the archive index file. |
|
361 member = self.__arc_tfile.firstmember |
|
362 if not member: |
|
363 # Archive is empty. |
|
364 raise InvalidArchive(self.__arc_name) |
|
365 |
|
366 if not member.name.startswith(self.__idx_pfx) or \ |
|
367 not member.name.endswith(self.__idx_sfx): |
|
368 return |
|
369 else: |
|
370 self.__idx_name = member.name |
|
371 |
|
372 comment = member.pax_headers.get("comment", "") |
|
373 if not comment.startswith("pkg5.archive.version."): |
|
374 return |
|
375 |
|
376 try: |
|
377 self.version = int(comment.rsplit(".", 1)[-1]) |
|
378 except (IndexError, ValueError): |
|
379 raise InvalidArchive(self.__arc_name) |
|
380 |
|
381 if self.version not in self.COMPATIBLE_VERSIONS: |
|
382 raise InvalidArchive(self.__arc_name) |
|
383 |
|
384 # Create a temporary file to extract the index to, |
|
385 # and then extract it from the archive. |
|
386 fobj, idxfn = self.__mkstemp() |
|
387 fobj.close() |
|
388 try: |
|
389 self.__arc_tfile.extract_to(member, |
|
390 path=self.__temp_dir, |
|
391 filename=os.path.basename(idxfn)) |
|
392 except tf.TarError: |
|
393 # Read error encountered. |
|
394 raise InvalidArchive(self.__arc_name) |
|
395 except EnvironmentError, e: |
|
396 raise apx._convert_error(e) |
|
397 |
|
398 # After extraction, the current archive file offset |
|
399 # is the base that will be used for all other |
|
400 # extractions. |
|
401 index_offset = self.__arc_tfile.offset |
|
402 |
|
403 # Load archive index. |
|
404 try: |
|
405 self.__index = ArchiveIndex(idxfn, mode="r", |
|
406 version=self.__idx_ver) |
|
407 for name, offset in self.__index.offsets(): |
|
408 self.__extract_offsets[name] = \ |
|
409 index_offset + offset |
|
410 except InvalidArchiveIndex: |
|
411 # Index is corrupt; rather than driving on |
|
412 # and failing later, bail now. |
|
413 os.unlink(idxfn) |
|
414 raise InvalidArchive(self.__arc_name) |
|
415 except EnvironmentError, e: |
|
416 raise apx._convert_error(e) |
|
417 |
|
418 elif "w" in mode: |
|
419 self.__pubs = {} |
|
420 |
|
421 # Force normalization of archive member mode and |
|
422 # ownership information during archive creation. |
|
423 def gettarinfo(*args, **kwargs): |
|
424 ti = ptf.PkgTarFile.gettarinfo(self.__arc_tfile, |
|
425 *args, **kwargs) |
|
426 if ti.isreg(): |
|
427 ti.mode = pkg.misc.PKG_FILE_MODE |
|
428 elif ti.isdir(): |
|
429 ti.mode = pkg.misc.PKG_DIR_MODE |
|
430 if ti.name == "pkg5.index.0.gz": |
|
431 ti.pax_headers["comment"] = \ |
|
432 "pkg5.archive.version.%d" % \ |
|
433 self.CURRENT_VERSION |
|
434 ti.uid = 0 |
|
435 ti.gid = 0 |
|
436 ti.uname = "root" |
|
437 ti.gname = "root" |
|
438 return ti |
|
439 self.__arc_tfile.gettarinfo = gettarinfo |
|
440 |
|
441 self.__idx_name = self.__idx_name % self.__idx_ver |
|
442 |
|
443 # Create a temporary file to write the index to, |
|
444 # and then create the index. |
|
445 fobj, idxfn = self.__mkstemp() |
|
446 fobj.close() |
|
447 self.__index = ArchiveIndex(idxfn, mode=arc_mode) |
|
448 |
|
449 # Used to determine what the default publisher will be |
|
450 # for the archive file at close(). |
|
451 self.__default_pub = "" |
|
452 |
|
453 # Used to keep track of which package files have already |
|
454 # been added to archive. |
|
455 self.__processed_pfiles = set() |
|
456 |
|
457 # Always create archives using current version. |
|
458 self.version = self.CURRENT_VERSION |
|
459 |
|
460 # Always add base publisher directory to start; tarfile |
|
461 # requires an actual filesystem object to do this, so |
|
462 # re-use an existing directory to do so. |
|
463 self.add("/", arcname="publisher") |
|
464 |
|
465 def __exit__(self, exc_type, exc_value, exc_tb): |
|
466 """Context handler that ensures archive is automatically closed |
|
467 in a non-error condition scenario. This enables 'with' usage. |
|
468 """ |
|
469 |
|
470 if exc_type or exc_value or exc_tb: |
|
471 # Only close file objects; don't actually write anything |
|
472 # out in an error condition. |
|
473 self.__close_fh() |
|
474 return |
|
475 |
|
476 # Close and/or write out archive as needed. |
|
477 self.close() |
|
478 |
|
479 def __find_extract_offsets(self): |
|
480 """Private helper method to find offsets for individual archive |
|
481 member extraction. |
|
482 """ |
|
483 |
|
484 if self.__extract_offsets: |
|
485 return |
|
486 |
|
487 # This causes the entire archive to be read, but is the only way |
|
488 # to find the offsets to extract everything. |
|
489 try: |
|
490 for member in self.__arc_tfile.getmembers(): |
|
491 self.__extract_offsets[member.name] = \ |
|
492 member.offset |
|
493 except tf.TarError: |
|
494 # Read error encountered. |
|
495 raise InvalidArchive(self.__arc_name) |
|
496 except EnvironmentError, e: |
|
497 raise apx._convert_error(e) |
|
498 |
|
499 def __mkdtemp(self): |
|
500 """Creates a temporary directory for use during archive |
|
501 operations, and return its absolute path. The temporary |
|
502 directory will be removed after the archive is closed. |
|
503 """ |
|
504 |
|
505 try: |
|
506 return tempfile.mkdtemp(dir=self.__temp_dir) |
|
507 except EnvironmentError, e: |
|
508 raise apx._convert_error(e) |
|
509 |
|
510 def __mkstemp(self): |
|
511 """Creates a temporary file for use during archive operations, |
|
512 and returns a file object for it and its absolute path. The |
|
513 temporary file will be removed after the archive is closed. |
|
514 """ |
|
515 try: |
|
516 fd, fn = tempfile.mkstemp(dir=self.__temp_dir) |
|
517 fobj = os.fdopen(fd, "wb") |
|
518 except EnvironmentError, e: |
|
519 raise apx._convert_error(e) |
|
520 return fobj, fn |
|
521 |
|
522 def add(self, pathname, arcname=None): |
|
523 """Queue the specified object for addition to the archive. |
|
524 The archive will be created and the object added to it when the |
|
525 close() method is called. The target object must not change |
|
526 after this method is called while the archive is open. The |
|
527 item being added must not already exist in the archive. |
|
528 |
|
529 'pathname' is an optional string specifying the absolute path |
|
530 of a file to add to the archive. The file may be a regular |
|
531 file, directory, symbolic link, or hard link. |
|
532 |
|
533 'arcname' is an optional string specifying an alternative name |
|
534 for the file in the archive. If not given, the full pathname |
|
535 provided will be used. |
|
536 """ |
|
537 |
|
538 assert not self.__closed and "w" in self.__mode |
|
539 tfile = self.__arc_tfile |
|
540 ti = tfile.gettarinfo(pathname, arcname=arcname) |
|
541 buf = ti.tobuf(tfile.format, tfile.encoding, tfile.errors) |
|
542 |
|
543 # Pre-calculate size of archive entry by determining where |
|
544 # in the archive the entry would be added. |
|
545 entry_sz = len(buf) |
|
546 blocks, rem = divmod(ti.size, tf.BLOCKSIZE) |
|
547 if rem > 0: |
|
548 blocks += 1 |
|
549 entry_sz += blocks * tf.BLOCKSIZE |
|
550 |
|
551 # Record name, offset, entry_size, size type for each file. |
|
552 self.__index.add(ti.name, self.__queue_offset, entry_sz, |
|
553 ti.size, ti.type) |
|
554 self.__queue_offset += entry_sz |
|
555 self.__queue.append((pathname, ti.name)) |
|
556 |
|
557 # Discard tarinfo; it would be more efficient to keep these in |
|
558 # memory, but at a significant memory footprint cost. |
|
559 ti.tarfile = None |
|
560 del ti |
|
561 |
|
562 def __add_publisher_files(self, root, file_dir, hashes, fpath=None, |
|
563 repo=None): |
|
564 """Private helper function for adding package files.""" |
|
565 |
|
566 if file_dir not in self.__processed_pfiles: |
|
567 # Directory entry needs to be added |
|
568 # for package files. |
|
569 self.add(root, arcname=file_dir) |
|
570 self.__processed_pfiles.add(file_dir) |
|
571 |
|
572 for fhash in hashes: |
|
573 hash_dir = os.path.join(file_dir, fhash[:2]) |
|
574 if hash_dir not in self.__processed_pfiles: |
|
575 # Directory entry needs to be added |
|
576 # for hash directory. |
|
577 self.add(root, arcname=hash_dir) |
|
578 self.__processed_pfiles.add(hash_dir) |
|
579 |
|
580 hash_fname = os.path.join(hash_dir, fhash) |
|
581 if hash_fname in self.__processed_pfiles: |
|
582 # Already added for a different |
|
583 # package. |
|
584 continue |
|
585 |
|
586 if repo: |
|
587 src = repo.file(fhash) |
|
588 else: |
|
589 src = os.path.join(fpath, fhash) |
|
590 self.add(src, arcname=hash_fname) |
|
591 |
|
592 # A bit expensive potentially in terms of |
|
593 # memory usage, but necessary to prevent |
|
594 # duplicate archive entries. |
|
595 self.__processed_pfiles.add(hash_fname) |
|
596 |
|
597 def __add_package(self, pfmri, mpath, fpath=None, repo=None): |
|
598 """Private helper function that queues a package for addition to |
|
599 the archive. |
|
600 |
|
601 'mpath' is the absolute path of the package manifest file. |
|
602 |
|
603 'fpath' is an optional directory containing the package files |
|
604 stored by hash. |
|
605 |
|
606 'repo' is an optional Repository object to use to retrieve the |
|
607 data for the package to be added to the archive. |
|
608 |
|
609 'fpath' or 'repo' must be provided. |
|
610 """ |
|
611 |
|
612 assert not self.__closed and "w" in self.__mode |
|
613 assert mpath |
|
614 assert not (fpath and repo) |
|
615 assert fpath or repo |
|
616 |
|
617 if not self.__default_pub: |
|
618 self.__default_pub = pfmri.publisher |
|
619 |
|
620 m = pkg.manifest.Manifest(pfmri) |
|
621 m.set_content(pathname=mpath) |
|
622 |
|
623 # Throughout this function, the archive root directory is used |
|
624 # as a template to add other directories that should be present |
|
625 # in the archive. This is necessary as the tarfile class does |
|
626 # not support adding arbitrary archive entries without a real |
|
627 # filesystem object as a source. |
|
628 root = os.path.dirname(self.__arc_name) |
|
629 pub_dir = os.path.join("publisher", pfmri.publisher) |
|
630 pkg_dir = os.path.join(pub_dir, "pkg") |
|
631 for d in pub_dir, pkg_dir: |
|
632 if d not in self.__processed_pfiles: |
|
633 self.add(root, arcname=d) |
|
634 self.__processed_pfiles.add(d) |
|
635 |
|
636 # After manifest has been loaded, assume it's ok to queue the |
|
637 # manifest itself for addition to the archive. |
|
638 arcname = os.path.join(pkg_dir, pfmri.get_dir_path()) |
|
639 |
|
640 # Entry may need to be added for manifest directory. |
|
641 man_dir = os.path.dirname(arcname) |
|
642 if man_dir not in self.__processed_pfiles: |
|
643 self.add(root, arcname=man_dir) |
|
644 self.__processed_pfiles.add(man_dir) |
|
645 |
|
646 # Entry needs to be added for manifest file. |
|
647 self.add(mpath, arcname=arcname) |
|
648 |
|
649 # Now add any files to the archive for every action that has a |
|
650 # payload. (That payload can consist of multiple files.) |
|
651 file_dir = os.path.join(pub_dir, "file") |
|
652 for a in m.gen_actions(): |
|
653 if not a.has_payload or not a.hash: |
|
654 # Nothing to archive. |
|
655 continue |
|
656 |
|
657 payloads = set([a.hash]) |
|
658 |
|
659 # Signature actions require special handling. |
|
660 if a.name == "signature": |
|
661 payloads.update(a.attrs.get("chain", |
|
662 "").split()) |
|
663 |
|
664 if repo: |
|
665 # This bit of logic only possible if |
|
666 # package source is a repository. |
|
667 pub = self.__pubs.get(pfmri.publisher, |
|
668 None) |
|
669 if not pub: |
|
670 self.__pubs[pfmri.publisher] = \ |
|
671 pub = repo.get_publisher( |
|
672 pfmri.publisher) |
|
673 assert pub |
|
674 |
|
675 payloads.update(pub.signing_ca_certs) |
|
676 payloads.update(pub.intermediate_certs) |
|
677 |
|
678 if not payloads: |
|
679 # Nothing more to do. |
|
680 continue |
|
681 |
|
682 self.__add_publisher_files(root, file_dir, payloads, |
|
683 fpath=fpath, repo=repo) |
|
684 |
|
685 def add_package(self, pfmri, mpath, fpath): |
|
686 """Queues the specified package for addition to the archive. |
|
687 The archive will be created and the package added to it when |
|
688 the close() method is called. The package contents must not |
|
689 change after this method is called while the archive is open. |
|
690 Please note that, for signed packages, signing certificates |
|
691 used by the publisher are not automatically added to the |
|
692 archive. |
|
693 |
|
694 'pfmri' is the FMRI string or object identifying the package to |
|
695 add. |
|
696 |
|
697 'mpath' is the absolute path of the package manifest file. |
|
698 |
|
699 'fpath' is the directory containing the package files stored |
|
700 by hash. |
|
701 """ |
|
702 |
|
703 assert pfmri and mpath and fpath |
|
704 if isinstance(pfmri, basestring): |
|
705 pfmri = pkg.fmri.PkgFmri(pfmri) |
|
706 assert pfmri.publisher |
|
707 self.__add_package(pfmri, mpath, fpath=fpath) |
|
708 |
|
709 def add_signing_certs(self, pub, hashes, ca): |
|
710 """Queues the specified publisher certs for addition to the |
|
711 archive. The archive will be created and the certs added to it |
|
712 when the close() method is called. The cert contents must not |
|
713 change after this method is called while the archive is open. |
|
714 |
|
715 'pub' is the prefix of the publisher to store the package |
|
716 files for. |
|
717 |
|
718 'hashes' is the list of certificate hash files to store. |
|
719 (The certificate files must be in the same compressed format |
|
720 that the Repository class stores them in.) |
|
721 |
|
722 'ca' is a boolean indicating whether the certs are added as |
|
723 as CA certificates or intermediate certificates. |
|
724 """ |
|
725 |
|
726 root = os.path.dirname(self.__arc_name) |
|
727 pub_dir = os.path.join("publisher", pub) |
|
728 file_dir = os.path.join(pub_dir, "file") |
|
729 |
|
730 pubobj = self.__pubs.get(pub, None) |
|
731 if not pubobj: |
|
732 self.__pubs[pub] = pubobj = \ |
|
733 pkg.client.publisher.Publisher(pub) |
|
734 |
|
735 for fname in hashes: |
|
736 hsh = os.path.basename(fname) |
|
737 self.__add_publisher_files(root, file_dir, [hsh], |
|
738 fpath=os.path.dirname(fname)) |
|
739 if ca: |
|
740 pubobj.signing_ca_certs.append(hsh) |
|
741 else: |
|
742 pubobj.intermediate_certs.append(hsh) |
|
743 |
|
744 def add_repo_package(self, pfmri, repo): |
|
745 """Queues the specified package in a repository for addition to |
|
746 the archive. The archive will be created and the package added |
|
747 to it when the close() method is called. The package contents |
|
748 must not change after this method is called while the archive is |
|
749 open. |
|
750 |
|
751 'pfmri' is the FMRI string or object identifying the package to |
|
752 add. |
|
753 |
|
754 'repo' is the Repository object to use to retrieve the data for |
|
755 the package to be added to the archive. |
|
756 """ |
|
757 |
|
758 assert pfmri and repo |
|
759 if isinstance(pfmri, basestring): |
|
760 pfmri = pkg.fmri.PkgFmri(pfmri) |
|
761 assert pfmri.publisher |
|
762 self.__add_package(pfmri, repo.manifest(pfmri), repo=repo) |
|
763 |
|
764 def extract_catalog1(self, part, path, pub=None): |
|
765 """Extract the named v1 catalog part to the specified directory. |
|
766 |
|
767 'part' is the name of the catalog file part. |
|
768 |
|
769 'path' is the absolute path of the directory to extract the |
|
770 file to. It will be created automatically if it does not |
|
771 exist. |
|
772 |
|
773 'pub' is an optional publisher prefix. If not provided, the |
|
774 first publisher catalog found in the archive will be used. |
|
775 """ |
|
776 |
|
777 # If the extraction index doesn't exist, scan the |
|
778 # complete archive and build one. |
|
779 self.__find_extract_offsets() |
|
780 |
|
781 pubs = [ |
|
782 p for p in self.get_publishers() |
|
783 if not pub or p.prefix == pub |
|
784 ] |
|
785 if not pubs: |
|
786 raise UnknownArchiveFiles(self.__arc_name, [part]) |
|
787 |
|
788 if not pub: |
|
789 # Default to first known publisher. |
|
790 pub = pubs[0].prefix |
|
791 |
|
792 # Expected locations in archive for various metadata. |
|
793 # A trailing slash is appended so that archive entry |
|
794 # comparisons skip the entries for the directory. |
|
795 pubpath = os.path.join("publisher", pub) + os.path.sep |
|
796 catpath = os.path.join(pubpath, "catalog") + os.path.sep |
|
797 partpath = os.path.join(catpath, part) |
|
798 |
|
799 if pub in self.__catalogs: |
|
800 # Catalog file requested for this publisher before. |
|
801 croot = self.__catalogs[pub] |
|
802 if croot: |
|
803 # Catalog data is cached because it was |
|
804 # generated on demand, so just copy it |
|
805 # from there to the destination. |
|
806 src = os.path.join(croot, part) |
|
807 if not os.path.exists(src): |
|
808 raise UnknownArchiveFiles( |
|
809 self.__arc_name, [partpath]) |
|
810 |
|
811 try: |
|
812 pkg.portable.copyfile( |
|
813 os.path.join(croot, part), |
|
814 os.path.join(path, part)) |
|
815 except EnvironmentError, e: |
|
816 raise apx._convert_error(e) |
|
817 else: |
|
818 # Use default extraction logic. |
|
819 self.extract_to(partpath, path, filename=part) |
|
820 return |
|
821 |
|
822 # Determine whether any catalog files are present for this |
|
823 # publisher in the archive. |
|
824 for name in self.__extract_offsets: |
|
825 if name.startswith(catpath): |
|
826 # Any catalog file at all means this publisher |
|
827 # should be marked as being known to have one |
|
828 # and then the request passed on to extract_to. |
|
829 self.__catalogs[pub] = None |
|
830 return self.extract_to(partpath, path, |
|
831 filename=part) |
|
832 |
|
833 # No catalog data found for publisher; construct a catalog |
|
834 # in memory based on packages found for publisher. |
|
835 cat = pkg.catalog.Catalog(batch_mode=True, sign=False) |
|
836 manpath = os.path.join(pubpath, "pkg") + os.path.sep |
|
837 for name in self.__extract_offsets: |
|
838 if name.startswith(manpath) and name.count("/") == 4: |
|
839 ignored, stem, ver = name.rsplit("/", 2) |
|
840 stem = urllib.unquote(stem) |
|
841 ver = urllib.unquote(ver) |
|
842 pfmri = pkg.fmri.PkgFmri("%s@%s" % (stem, ver), |
|
843 publisher=pub) |
|
844 |
|
845 fobj = self.get_file(name) |
|
846 m = pkg.manifest.Manifest(pfmri=pfmri) |
|
847 m.set_content(content=fobj.read(), |
|
848 signatures=True) |
|
849 cat.add_package(pfmri, manifest=m) |
|
850 |
|
851 # Store catalog in a temporary directory and mark publisher |
|
852 # as having catalog data cached. |
|
853 croot = self.__mkdtemp() |
|
854 cat.meta_root = croot |
|
855 cat.batch_mode = False |
|
856 cat.finalize() |
|
857 cat.save() |
|
858 self.__catalogs[pub] = croot |
|
859 |
|
860 # Finally, copy requested file to destination. |
|
861 try: |
|
862 pkg.portable.copyfile(os.path.join(croot, part), |
|
863 os.path.join(path, part)) |
|
864 except EnvironmentError, e: |
|
865 raise apx._convert_error(e) |
|
866 |
|
867 def extract_package_files(self, hashes, path, pub=None): |
|
868 """Extract one or more package files from the archive. |
|
869 |
|
870 'hashes' is a list of the files to extract named by their hash. |
|
871 |
|
872 'path' is the absolute path of the directory to extract the |
|
873 files to. It will be created automatically if it does not |
|
874 exist. |
|
875 |
|
876 'pub' is the prefix (name) of the publisher that the package |
|
877 files are associated with. If not provided, the first file |
|
878 named after the given hash found in the archive will be used. |
|
879 (This will be noticeably slower depending on the size of the |
|
880 archive.) |
|
881 """ |
|
882 |
|
883 assert not self.__closed and "r" in self.__mode |
|
884 assert hashes |
|
885 |
|
886 # If the extraction index doesn't exist, scan the complete |
|
887 # archive and build one. |
|
888 self.__find_extract_offsets() |
|
889 |
|
890 if not pub: |
|
891 # Scan extract offsets index for the first instance of |
|
892 # any package file seen for each hash and extract the |
|
893 # file as each is found. |
|
894 hashes = set(hashes) |
|
895 |
|
896 for name in self.__extract_offsets: |
|
897 for fhash in hashes: |
|
898 hash_fname = os.path.join("file", |
|
899 fhash[:2], fhash) |
|
900 if name.endswith(hash_fname): |
|
901 self.extract_to(name, path, |
|
902 filename=fhash) |
|
903 hashes.discard(fhash) |
|
904 break |
|
905 if not hashes: |
|
906 break |
|
907 |
|
908 if hashes: |
|
909 # Any remaining hashes are for package files |
|
910 # that couldn't be found. |
|
911 raise UnknownArchiveFiles(self.__arc_name, |
|
912 hashes) |
|
913 return |
|
914 |
|
915 for fhash in hashes: |
|
916 arcname = os.path.join("publisher", pub, "file", |
|
917 fhash[:2], fhash) |
|
918 self.extract_to(arcname, path, filename=fhash) |
|
919 |
|
920 def extract_package_manifest(self, pfmri, path, filename=""): |
|
921 """Extract a package manifest from the archive. |
|
922 |
|
923 'pfmri' is the FMRI string or object identifying the package |
|
924 manifest to extract. |
|
925 |
|
926 'path' is the absolute path of the directory to extract the |
|
927 manifest to. It will be created automatically if it does not |
|
928 exist. |
|
929 |
|
930 'filename' is an optional name to use for the extracted file. |
|
931 If not provided, the default behaviour is to create a directory |
|
932 named after the package stem in 'path' and a file named after |
|
933 the version in that directory; both components will be URI |
|
934 encoded. |
|
935 """ |
|
936 |
|
937 assert not self.__closed and "r" in self.__mode |
|
938 assert pfmri and path |
|
939 if isinstance(pfmri, basestring): |
|
940 pfmri = pkg.fmri.PkgFmri(pfmri) |
|
941 assert pfmri.publisher |
|
942 |
|
943 if not filename: |
|
944 filename = pfmri.get_dir_path() |
|
945 |
|
946 arcname = os.path.join("publisher", pfmri.publisher, "pkg", |
|
947 pfmri.get_dir_path()) |
|
948 try: |
|
949 self.extract_to(arcname, path, filename=filename) |
|
950 except UnknownArchiveFiles: |
|
951 raise UnknownPackageManifest(self.__arc_name, pfmri) |
|
952 |
|
953 def extract_to(self, src, path, filename=""): |
|
954 """Extract a member from the archive. |
|
955 |
|
956 'src' is the pathname of the archive file to extract. |
|
957 |
|
958 'path' is the absolute path of the directory to extract the file |
|
959 to. |
|
960 |
|
961 'filename' is an optional string indicating the name to use for |
|
962 the extracted file. If not provided, the full member name in |
|
963 the archive will be used. |
|
964 """ |
|
965 |
|
966 assert not self.__closed and "r" in self.__mode |
|
967 |
|
968 # Get the offset in the archive for the given file, and then |
|
969 # seek to it. |
|
970 offset = self.__extract_offsets.get(src, None) |
|
971 tfile = self.__arc_tfile |
|
972 if offset is not None: |
|
973 # Prepare the tarfile object for extraction by telling |
|
974 # it where to look for the file. |
|
975 self.__arc_file.seek(offset) |
|
976 tfile.offset = offset |
|
977 |
|
978 # Get the tarinfo object needed to extract the file. |
|
979 try: |
|
980 member = tf.TarInfo.fromtarfile(tfile) |
|
981 except tf.TarError: |
|
982 # Read error encountered. |
|
983 raise InvalidArchive(self.__arc_name) |
|
984 except EnvironmentError, e: |
|
985 raise apx._convert_error(e) |
|
986 |
|
987 if member.name != src: |
|
988 # Index must be invalid or tarfile has gone off |
|
989 # the rails trying to read the archive. |
|
990 raise InvalidArchive(self.__arc_name) |
|
991 |
|
992 elif self.__extract_offsets: |
|
993 # Assume there is no such archive member if extract |
|
994 # offsets are known, but the item can't be found. |
|
995 raise UnknownArchiveFiles(self.__arc_name, [src]) |
|
996 else: |
|
997 # No archive index; fallback to retrieval by name. |
|
998 member = src |
|
999 |
|
1000 # Extract the file to the specified location. |
|
1001 try: |
|
1002 self.__arc_tfile.extract_to(member, path=path, |
|
1003 filename=filename) |
|
1004 except KeyError: |
|
1005 raise UnknownArchiveFiles(self.__arc_name, [src]) |
|
1006 except tf.TarError: |
|
1007 # Read error encountered. |
|
1008 raise InvalidArchive(self.__arc_name) |
|
1009 except EnvironmentError, e: |
|
1010 raise apx._convert_error(e) |
|
1011 |
|
1012 if not isinstance(member, tf.TarInfo): |
|
1013 # Nothing more to do. |
|
1014 return |
|
1015 |
|
1016 # If possible, validate the size of the extracted object. |
|
1017 try: |
|
1018 if not filename: |
|
1019 filename = member.name |
|
1020 dest = os.path.join(path, filename) |
|
1021 if os.stat(dest).st_size != member.size: |
|
1022 raise CorruptArchiveFiles(self.__arc_name, |
|
1023 [src]) |
|
1024 except EnvironmentError, e: |
|
1025 raise apx._convert_error(e) |
|
1026 |
|
1027 def get_file(self, src): |
|
1028 """Returns an archive member as a file object. If the matching |
|
1029 member is a regular file, a file-like object will be returned. |
|
1030 If it is a link, a file-like object is constructed from the |
|
1031 link's target. In all other cases, None will be returned. The |
|
1032 file-like object is read-only and provides methods: read(), |
|
1033 readline(), readlines(), seek() and tell(). The returned object |
|
1034 must be closed before the archive is, and must not be used after |
|
1035 the archive is closed. |
|
1036 |
|
1037 'src' is the pathname of the archive file to return. |
|
1038 """ |
|
1039 |
|
1040 assert not self.__closed and "r" in self.__mode |
|
1041 |
|
1042 # Get the offset in the archive for the given file, and then |
|
1043 # seek to it. |
|
1044 offset = self.__extract_offsets.get(src, None) |
|
1045 tfile = self.__arc_tfile |
|
1046 if offset is not None: |
|
1047 # Prepare the tarfile object for extraction by telling |
|
1048 # it where to look for the file. |
|
1049 self.__arc_file.seek(offset) |
|
1050 tfile.offset = offset |
|
1051 |
|
1052 # Get the tarinfo object needed to extract the file. |
|
1053 member = tf.TarInfo.fromtarfile(tfile) |
|
1054 elif self.__extract_offsets: |
|
1055 # Assume there is no such archive member if extract |
|
1056 # offsets are known, but the item can't be found. |
|
1057 raise UnknownArchiveFiles(self.__arc_name, [src]) |
|
1058 else: |
|
1059 # No archive index; fallback to retrieval by name. |
|
1060 member = src |
|
1061 |
|
1062 # Finally, return the object for the matching archive member. |
|
1063 try: |
|
1064 return tfile.extractfile(member) |
|
1065 except KeyError: |
|
1066 raise UnknownArchiveFiles(self.__arc_name, [src]) |
|
1067 |
|
1068 def get_package_file(self, fhash, pub=None): |
|
1069 """Returns the first package file matching the given hash as a |
|
1070 file-like object. The file-like object is read-only and provides |
|
1071 methods: read(), readline(), readlines(), seek() and tell(). |
|
1072 The returned object must be closed before the archive is, and |
|
1073 must not be used after the archive is closed. |
|
1074 |
|
1075 'fhash' is the hash name of the file to return. |
|
1076 |
|
1077 'pub' is the prefix (name) of the publisher that the package |
|
1078 files are associated with. If not provided, the first file |
|
1079 named after the given hash found in the archive will be used. |
|
1080 (This will be noticeably slower depending on the size of the |
|
1081 archive.) |
|
1082 """ |
|
1083 |
|
1084 assert not self.__closed and "r" in self.__mode |
|
1085 |
|
1086 if not self.__extract_offsets: |
|
1087 # If the extraction index doesn't exist, scan the |
|
1088 # complete archive and build one. |
|
1089 self.__find_extract_offsets() |
|
1090 |
|
1091 if not pub: |
|
1092 # Scan extract offsets index for the first instance of |
|
1093 # any package file seen for the hash and extract it. |
|
1094 hash_fname = os.path.join("file", fhash[:2], fhash) |
|
1095 for name in self.__extract_offsets: |
|
1096 if name.endswith(hash_fname): |
|
1097 return self.get_file(name) |
|
1098 raise UnknownArchiveFiles(self.__arc_name, [fhash]) |
|
1099 |
|
1100 return self.get_file(os.path.join("publisher", pub, "file", |
|
1101 fhash[:2], fhash)) |
|
1102 |
|
1103 def get_package_manifest(self, pfmri, raw=False): |
|
1104 """Returns a package manifest from the archive. |
|
1105 |
|
1106 'pfmri' is the FMRI string or object identifying the package |
|
1107 manifest to extract. |
|
1108 |
|
1109 'raw' is an optional boolean indicating whether the raw |
|
1110 content of the Manifest should be returned. If True, |
|
1111 a file-like object containing the content of the manifest. |
|
1112 If False, a Manifest object will be returned. |
|
1113 """ |
|
1114 |
|
1115 assert not self.__closed and "r" in self.__mode |
|
1116 assert pfmri |
|
1117 if isinstance(pfmri, basestring): |
|
1118 pfmri = pkg.fmri.PkgFmri(pfmri) |
|
1119 assert pfmri.publisher |
|
1120 |
|
1121 arcname = os.path.join("publisher", pfmri.publisher, "pkg", |
|
1122 pfmri.get_dir_path()) |
|
1123 |
|
1124 try: |
|
1125 fobj = self.get_file(arcname) |
|
1126 except UnknownArchiveFiles: |
|
1127 raise UnknownPackageManifest(self.__arc_name, pfmri) |
|
1128 |
|
1129 if raw: |
|
1130 return fobj |
|
1131 |
|
1132 m = pkg.manifest.Manifest(pfmri=pfmri) |
|
1133 m.set_content(content=fobj.read(), signatures=True) |
|
1134 return m |
|
1135 |
|
1136 def get_publishers(self): |
|
1137 """Return a list of publisher objects for all publishers used |
|
1138 in the archive.""" |
|
1139 |
|
1140 if self.__pubs: |
|
1141 return self.__pubs.values() |
|
1142 |
|
1143 # If the extraction index doesn't exist, scan the complete |
|
1144 # archive and build one. |
|
1145 self.__find_extract_offsets() |
|
1146 |
|
1147 # Search through offset index to find publishers |
|
1148 # in use. |
|
1149 self.__pubs = {} |
|
1150 for name in self.__extract_offsets: |
|
1151 if name.count("/") == 1 and \ |
|
1152 name.startswith("publisher/"): |
|
1153 ignored, pfx = name.split("/", 1) |
|
1154 |
|
1155 # See if this publisher has a .p5i file in the |
|
1156 # archive (needed for signed packages). |
|
1157 p5iname = os.path.join("publisher", pfx, |
|
1158 "pub.p5i") |
|
1159 try: |
|
1160 fobj = self.get_file(p5iname) |
|
1161 except UnknownArchiveFiles: |
|
1162 # No p5i; that's ok. |
|
1163 pub = pkg.client.publisher.Publisher( |
|
1164 pfx) |
|
1165 else: |
|
1166 pubs = pkg.p5i.parse(fileobj=fobj) |
|
1167 assert len(pubs) == 1 |
|
1168 pub = pubs[0][0] |
|
1169 assert pub |
|
1170 |
|
1171 self.__pubs[pfx] = pub |
|
1172 |
|
1173 return self.__pubs.values() |
|
1174 |
|
1175 def __cleanup(self): |
|
1176 """Private helper method to cleanup temporary files.""" |
|
1177 |
|
1178 try: |
|
1179 if os.path.exists(self.__temp_dir): |
|
1180 shutil.rmtree(self.__temp_dir) |
|
1181 except EnvironmentError, e: |
|
1182 raise apx._convert_error(e) |
|
1183 |
|
1184 def __close_fh(self): |
|
1185 """Private helper method to close filehandles.""" |
|
1186 |
|
1187 # Some archives may not have an index. |
|
1188 if self.__index: |
|
1189 self.__index.close() |
|
1190 self.__index = None |
|
1191 |
|
1192 # A read error during archive load may cause these to have |
|
1193 # never been set. |
|
1194 if self.__arc_tfile: |
|
1195 self.__arc_tfile.close() |
|
1196 self.__arc_tfile = None |
|
1197 |
|
1198 if self.__arc_file: |
|
1199 self.__arc_file.close() |
|
1200 self.__arc_file = None |
|
1201 self.__closed = True |
|
1202 |
|
1203 def close(self, progtrack=None): |
|
1204 """If mode is 'r', this will close the archive file. If mode is |
|
1205 'w', this will write all queued files to the archive and close |
|
1206 it. Further operations on the archive are not possible after |
|
1207 calling this function.""" |
|
1208 |
|
1209 assert not self.__closed |
|
1210 |
|
1211 if "w" not in self.__mode: |
|
1212 self.__close_fh() |
|
1213 self.__cleanup() |
|
1214 return |
|
1215 |
|
1216 # Add the standard pkg5.repository file before closing the |
|
1217 # index. |
|
1218 fobj, fname = self.__mkstemp() |
|
1219 fobj.write("[CONFIGURATION]\nversion = 4\n\n" |
|
1220 "[publisher]\nprefix = %s\n\n" |
|
1221 "[repository]\nversion = 4\n" % self.__default_pub) |
|
1222 fobj.close() |
|
1223 self.add(fname, arcname="pkg5.repository") |
|
1224 |
|
1225 # If any publisher objects were cached, then there were |
|
1226 # signed packages present, and p5i information for each |
|
1227 # must be added to the archive so that the client can |
|
1228 # handle signing ca and intermediate certs. |
|
1229 for pub in self.__pubs.values(): |
|
1230 # A new publisher object is created with a copy of only |
|
1231 # the information that's needed for the archive. |
|
1232 npub = pkg.client.publisher.Publisher(pub.prefix, |
|
1233 alias=pub.alias, ca_certs=pub.signing_ca_certs, |
|
1234 intermediate_certs=pub.intermediate_certs, |
|
1235 revoked_ca_certs=pub.revoked_ca_certs, |
|
1236 approved_ca_certs=pub.approved_ca_certs) |
|
1237 |
|
1238 # Create a p5i file. |
|
1239 fobj, fn = self.__mkstemp() |
|
1240 pkg.p5i.write(fobj, [npub]) |
|
1241 fobj.close() |
|
1242 |
|
1243 # Queue the p5i file for addition to the archive. |
|
1244 arcname = os.path.join("publisher", npub.prefix, |
|
1245 "pub.p5i") |
|
1246 self.add(fn, arcname=arcname) |
|
1247 |
|
1248 # Close the index; no more entries can be added. |
|
1249 self.__index.close() |
|
1250 |
|
1251 # If a tracker was provided, setup a progress goal. |
|
1252 idxbytes = 0 |
|
1253 if progtrack: |
|
1254 nfiles = len(self.__queue) |
|
1255 nbytes = self.__queue_offset |
|
1256 try: |
|
1257 fs = os.stat(self.__index.pathname) |
|
1258 nfiles += 1 |
|
1259 idxbytes = fs.st_size |
|
1260 nbytes += idxbytes |
|
1261 except EnvironmentError, e: |
|
1262 raise apx._convert_error(e) |
|
1263 |
|
1264 progtrack.archive_set_goal( |
|
1265 os.path.basename(self.__arc_name), nfiles, |
|
1266 nbytes) |
|
1267 |
|
1268 # Add the index file to the archive as the first file; it will |
|
1269 # automatically be marked with a comment identifying the index |
|
1270 # version. |
|
1271 tfile = self.__arc_tfile |
|
1272 tfile.add(self.__index.pathname, arcname=self.__idx_name) |
|
1273 if progtrack: |
|
1274 progtrack.archive_add_progress(1, idxbytes) |
|
1275 self.__index = None |
|
1276 |
|
1277 # Add all queued files to the archive. |
|
1278 while self.__queue: |
|
1279 src, arcname = self.__queue.popleft() |
|
1280 |
|
1281 start_offset = tfile.offset |
|
1282 tfile.add(src, arcname=arcname, recursive=False) |
|
1283 |
|
1284 # tarfile caches member information for every item |
|
1285 # added by default, which provides fast access to the |
|
1286 # archive contents after generation, but isn't needed |
|
1287 # here (and uses a significant amount of memory). |
|
1288 # Plus popping it off the stack here allows use of |
|
1289 # the object's info to provide progress updates. |
|
1290 ti = tfile.members.pop() |
|
1291 if progtrack: |
|
1292 progtrack.archive_add_progress(1, |
|
1293 tfile.offset - start_offset) |
|
1294 ti.tarfile = None |
|
1295 del ti |
|
1296 |
|
1297 # Cleanup temporary files. |
|
1298 self.__cleanup() |
|
1299 |
|
1300 # Archive created; success! |
|
1301 if progtrack: |
|
1302 progtrack.archive_done() |
|
1303 self.__close_fh() |
|
1304 |
|
1305 @property |
|
1306 def pathname(self): |
|
1307 """The absolute path of the archive file.""" |
|
1308 return self.__arc_name |