Package CedarBackup2 :: Package writers :: Module dvdwriter
[hide private]
[frames] | no frames]

Source Code for Module CedarBackup2.writers.dvdwriter

  1  # -*- coding: iso-8859-1 -*- 
  2  # vim: set ft=python ts=3 sw=3 expandtab: 
  3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  4  # 
  5  #              C E D A R 
  6  #          S O L U T I O N S       "Software done right." 
  7  #           S O F T W A R E 
  8  # 
  9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 10  # 
 11  # Copyright (c) 2007 Kenneth J. Pronovici. 
 12  # All rights reserved. 
 13  # 
 14  # This program is free software; you can redistribute it and/or 
 15  # modify it under the terms of the GNU General Public License, 
 16  # Version 2, as published by the Free Software Foundation. 
 17  # 
 18  # This program is distributed in the hope that it will be useful, 
 19  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 21  # 
 22  # Copies of the GNU General Public License are available from 
 23  # the Free Software Foundation website, http://www.gnu.org/. 
 24  # 
 25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 26  # 
 27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org> 
 28  # Language : Python (>= 2.3) 
 29  # Project  : Cedar Backup, release 2 
 30  # Revision : $Id: dvdwriter.py 1193 2007-03-30 01:55:18Z pronovic $ 
 31  # Purpose  : Provides functionality related to DVD writer devices. 
 32  # 
 33  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 34   
 35  ######################################################################## 
 36  # Module documentation 
 37  ######################################################################## 
 38   
 39  """ 
 40  Provides functionality related to DVD writer devices. 
 41   
 42  @sort: MediaDefinition, DvdWriter, MEDIA_DVDPLUSR, MEDIA_DVDPLUSRW 
 43   
 44  @var MEDIA_DVDPLUSR: Constant representing DVD+R media. 
 45  @var MEDIA_DVDPLUSRW: Constant representing DVD+RW media. 
 46   
 47  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
 48  @author: Dmitry Rutsky <rutsky@inbox.ru> 
 49  """ 
 50   
 51  ######################################################################## 
 52  # Imported modules 
 53  ######################################################################## 
 54   
 55  # System modules 
 56  import os 
 57  import re 
 58  import logging 
 59  import tempfile 
 60   
 61  # Cedar Backup modules 
 62  from CedarBackup2.filesystem import BackupFileList 
 63  from CedarBackup2.writers.util import IsoImage 
 64  from CedarBackup2.util import resolveCommand, executeCommand 
 65  from CedarBackup2.util import convertSize, displayBytes, encodePath 
 66  from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_GBYTES 
 67  from CedarBackup2.writers.util import validateDevice, validateDriveSpeed 
 68   
 69   
 70  ######################################################################## 
 71  # Module-wide constants and variables 
 72  ######################################################################## 
 73   
 74  logger = logging.getLogger("CedarBackup2.log.writers.dvdwriter") 
 75   
 76  MEDIA_DVDPLUSR  = 1 
 77  MEDIA_DVDPLUSRW = 2 
 78   
 79  GROWISOFS_COMMAND = [ "growisofs", ] 
 80  EJECT_COMMAND     = [ "eject", ] 
 81   
 82   
 83  ######################################################################## 
 84  # MediaDefinition class definition 
 85  ######################################################################## 
 86   
