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