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