87 -class MediaDefinition(object):
88 89 """ 90 Class encapsulating information about DVD media definitions. 91 92 The following media types are accepted: 93 94 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 95 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 96 97 Note that the capacity attribute returns capacity in terms of ISO sectors 98 (C{util.ISO_SECTOR_SIZE)}. This is for compatibility with the CD writer 99 functionality. 100 101 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 102 of 1024*1024*1024 bytes per gigabyte. 103 104 @sort: __init__, mediaType, rewritable, capacity 105 """ 106
107 - def __init__(self, mediaType):
108 """ 109 Creates a media definition for the indicated media type. 110 @param mediaType: Type of the media, as discussed above. 111 @raise ValueError: If the media type is unknown or unsupported. 112 """ 113 self._mediaType = None 114 self._rewritable = False 115 self._capacity = 0.0 116 self._setValues(mediaType)
117
118 - def _setValues(self, mediaType):
119 """ 120 Sets values based on media type. 121 @param mediaType: Type of the media, as discussed above. 122 @raise ValueError: If the media type is unknown or unsupported. 123 """ 124 if mediaType not in [MEDIA_DVDPLUSR, MEDIA_DVDPLUSRW, ]: 125 raise ValueError("Invalid media type %d." % mediaType) 126 self._mediaType = mediaType 127 if self._mediaType == MEDIA_DVDPLUSR: 128 self._rewritable = False 129 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB 130 elif self._mediaType == MEDIA_DVDPLUSRW: 131 self._rewritable = True 132 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB
133
134 - def _getMediaType(self):
135 """ 136 Property target used to get the media type value. 137 """ 138 return self._mediaType
139
140 - def _getRewritable(self):
141 """ 142 Property target used to get the rewritable flag value. 143 """ 144 return self._rewritable
145
146 - def _getCapacity(self):
147 """ 148 Property target used to get the capacity value. 149 """ 150 return self._capacity
151 152 mediaType = property(_getMediaType, None, None, doc="Configured media type.") 153 rewritable = property(_getRewritable, None, None, doc="Boolean indicating whether the media is rewritable.") 154 capacity = property(_getCapacity, None, None, doc="Total capacity of media in 2048-byte sectors.")
155 156 157 ######################################################################## 158 # MediaCapacity class definition 159 ######################################################################## 160
161 -class MediaCapacity(object):
162 163 """ 164 Class encapsulating information about DVD media capacity. 165 166 Space used and space available do not include any information about media 167 lead-in or other overhead. 168 169 @sort: __init__, bytesUsed, bytesAvailable 170 """ 171
172 - def __init__(self, bytesUsed, bytesAvailable):
173 """ 174 Initializes a capacity object. 175 @raise ValueError: If the bytes used and available values are not floats. 176 """ 177 self._bytesUsed = float(bytesUsed) 178 self._bytesAvailable = float(bytesAvailable)
179
180 - def _getBytesUsed(self):
181 """ 182 Property target used to get the bytes-used value. 183 """ 184 return self._bytesUsed
185
186 - def _getBytesAvailable(self):
187 """ 188 Property target available to get the bytes-available value. 189 """ 190 return self._bytesAvailable
191 192 bytesUsed = property(_getBytesUsed, None, None, doc="Space used on disc, in bytes.") 193 bytesAvailable = property(_getBytesAvailable, None, None, doc="Space available on disc, in bytes.")
194 195 196 ######################################################################## 197 # _ImageProperties class definition 198 ######################################################################## 199
200 -class _ImageProperties(object):
201 """ 202 Simple value object to hold image properties for C{DvdWriter}. 203 """
204 - def __init__(self):
205 self.newDisc = False 206 self.tmpdir = None 207 self.mediaLabel = None 208 self.entries = None # dict mapping path to graft point
209 210 211 ######################################################################## 212 # DvdWriter class definition 213 ######################################################################## 214
215 -class DvdWriter(object):
216 217 ###################### 218 # Class documentation 219 ###################### 220 221 """ 222 Class representing a device that knows how to write some kinds of DVD media. 223 224 Summary 225 ======= 226 227 This is a class representing a device that knows how to write some kinds 228 of DVD media. It provides common operations for the device, such as 229 ejecting the media and writing data to the media. 230 231 This class is implemented in terms of the C{eject}, C{growisofs} and 232 C{dvd+rw-mediainfo} utilities, all of which should be available on most 233 UN*X platforms. 234 235 Image Writer Interface 236 ====================== 237 238 The following methods make up the "image writer" interface shared 239 with other kinds of writers:: 240 241 __init__ 242 initializeImage() 243 addImageEntry() 244 writeImage() 245 setImageNewDisc() 246 retrieveCapacity() 247 getEstimatedImageSize() 248 249 Only these methods will be used by other Cedar Backup functionality 250 that expects a compatible image writer. 251 252 The media attribute is also assumed to be available. 253 254 Unlike the C{CdWriter}, the C{DvdWriter} can only operate in terms of 255 filesystem devices, not SCSI devices. So, although the constructor 256 interface accepts a SCSI device parameter for the sake of compatibility, 257 it's not used. 258 259 Media Types 260 =========== 261 262 This class knows how to write to DVD+R and DVD+RW media, represented 263 by the following constants: 264 265 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 266 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 267 268 The difference is that DVD+RW media can be rewritten, while DVD+R media 269 cannot be (although at present, C{DvdWriter} does not really 270 differentiate between rewritable and non-rewritable media). 271 272 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 273 of 1024*1024*1024 bytes per gigabyte. 274 275 The underlying C{growisofs} utility does support other kinds of media 276 (including DVD-R, DVD-RW and BlueRay) which work somewhat differently 277 than standard DVD+R and DVD+RW media. I don't support these other kinds 278 of media because I haven't had any opportunity to work with them. The 279 same goes for dual-layer media of any type. 280 281 Device Attributes vs. Media Attributes 282 ====================================== 283 284 As with the cdwriter functionality, a given dvdwriter instance has two 285 different kinds of attributes associated with it. I call these device 286 attributes and media attributes. 287 288 Device attributes are things which can be determined without looking at 289 the media. Media attributes are attributes which vary depending on the 290 state of the media. In general, device attributes are available via 291 instance variables and are constant over the life of an object, while 292 media attributes can be retrieved through method calls. 293 294 Compared to cdwriters, dvdwriters have very few attributes. This is due 295 to differences between the way C{growisofs} works relative to 296 C{cdrecord}. 297 298 Media Capacity 299 ============== 300 301 One major difference between the C{cdrecord}/C{mkisofs} utilities used by 302 the cdwriter class and the C{growisofs} utility used here is that the 303 process of estimating remaining capacity and image size is more 304 straightforward with C{cdrecord}/C{mkisofs} than with C{growisofs}. 305 306 In this class, remaining capacity is calculated by asking doing a dry run 307 of C{growisofs} and grabbing some information from the output of that 308 command. Image size is estimated by asking the C{IsoImage} class for an 309 estimate and then adding on a "fudge factor" determined through 310 experimentation. 311 312 Testing 313 ======= 314 315 It's rather difficult to test this code in an automated fashion, even if 316 you have access to a physical DVD writer drive. It's even more difficult 317 to test it if you are running on some build daemon (think of a Debian 318 autobuilder) which can't be expected to have any hardware or any media 319 that you could write to. 320 321 Because of this, some of the implementation below is in terms of static 322 methods that are supposed to take defined actions based on their 323 arguments. Public methods are then implemented in terms of a series of 324 calls to simplistic static methods. This way, we can test as much as 325 possible of the "difficult" functionality via testing the static methods, 326 while hoping that if the static methods are called appropriately, things 327 will work properly. It's not perfect, but it's much better than no 328 testing at all. 329 330 @sort: __init__, isRewritable, retrieveCapacity, openTray, closeTray, refreshMedia, 331 initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize, 332 _writeImage, _getEstimatedImageSize, _searchForOverburn, _buildWriteArgs, 333 device, scsiId, hardwareId, driveSpeed, media, deviceHasTray, deviceCanEject 334 """ 335 336 ############## 337 # Constructor 338 ############## 339
340 - def __init__(self, device, scsiId=None, driveSpeed=None, 341 mediaType=MEDIA_DVDPLUSRW, noEject=False, unittest=False):
342 """ 343 Initializes a DVD writer object. 344 345 Since C{growisofs} can only address devices using the device path (i.e. 346 C{/dev/dvd}), the hardware id will always be set based on the device. If 347 passed in, it will be saved for reference purposes only. 348 349 We have no way to query the device to ask whether it has a tray or can be 350 safely opened and closed. So, the C{noEject} flag is used to set these 351 values. If C{noEject=False}, then we assume a tray exists and open/close 352 is safe. If C{noEject=True}, then we assume that there is no tray and 353 open/close is not safe. 354 355 @note: The C{unittest} parameter should never be set to C{True} 356 outside of Cedar Backup code. It is intended for use in unit testing 357 Cedar Backup internals and has no other sensible purpose. 358 359 @param device: Filesystem device associated with this writer. 360 @type device: Absolute path to a filesystem device, i.e. C{/dev/dvd} 361 362 @param scsiId: SCSI id for the device (optional, for reference only). 363 @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun} 364 365 @param driveSpeed: Speed at which the drive writes. 366 @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default. 367 368 @param mediaType: Type of the media that is assumed to be in the drive. 369 @type mediaType: One of the valid media type as discussed above. 370 371 @param noEject: Tells Cedar Backup that the device cannot safely be ejected 372 @type noEject: Boolean true/false 373 374 @param unittest: Turns off certain validations, for use in unit testing. 375 @type unittest: Boolean true/false 376 377 @raise ValueError: If the device is not valid for some reason. 378 @raise ValueError: If the SCSI id is not in a valid form. 379 @raise ValueError: If the drive speed is not an integer >= 1. 380 """ 381 if scsiId is not None: 382 logger.warn("SCSI id [%s] will be ignored by DvdWriter." % scsiId) 383 self._image = None # optionally filled in by initializeImage() 384 self._device = validateDevice(device, unittest) 385 self._scsiId = scsiId # not validated, because it's just for reference 386 self._driveSpeed = validateDriveSpeed(driveSpeed) 387 self._media = MediaDefinition(mediaType) 388 if noEject: 389 self._deviceHasTray = False 390 self._deviceCanEject = False 391 else: 392 self._deviceHasTray = True # just assume 393 self._deviceCanEject = True # just assume
394 395 396 ############# 397 # Properties 398 ############# 399
400 - def _getDevice(self):
401 """ 402 Property target used to get the device value. 403 """ 404 return self._device
405
406 - def _getScsiId(self):
407 """ 408 Property target used to get the SCSI id value. 409 """ 410 return self._scsiId
411
412 - def _getHardwareId(self):
413 """ 414 Property target used to get the hardware id value. 415 """ 416 return self._device
417
418 - def _getDriveSpeed(self):
419 """ 420 Property target used to get the drive speed. 421 """ 422 return self._driveSpeed
423
424 - def _getMedia(self):
425 """ 426 Property target used to get the media description. 427 """ 428 return self._media
429
430 - def _getDeviceHasTray(self):
431 """ 432 Property target used to get the device-has-tray flag. 433 """ 434 return self._deviceHasTray
435
436 - def _getDeviceCanEject(self):
437 """ 438 Property target used to get the device-can-eject flag. 439 """ 440 return self._deviceCanEject
441 442 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.") 443 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device (saved for reference only).") 444 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer (always the device path)."); 445 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.") 446 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.") 447 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.") 448 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.") 449 450 451 ################################################# 452 # Methods related to device and media attributes 453 ################################################# 454
455 - def isRewritable(self):
456 """Indicates whether the media is rewritable per configuration.""" 457 return self._media.rewritable
458
459 - def retrieveCapacity(self, entireDisc=False):
460 """ 461 Retrieves capacity for the current media in terms of a C{MediaCapacity} 462 object. 463 464 If C{entireDisc} is passed in as C{True}, the capacity will be for the 465 entire disc, as if it were to be rewritten from scratch. The same will 466 happen if the disc can't be read for some reason. Otherwise, the capacity 467 will be calculated by subtracting the sectors currently used on the disc, 468 as reported by C{growisofs} itself. 469 470 @param entireDisc: Indicates whether to return capacity for entire disc. 471 @type entireDisc: Boolean true/false 472 473 @return: C{MediaCapacity} object describing the capacity of the media. 474 475 @raise ValueError: If there is a problem parsing the C{growisofs} output 476 @raise IOError: If the media could not be read for some reason. 477 """ 478 sectorsUsed = 0 479 if not entireDisc: 480 sectorsUsed = self._retrieveSectorsUsed() 481 sectorsAvailable = self._media.capacity - sectorsUsed # both are in sectors 482 bytesUsed = convertSize(sectorsUsed, UNIT_SECTORS, UNIT_BYTES) 483 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 484 return MediaCapacity(bytesUsed, bytesAvailable)
485 486 487 ####################################################### 488 # Methods used for working with the internal ISO image 489 ####################################################### 490
491 - def initializeImage(self, newDisc, tmpdir, mediaLabel=None):
492 """ 493 Initializes the writer's associated ISO image. 494 495 This method initializes the C{image} instance variable so that the caller 496 can use the C{addImageEntry} method. Once entries have been added, the 497 C{writeImage} method can be called with no arguments. 498 499 @param newDisc: Indicates whether the disc should be re-initialized 500 @type newDisc: Boolean true/false 501 502 @param tmpdir: Temporary directory to use if needed 503 @type tmpdir: String representing a directory path on disk 504 505 @param mediaLabel: Media label to be applied to the image, if any 506 @type mediaLabel: String, no more than 25 characters long 507 """ 508 self._image = _ImageProperties() 509 self._image.newDisc = newDisc 510 self._image.tmpdir = encodePath(tmpdir) 511 self._image.mediaLabel = mediaLabel 512 self._image.entries = {} # mapping from path to graft point (if any)
513
514 - def addImageEntry(self, path, graftPoint):
515 """ 516 Adds a filepath entry to the writer's associated ISO image. 517 518 The contents of the filepath -- but not the path itself -- will be added 519 to the image at the indicated graft point. If you don't want to use a 520 graft point, just pass C{None}. 521 522 @note: Before calling this method, you must call L{initializeImage}. 523 524 @param path: File or directory to be added to the image 525 @type path: String representing a path on disk 526 527 @param graftPoint: Graft point to be used when adding this entry 528 @type graftPoint: String representing a graft point path, as described above 529 530 @raise ValueError: If initializeImage() was not previously called 531 @raise ValueError: If the path is not a valid file or directory 532 """ 533 if self._image is None: 534 raise ValueError("Must call initializeImage() before using this method.") 535 if not os.path.exists(path): 536 raise ValueError("Path [%s] does not exist." % path) 537 self._image.entries[path] = graftPoint
538
539 - def setImageNewDisc(self, newDisc):
540 """ 541 Resets (overrides) the newDisc flag on the internal image. 542 @param newDisc: New disc flag to set 543 @raise ValueError: If initializeImage() was not previously called 544 """ 545 if self._image is None: 546 raise ValueError("Must call initializeImage() before using this method.") 547 self._image.newDisc = newDisc
548
549 - def getEstimatedImageSize(self):
550 """ 551 Gets the estimated size of the image associated with the writer. 552 553 This is an estimate and is conservative. The actual image could be as 554 much as 450 blocks (sectors) smaller under some circmstances. 555 556 @return: Estimated size of the image, in bytes. 557 558 @raise IOError: If there is a problem calling C{mkisofs}. 559 @raise ValueError: If initializeImage() was not previously called 560 """ 561 if self._image is None: 562 raise ValueError("Must call initializeImage() before using this method.") 563 return DvdWriter._getEstimatedImageSize(self._image.entries)
564 565 566 ###################################### 567 # Methods which expose device actions 568 ###################################### 569
570 - def openTray(self):
571 """ 572 Opens the device's tray and leaves it open. 573 574 This only works if the device has a tray and supports ejecting its media. 575 We have no way to know if the tray is currently open or closed, so we 576 just send the appropriate command and hope for the best. If the device 577 does not have a tray or does not support ejecting its media, then we do 578 nothing. 579 580 @raise IOError: If there is an error talking to the device. 581 """ 582 if self._deviceHasTray and self._deviceCanEject: 583 command = resolveCommand(EJECT_COMMAND) 584 args = [ self.device, ] 585 result = executeCommand(command, args)[0] 586 if result != 0: 587 raise IOError("Error (%d) executing eject command to open tray." % result)
588
589 - def closeTray(self):
590 """ 591 Closes the device's tray. 592 593 This only works if the device has a tray and supports ejecting its media. 594 We have no way to know if the tray is currently open or closed, so we 595 just send the appropriate command and hope for the best. If the device 596 does not have a tray or does not support ejecting its media, then we do 597 nothing. 598 599 @raise IOError: If there is an error talking to the device. 600 """ 601 if self._deviceHasTray and self._deviceCanEject: 602 command = resolveCommand(EJECT_COMMAND) 603 args = [ "-t", self.device, ] 604 result = executeCommand(command, args)[0] 605 if result != 0: 606 raise IOError("Error (%d) executing eject command to close tray." % result)
607
608 - def refreshMedia(self):
609 """ 610 Opens and then immediately closes the device's tray, to refresh the 611 device's idea of the media. 612 613 Sometimes, a device gets confused about the state of its media. Often, 614 all it takes to solve the problem is to eject the media and then 615 immediately reload it. 616 617 This only works if the device has a tray and supports ejecting its media. 618 We have no way to know if the tray is currently open or closed, so we 619 just send the appropriate command and hope for the best. If the device 620 does not have a tray or does not support ejecting its media, then we do 621 nothing. 622 623 @raise IOError: If there is an error talking to the device. 624 """ 625 self.openTray() 626 self.closeTray()
627
628 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
629 """ 630 Writes an ISO image to the media in the device. 631 632 If C{newDisc} is passed in as C{True}, we assume that the entire disc 633 will be re-created from scratch. Note that unlike C{CdWriter}, 634 C{DvdWriter} does not blank rewritable media before reusing it; however, 635 C{growisofs} is called such that the media will be re-initialized as 636 needed. 637 638 If C{imagePath} is passed in as C{None}, then the existing image 639 configured with C{initializeImage()} will be used. Under these 640 circumstances, the passed-in C{newDisc} flag will be ignored and the 641 value passed in to C{initializeImage()} will apply instead. 642 643 The C{writeMulti} argument is ignored. It exists for compatibility with 644 the Cedar Backup image writer interface. 645 646 @note: The image size indicated in the log ("Image size will be...") is 647 an estimate. The estimate is conservative and is probably larger than 648 the actual space that C{dvdwriter} will use. 649 650 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image 651 @type imagePath: String representing a path on disk 652 653 @param newDisc: Indicates whether the disc should be re-initialized 654 @type newDisc: Boolean true/false. 655 656 @param writeMulti: Unused 657 @type writeMulti: Boolean true/false 658 659 @raise ValueError: If the image path is not absolute. 660 @raise ValueError: If some path cannot be encoded properly. 661 @raise IOError: If the media could not be written to for some reason. 662 @raise ValueError: If no image is passed in and initializeImage() was not previously called 663 """ 664 if not writeMulti: 665 logger.warn("writeMulti value of [%s] ignored." % writeMulti) 666 if imagePath is None: 667 if self._image is None: 668 raise ValueError("Must call initializeImage() before using this method with no image path.") 669 size = self.getEstimatedImageSize() 670 logger.info("Image size will be %s (estimated)." % displayBytes(size)) 671 available = self.retrieveCapacity(entireDisc=self._image.newDisc).bytesAvailable 672 if size > available: 673 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 674 raise IOError("Media does not contain enough capacity to store image.") 675 self._writeImage(self._image.newDisc, None, self._image.entries, self._image.mediaLabel) 676 else: 677 if not os.path.isabs(imagePath): 678 raise ValueError("Image path must be absolute.") 679 imagePath = encodePath(imagePath) 680 self._writeImage(newDisc, imagePath, None)
681 682 683 ################################################################## 684 # Utility methods for dealing with growisofs and dvd+rw-mediainfo 685 ################################################################## 686
687 - def _writeImage(self, newDisc, imagePath, entries, mediaLabel=None):
688 """ 689 Writes an image to disc using either an entries list or an ISO image on 690 disk. 691 692 Callers are assumed to have done validation on paths, etc. before calling 693 this method. 694 695 @param newDisc: Indicates whether the disc should be re-initialized 696 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries} 697 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath} 698 699 @raise IOError: If the media could not be written to for some reason. 700 """ 701 command = resolveCommand(GROWISOFS_COMMAND) 702 args = DvdWriter._buildWriteArgs(newDisc, self.hardwareId, self._driveSpeed, imagePath, entries, mediaLabel, dryRun=False) 703 (result, output) = executeCommand(command, args, returnOutput=True) 704 if result != 0: 705 DvdWriter._searchForOverburn(output) # throws own exception if overburn condition is found 706 raise IOError("Error (%d) executing command to write disc." % result) 707 self.refreshMedia()
708
709 - def _getEstimatedImageSize(entries):
710 """ 711 Gets the estimated size of a set of image entries. 712 713 This is implemented in terms of the C{IsoImage} class. The returned 714 value is calculated by adding a "fudge factor" to the value from 715 C{IsoImage}. This fudge factor was determined by experimentation and is 716 conservative -- the actual image could be as much as 450 blocks smaller 717 under some circumstances. 718 719 @param entries: Dictionary mapping path to graft point. 720 721 @return: Total estimated size of image, in bytes. 722 723 @raise ValueError: If there are no entries in the dictionary 724 @raise ValueError: If any path in the dictionary does not exist 725 @raise IOError: If there is a problem calling C{mkisofs}. 726 """ 727 fudgeFactor = convertSize(2500.0, UNIT_SECTORS, UNIT_BYTES) # determined through experimentation 728 if len(entries.keys()) == 0: 729 raise ValueError("Must add at least one entry with addImageEntry().") 730 image = IsoImage() 731 for path in entries.keys(): 732 image.addEntry(path, entries[path], override=False, contentsOnly=True) 733 estimatedSize = image.getEstimatedSize() + fudgeFactor 734 return estimatedSize
735 _getEstimatedImageSize = staticmethod(_getEstimatedImageSize) 736
737 - def _retrieveSectorsUsed(self):
738 """ 739 Retrieves the number of sectors used on the current media. 740 741 This is a little ugly. We need to call growisofs in "dry-run" mode and 742 parse some information from its output. However, to do that, we need to 743 create a dummy file that we can pass to the command -- and we have to 744 make sure to remove it later. 745 746 Once growisofs has been run, then we call C{_parseSectorsUsed} to parse 747 the output and calculate the number of sectors used on the media. 748 749 @return: Number of sectors used on the media 750 """ 751 tempdir = tempfile.mkdtemp() 752 try: 753 entries = { tempdir: None } 754 args = DvdWriter._buildWriteArgs(False, self.hardwareId, self.driveSpeed, None, entries, None, dryRun=True) 755 command = resolveCommand(GROWISOFS_COMMAND) 756 (result, output) = executeCommand(command, args, returnOutput=True) 757 if result != 0: 758 logger.debug("Error (%d) calling growisofs to read sectors used." % result) 759 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.") 760 return 0.0 761 sectorsUsed = DvdWriter._parseSectorsUsed(output) 762 logger.debug("Determined sectors used as %s" % sectorsUsed) 763 return sectorsUsed 764 finally: 765 if os.path.exists(tempdir): 766 try: 767 os.rmdir(tempdir) 768 except: pass
769
770 - def _parseSectorsUsed(output):
771 """ 772 Parse sectors used information out of C{growisofs} output. 773 774 The first line of a growisofs run looks something like this:: 775 776 Executing 'mkisofs -C 973744,1401056 -M /dev/fd/3 -r -graft-points music4/=music | builtin_dd of=/dev/cdrom obs=32k seek=87566' 777 778 Dmitry has determined that the seek value in this line gives us 779 information about how much data has previously been written to the media. 780 That value multiplied by 16 yields the number of sectors used. 781 782 If the seek line cannot be found in the output, then sectors used of zero 783 is assumed. 784 785 @return: Sectors used on the media, as a floating point number. 786 787 @raise ValueError: If the output cannot be parsed properly. 788 """ 789 if output is not None: 790 pattern = re.compile(r"(^)(.*)(seek=)(.*)('$)") 791 for line in output: 792 match = pattern.search(line) 793 if match is not None: 794 try: 795 return float(match.group(4).strip()) * 16.0 796 except ValueError: 797 raise ValueError("Unable to parse sectors used out of growisofs output.") 798 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.") 799 return 0.0
800 _parseSectorsUsed = staticmethod(_parseSectorsUsed) 801
802 - def _searchForOverburn(output):
803 """ 804 Search for an "overburn" error message in C{growisofs} output. 805 806 The C{growisofs} command returns a non-zero exit code and puts a message 807 into the output -- even on a dry run -- if there is not enough space on 808 the media. This is called an "overburn" condition. 809 810 The error message looks like this:: 811 812 :-( /dev/cdrom: 894048 blocks are free, 2033746 to be written! 813 814 This method looks for the overburn error message anywhere in the output. 815 If a matching error message is found, an C{IOError} exception is raised 816 containing relevant information about the problem. Otherwise, the method 817 call returns normally. 818 819 @param output: List of output lines to search, as from C{executeCommand} 820 821 @raise IOError: If an overburn condition is found. 822 """ 823 if output is None: 824 return 825 pattern = re.compile(r"(^)(:-[(])(\s*.*:\s*)(.* )(blocks are free, )(.* )(to be written!)") 826 for line in output: 827 match = pattern.search(line) 828 if match is not None: 829 try: 830 available = convertSize(float(match.group(4).strip()), UNIT_SECTORS, UNIT_BYTES) 831 size = convertSize(float(match.group(6).strip()), UNIT_SECTORS, UNIT_BYTES) 832 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 833 except ValueError: 834 logger.error("Image does not fit in available capacity (no useful capacity info available).") 835 raise IOError("Media does not contain enough capacity to store image.")
836 _searchForOverburn = staticmethod(_searchForOverburn) 837
838 - def _buildWriteArgs(newDisc, hardwareId, driveSpeed, imagePath, entries, mediaLabel=None, dryRun=False):
839 """ 840 Builds a list of arguments to be passed to a C{growisofs} command. 841 842 The arguments will either cause C{growisofs} to write the indicated image 843 file to disc, or will pass C{growisofs} a list of directories or files 844 that should be written to disc. 845 846 If a new image is created, it will always be created with Rock Ridge 847 extensions (-r). A volume name will be applied (-V) if C{mediaLabel} is 848 not C{None}. 849 850 @param newDisc: Indicates whether the disc should be re-initialized 851 @param hardwareId: Hardware id for the device 852 @param driveSpeed: Speed at which the drive writes. 853 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries} 854 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath} 855 @param mediaLabel: Media label to set on the image, if any 856 @param dryRun: Says whether to make this a dry run (for checking capacity) 857 858 @note: If we write an existing image to disc, then the mediaLabel is 859 ignored. The media label is an attribute of the image, and should be set 860 on the image when it is created. 861 862 @note: We always pass the undocumented option C{-use-the-force-like=tty} 863 to growisofs. Without this option, growisofs will refuse to execute 864 certain actions when running from cron. A good example is -Z, which 865 happily overwrites an existing DVD from the command-line, but fails when 866 run from cron. It took a while to figure that out, since it worked every 867 time I tested it by hand. :( 868 869 @return: List suitable for passing to L{util.executeCommand} as C{args}. 870 871 @raise ValueError: If caller does not pass one or the other of imagePath or entries. 872 """ 873 args = [] 874 if (imagePath is None and entries is None) or (imagePath is not None and entries is not None): 875 raise ValueError("Must use either imagePath or entries.") 876 args.append("-use-the-force-luke=tty") # tell growisofs to let us run from cron 877 if dryRun: 878 args.append("-dry-run") 879 if driveSpeed is not None: 880 args.append("-speed=%d" % driveSpeed) 881 if newDisc: 882 args.append("-Z") 883 else: 884 args.append("-M") 885 if imagePath is not None: 886 args.append("%s=%s" % (hardwareId, imagePath)) 887 else: 888 args.append(hardwareId) 889 if mediaLabel is not None: 890 args.append("-V") 891 args.append(mediaLabel) 892 args.append("-r") # Rock Ridge extensions with sane ownership and permissions 893 args.append("-graft-points") 894 keys = entries.keys() 895 keys.sort() # just so we get consistent results 896 for key in keys: 897 # Same syntax as when calling mkisofs in IsoImage 898 if entries[key] is None: 899 args.append(key) 900 else: 901 args.append("%s/=%s" % (entries[key].strip("/"), key)) 902 return args
903 _buildWriteArgs = staticmethod(_buildWriteArgs)
904