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 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 972 2010-05-22 16:51:20Z 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.filesystem import BackupFileList 
 64  from CedarBackup2.writers.util import IsoImage 
 65  from CedarBackup2.util import resolveCommand, executeCommand 
 66  from CedarBackup2.util import convertSize, displayBytes, encodePath 
 67  from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_GBYTES 
 68  from CedarBackup2.writers.util import validateDevice, validateDriveSpeed 
 69   
 70   
 71  ######################################################################## 
 72  # Module-wide constants and variables 
 73  ######################################################################## 
 74   
 75  logger = logging.getLogger("CedarBackup2.log.writers.dvdwriter") 
 76   
 77  MEDIA_DVDPLUSR  = 1 
 78  MEDIA_DVDPLUSRW = 2 
 79   
 80  GROWISOFS_COMMAND = [ "growisofs", ] 
 81  EJECT_COMMAND     = [ "eject", ] 
 82   
 83   
 84  ######################################################################## 
 85  # MediaDefinition class definition 
 86  ######################################################################## 
 87   
88 -class MediaDefinition(object):
89 90 """ 91 Class encapsulating information about DVD media definitions. 92 93 The following media types are accepted: 94 95 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 96 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 97 98 Note that the capacity attribute returns capacity in terms of ISO sectors 99 (C{util.ISO_SECTOR_SIZE)}. This is for compatibility with the CD writer 100 functionality. 101 102 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 103 of 1024*1024*1024 bytes per gigabyte. 104 105 @sort: __init__, mediaType, rewritable, capacity 106 """ 107
108 - def __init__(self, mediaType):
109 """ 110 Creates a media definition for the indicated media type. 111 @param mediaType: Type of the media, as discussed above. 112 @raise ValueError: If the media type is unknown or unsupported. 113 """ 114 self._mediaType = None 115 self._rewritable = False 116 self._capacity = 0.0 117 self._setValues(mediaType)
118
119 - def _setValues(self, mediaType):
120 """ 121 Sets values based on media type. 122 @param mediaType: Type of the media, as discussed above. 123 @raise ValueError: If the media type is unknown or unsupported. 124 """ 125 if mediaType not in [MEDIA_DVDPLUSR, MEDIA_DVDPLUSRW, ]: 126 raise ValueError("Invalid media type %d." % mediaType) 127 self._mediaType = mediaType 128 if self._mediaType == MEDIA_DVDPLUSR: 129 self._rewritable = False 130 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB 131 elif self._mediaType == MEDIA_DVDPLUSRW: 132 self._rewritable = True 133 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB
134
135 - def _getMediaType(self):
136 """ 137 Property target used to get the media type value. 138 """ 139 return self._mediaType
140
141 - def _getRewritable(self):
142 """ 143 Property target used to get the rewritable flag value. 144 """ 145 return self._rewritable
146
147 - def _getCapacity(self):
148 """ 149 Property target used to get the capacity value. 150 """ 151 return self._capacity
152 153 mediaType = property(_getMediaType, None, None, doc="Configured media type.") 154 rewritable = property(_getRewritable, None, None, doc="Boolean indicating whether the media is rewritable.") 155 capacity = property(_getCapacity, None, None, doc="Total capacity of media in 2048-byte sectors.")
156 157 158 ######################################################################## 159 # MediaCapacity class definition 160 ######################################################################## 161
162 -class MediaCapacity(object):
163 164 """ 165 Class encapsulating information about DVD media capacity. 166 167 Space used and space available do not include any information about media 168 lead-in or other overhead. 169 170 @sort: __init__, bytesUsed, bytesAvailable, totalCapacity, utilized 171 """ 172
173 - def __init__(self, bytesUsed, bytesAvailable):
174 """ 175 Initializes a capacity object. 176 @raise ValueError: If the bytes used and available values are not floats. 177 """ 178 self._bytesUsed = float(bytesUsed) 179 self._bytesAvailable = float(bytesAvailable)
180
181 - def __str__(self):
182 """ 183 Informal string representation for class instance. 184 """ 185 return "utilized %s of %s (%.2f%%)" % (displayBytes(self.bytesUsed), displayBytes(self.totalCapacity), self.utilized)
186
187 - def _getBytesUsed(self):
188 """ 189 Property target used to get the bytes-used value. 190 """ 191 return self._bytesUsed
192
193 - def _getBytesAvailable(self):
194 """ 195 Property target available to get the bytes-available value. 196 """ 197 return self._bytesAvailable
198
199 - def _getTotalCapacity(self):
200 """ 201 Property target to get the total capacity (used + available). 202 """ 203 return self.bytesUsed + self.bytesAvailable
204
205 - def _getUtilized(self):
206 """ 207 Property target to get the percent of capacity which is utilized. 208 """ 209 if self.bytesAvailable <= 0.0: 210 return 100.0 211 elif self.bytesUsed <= 0.0: 212 return 0.0 213 return (self.bytesUsed / self.totalCapacity) * 100.0
214 215 bytesUsed = property(_getBytesUsed, None, None, doc="Space used on disc, in bytes.") 216 bytesAvailable = property(_getBytesAvailable, None, None, doc="Space available on disc, in bytes.") 217 totalCapacity = property(_getTotalCapacity, None, None, doc="Total capacity of the disc, in bytes.") 218 utilized = property(_getUtilized, None, None, "Percentage of the total capacity which is utilized.")
219 220 221 ######################################################################## 222 # _ImageProperties class definition 223 ######################################################################## 224
225 -class _ImageProperties(object):
226 """ 227 Simple value object to hold image properties for C{DvdWriter}. 228 """
229 - def __init__(self):
230 self.newDisc = False 231 self.tmpdir = None 232 self.mediaLabel = None 233 self.entries = None # dict mapping path to graft point
234 235 236 ######################################################################## 237 # DvdWriter class definition 238 ######################################################################## 239
240 -class DvdWriter(object):
241 242 ###################### 243 # Class documentation 244 ###################### 245 246 """ 247 Class representing a device that knows how to write some kinds of DVD media. 248 249 Summary 250 ======= 251 252 This is a class representing a device that knows how to write some kinds 253 of DVD media. It provides common operations for the device, such as 254 ejecting the media and writing data to the media. 255 256 This class is implemented in terms of the C{eject} and C{growisofs} 257 utilities, all of which should be available on most UN*X platforms. 258 259 Image Writer Interface 260 ====================== 261 262 The following methods make up the "image writer" interface shared 263 with other kinds of writers:: 264 265 __init__ 266 initializeImage() 267 addImageEntry() 268 writeImage() 269 setImageNewDisc() 270 retrieveCapacity() 271 getEstimatedImageSize() 272 273 Only these methods will be used by other Cedar Backup functionality 274 that expects a compatible image writer. 275 276 The media attribute is also assumed to be available. 277 278 Unlike the C{CdWriter}, the C{DvdWriter} can only operate in terms of 279 filesystem devices, not SCSI devices. So, although the constructor 280 interface accepts a SCSI device parameter for the sake of compatibility, 281 it's not used. 282 283 Media Types 284 =========== 285 286 This class knows how to write to DVD+R and DVD+RW media, represented 287 by the following constants: 288 289 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 290 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 291 292 The difference is that DVD+RW media can be rewritten, while DVD+R media 293 cannot be (although at present, C{DvdWriter} does not really 294 differentiate between rewritable and non-rewritable media). 295 296 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 297 of 1024*1024*1024 bytes per gigabyte. 298 299 The underlying C{growisofs} utility does support other kinds of media 300 (including DVD-R, DVD-RW and BlueRay) which work somewhat differently 301 than standard DVD+R and DVD+RW media. I don't support these other kinds 302 of media because I haven't had any opportunity to work with them. The 303 same goes for dual-layer media of any type. 304 305 Device Attributes vs. Media Attributes 306 ====================================== 307 308 As with the cdwriter functionality, a given dvdwriter instance has two 309 different kinds of attributes associated with it. I call these device 310 attributes and media attributes. 311 312 Device attributes are things which can be determined without looking at 313 the media. Media attributes are attributes which vary depending on the 314 state of the media. In general, device attributes are available via 315 instance variables and are constant over the life of an object, while 316 media attributes can be retrieved through method calls. 317 318 Compared to cdwriters, dvdwriters have very few attributes. This is due 319 to differences between the way C{growisofs} works relative to 320 C{cdrecord}. 321 322 Media Capacity 323 ============== 324 325 One major difference between the C{cdrecord}/C{mkisofs} utilities used by 326 the cdwriter class and the C{growisofs} utility used here is that the 327 process of estimating remaining capacity and image size is more 328 straightforward with C{cdrecord}/C{mkisofs} than with C{growisofs}. 329 330 In this class, remaining capacity is calculated by asking doing a dry run 331 of C{growisofs} and grabbing some information from the output of that 332 command. Image size is estimated by asking the C{IsoImage} class for an 333 estimate and then adding on a "fudge factor" determined through 334 experimentation. 335 336 Testing 337 ======= 338 339 It's rather difficult to test this code in an automated fashion, even if 340 you have access to a physical DVD writer drive. It's even more difficult 341 to test it if you are running on some build daemon (think of a Debian 342 autobuilder) which can't be expected to have any hardware or any media 343 that you could write to. 344 345 Because of this, some of the implementation below is in terms of static 346 methods that are supposed to take defined actions based on their 347 arguments. Public methods are then implemented in terms of a series of 348 calls to simplistic static methods. This way, we can test as much as 349 possible of the "difficult" functionality via testing the static methods, 350 while hoping that if the static methods are called appropriately, things 351 will work properly. It's not perfect, but it's much better than no 352 testing at all. 353 354 @sort: __init__, isRewritable, retrieveCapacity, openTray, closeTray, refreshMedia, 355 initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize, 356 _writeImage, _getEstimatedImageSize, _searchForOverburn, _buildWriteArgs, 357 device, scsiId, hardwareId, driveSpeed, media, deviceHasTray, deviceCanEject 358 """ 359 360 ############## 361 # Constructor 362 ############## 363
364 - def __init__(self, device, scsiId=None, driveSpeed=None, 365 mediaType=MEDIA_DVDPLUSRW, noEject=False, 366 refreshMediaDelay=0, unittest=False):
367 """ 368 Initializes a DVD writer object. 369 370 Since C{growisofs} can only address devices using the device path (i.e. 371 C{/dev/dvd}), the hardware id will always be set based on the device. If 372 passed in, it will be saved for reference purposes only. 373 374 We have no way to query the device to ask whether it has a tray or can be 375 safely opened and closed. So, the C{noEject} flag is used to set these 376 values. If C{noEject=False}, then we assume a tray exists and open/close 377 is safe. If C{noEject=True}, then we assume that there is no tray and 378 open/close is not safe. 379 380 @note: The C{unittest} parameter should never be set to C{True} 381 outside of Cedar Backup code. It is intended for use in unit testing 382 Cedar Backup internals and has no other sensible purpose. 383 384 @param device: Filesystem device associated with this writer. 385 @type device: Absolute path to a filesystem device, i.e. C{/dev/dvd} 386 387 @param scsiId: SCSI id for the device (optional, for reference only). 388 @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun} 389 390 @param driveSpeed: Speed at which the drive writes. 391 @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default. 392 393 @param mediaType: Type of the media that is assumed to be in the drive. 394 @type mediaType: One of the valid media type as discussed above. 395 396 @param noEject: Tells Cedar Backup that the device cannot safely be ejected 397 @type noEject: Boolean true/false 398 399 @param refreshMediaDelay: Refresh media delay to use, if any 400 @type refreshMediaDelay: Number of seconds, an integer >= 0 401 402 @param unittest: Turns off certain validations, for use in unit testing. 403 @type unittest: Boolean true/false 404 405 @raise ValueError: If the device is not valid for some reason. 406 @raise ValueError: If the SCSI id is not in a valid form. 407 @raise ValueError: If the drive speed is not an integer >= 1. 408 """ 409 if scsiId is not None: 410 logger.warn("SCSI id [%s] will be ignored by DvdWriter." % scsiId) 411 self._image = None # optionally filled in by initializeImage() 412 self._device = validateDevice(device, unittest) 413 self._scsiId = scsiId # not validated, because it's just for reference 414 self._driveSpeed = validateDriveSpeed(driveSpeed) 415 self._media = MediaDefinition(mediaType) 416 self._refreshMediaDelay = refreshMediaDelay 417 if noEject: 418 self._deviceHasTray = False 419 self._deviceCanEject = False 420 else: 421 self._deviceHasTray = True # just assume 422 self._deviceCanEject = True # just assume
423 424 425 ############# 426 # Properties 427 ############# 428
429 - def _getDevice(self):
430 """ 431 Property target used to get the device value. 432 """ 433 return self._device
434
435 - def _getScsiId(self):
436 """ 437 Property target used to get the SCSI id value. 438 """ 439 return self._scsiId
440
441 - def _getHardwareId(self):
442 """ 443 Property target used to get the hardware id value. 444 """ 445 return self._device
446
447 - def _getDriveSpeed(self):
448 """ 449 Property target used to get the drive speed. 450 """ 451 return self._driveSpeed
452
453 - def _getMedia(self):
454 """ 455 Property target used to get the media description. 456 """ 457 return self._media
458
459 - def _getDeviceHasTray(self):
460 """ 461 Property target used to get the device-has-tray flag. 462 """ 463 return self._deviceHasTray
464
465 - def _getDeviceCanEject(self):
466 """ 467 Property target used to get the device-can-eject flag. 468 """ 469 return self._deviceCanEject
470
471 - def _getRefreshMediaDelay(self):
472 """ 473 Property target used to get the configured refresh media delay, in seconds. 474 """ 475 return self._refreshMediaDelay
476 477 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.") 478 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device (saved for reference only).") 479 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer (always the device path)."); 480 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.") 481 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.") 482 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.") 483 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.") 484 refreshMediaDelay = property(_getRefreshMediaDelay, None, None, doc="Refresh media delay, in seconds.") 485 486 487 ################################################# 488 # Methods related to device and media attributes 489 ################################################# 490
491 - def isRewritable(self):
492 """Indicates whether the media is rewritable per configuration.""" 493 return self._media.rewritable
494
495 - def retrieveCapacity(self, entireDisc=False):
496 """ 497 Retrieves capacity for the current media in terms of a C{MediaCapacity} 498 object. 499 500 If C{entireDisc} is passed in as C{True}, the capacity will be for the 501 entire disc, as if it were to be rewritten from scratch. The same will 502 happen if the disc can't be read for some reason. Otherwise, the capacity 503 will be calculated by subtracting the sectors currently used on the disc, 504 as reported by C{growisofs} itself. 505 506 @param entireDisc: Indicates whether to return capacity for entire disc. 507 @type entireDisc: Boolean true/false 508 509 @return: C{MediaCapacity} object describing the capacity of the media. 510 511 @raise ValueError: If there is a problem parsing the C{growisofs} output 512 @raise IOError: If the media could not be read for some reason. 513 """ 514 sectorsUsed = 0 515 if not entireDisc: 516 sectorsUsed = self._retrieveSectorsUsed() 517 sectorsAvailable = self._media.capacity - sectorsUsed # both are in sectors 518 bytesUsed = convertSize(sectorsUsed, UNIT_SECTORS, UNIT_BYTES) 519 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 520 return MediaCapacity(bytesUsed, bytesAvailable)
521 522 523 ####################################################### 524 # Methods used for working with the internal ISO image 525 ####################################################### 526
527 - def initializeImage(self, newDisc, tmpdir, mediaLabel=None):
528 """ 529 Initializes the writer's associated ISO image. 530 531 This method initializes the C{image} instance variable so that the caller 532 can use the C{addImageEntry} method. Once entries have been added, the 533 C{writeImage} method can be called with no arguments. 534 535 @param newDisc: Indicates whether the disc should be re-initialized 536 @type newDisc: Boolean true/false 537 538 @param tmpdir: Temporary directory to use if needed 539 @type tmpdir: String representing a directory path on disk 540 541 @param mediaLabel: Media label to be applied to the image, if any 542 @type mediaLabel: String, no more than 25 characters long 543 """ 544 self._image = _ImageProperties() 545 self._image.newDisc = newDisc 546 self._image.tmpdir = encodePath(tmpdir) 547 self._image.mediaLabel = mediaLabel 548 self._image.entries = {} # mapping from path to graft point (if any)
549
550 - def addImageEntry(self, path, graftPoint):
551 """ 552 Adds a filepath entry to the writer's associated ISO image. 553 554 The contents of the filepath -- but not the path itself -- will be added 555 to the image at the indicated graft point. If you don't want to use a 556 graft point, just pass C{None}. 557 558 @note: Before calling this method, you must call L{initializeImage}. 559 560 @param path: File or directory to be added to the image 561 @type path: String representing a path on disk 562 563 @param graftPoint: Graft point to be used when adding this entry 564 @type graftPoint: String representing a graft point path, as described above 565 566 @raise ValueError: If initializeImage() was not previously called 567 @raise ValueError: If the path is not a valid file or directory 568 """ 569 if self._image is None: 570 raise ValueError("Must call initializeImage() before using this method.") 571 if not os.path.exists(path): 572 raise ValueError("Path [%s] does not exist." % path) 573 self._image.entries[path] = graftPoint
574
575 - def setImageNewDisc(self, newDisc):
576 """ 577 Resets (overrides) the newDisc flag on the internal image. 578 @param newDisc: New disc flag to set 579 @raise ValueError: If initializeImage() was not previously called 580 """ 581 if self._image is None: 582 raise ValueError("Must call initializeImage() before using this method.") 583 self._image.newDisc = newDisc
584
585 - def getEstimatedImageSize(self):
586 """ 587 Gets the estimated size of the image associated with the writer. 588 589 This is an estimate and is conservative. The actual image could be as 590 much as 450 blocks (sectors) smaller under some circmstances. 591 592 @return: Estimated size of the image, in bytes. 593 594 @raise IOError: If there is a problem calling C{mkisofs}. 595 @raise ValueError: If initializeImage() was not previously called 596 """ 597 if self._image is None: 598 raise ValueError("Must call initializeImage() before using this method.") 599 return DvdWriter._getEstimatedImageSize(self._image.entries)
600 601 602 ###################################### 603 # Methods which expose device actions 604 ###################################### 605
606 - def openTray(self):
607 """ 608 Opens the device's tray and leaves it open. 609 610 This only works if the device has a tray and supports ejecting its media. 611 We have no way to know if the tray is currently open or closed, so we 612 just send the appropriate command and hope for the best. If the device 613 does not have a tray or does not support ejecting its media, then we do 614 nothing. 615 616 @raise IOError: If there is an error talking to the device. 617 """ 618 if self._deviceHasTray and self._deviceCanEject: 619 command = resolveCommand(EJECT_COMMAND) 620 args = [ self.device, ] 621 result = executeCommand(command, args)[0] 622 if result != 0: 623 raise IOError("Error (%d) executing eject command to open tray." % result)
624
625 - def closeTray(self):
626 """ 627 Closes the device's tray. 628 629 This only works if the device has a tray and supports ejecting its media. 630 We have no way to know if the tray is currently open or closed, so we 631 just send the appropriate command and hope for the best. If the device 632 does not have a tray or does not support ejecting its media, then we do 633 nothing. 634 635 @raise IOError: If there is an error talking to the device. 636 """ 637 if self._deviceHasTray and self._deviceCanEject: 638 command = resolveCommand(EJECT_COMMAND) 639 args = [ "-t", self.device, ] 640 result = executeCommand(command, args)[0] 641 if result != 0: 642 raise IOError("Error (%d) executing eject command to close tray." % result)
643
644 - def refreshMedia(self):
645 """ 646 Opens and then immediately closes the device's tray, to refresh the 647 device's idea of the media. 648 649 Sometimes, a device gets confused about the state of its media. Often, 650 all it takes to solve the problem is to eject the media and then 651 immediately reload it. (There is also a configurable refresh media delay 652 which can be applied after the tray is closed, for situations where this 653 makes a difference.) 654 655 This only works if the device has a tray and supports ejecting its media. 656 We have no way to know if the tray is currently open or closed, so we 657 just send the appropriate command and hope for the best. If the device 658 does not have a tray or does not support ejecting its media, then we do 659 nothing. The configured delay still applies, though. 660 661 @raise IOError: If there is an error talking to the device. 662 """ 663 self.openTray() 664 self.closeTray() 665 if self.refreshMediaDelay is not None: 666 logger.debug("Per configuration, sleeping %d seconds to stabilize media state." % self.refreshMediaDelay) 667 time.sleep(self.refreshMediaDelay) 668 logger.debug("Sleep is complete; hopefully media state is stable now.")
669
670 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
671 """ 672 Writes an ISO image to the media in the device. 673 674 If C{newDisc} is passed in as C{True}, we assume that the entire disc 675 will be re-created from scratch. Note that unlike C{CdWriter}, 676 C{DvdWriter} does not blank rewritable media before reusing it; however, 677 C{growisofs} is called such that the media will be re-initialized as 678 needed. 679 680 If C{imagePath} is passed in as C{None}, then the existing image 681 configured with C{initializeImage()} will be used. Under these 682 circumstances, the passed-in C{newDisc} flag will be ignored and the 683 value passed in to C{initializeImage()} will apply instead. 684 685 The C{writeMulti} argument is ignored. It exists for compatibility with 686 the Cedar Backup image writer interface. 687 688 @note: The image size indicated in the log ("Image size will be...") is 689 an estimate. The estimate is conservative and is probably larger than 690 the actual space that C{dvdwriter} will use. 691 692 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image 693 @type imagePath: String representing a path on disk 694 695 @param newDisc: Indicates whether the disc should be re-initialized 696 @type newDisc: Boolean true/false. 697 698 @param writeMulti: Unused 699 @type writeMulti: Boolean true/false 700 701 @raise ValueError: If the image path is not absolute. 702 @raise ValueError: If some path cannot be encoded properly. 703 @raise IOError: If the media could not be written to for some reason. 704 @raise ValueError: If no image is passed in and initializeImage() was not previously called 705 """ 706 if not writeMulti: 707 logger.warn("writeMulti value of [%s] ignored." % writeMulti) 708 if imagePath is None: 709 if self._image is None: 710 raise ValueError("Must call initializeImage() before using this method with no image path.") 711 size = self.getEstimatedImageSize() 712 logger.info("Image size will be %s (estimated)." % displayBytes(size)) 713 available = self.retrieveCapacity(entireDisc=self._image.newDisc).bytesAvailable 714 if size > available: 715 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 716 raise IOError("Media does not contain enough capacity to store image.") 717 self._writeImage(self._image.newDisc, None, self._image.entries, self._image.mediaLabel) 718 else: 719 if not os.path.isabs(imagePath): 720 raise ValueError("Image path must be absolute.") 721 imagePath = encodePath(imagePath) 722 self._writeImage(newDisc, imagePath, None)
723 724 725 ################################################################## 726 # Utility methods for dealing with growisofs and dvd+rw-mediainfo 727 ################################################################## 728
729 - def _writeImage(self, newDisc, imagePath, entries, mediaLabel=None):
730 """ 731 Writes an image to disc using either an entries list or an ISO image on 732 disk. 733 734 Callers are assumed to have done validation on paths, etc. before calling 735 this method. 736 737 @param newDisc: Indicates whether the disc should be re-initialized 738 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries} 739 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath} 740 741 @raise IOError: If the media could not be written to for some reason. 742 """ 743 command = resolveCommand(GROWISOFS_COMMAND) 744 args = DvdWriter._buildWriteArgs(newDisc, self.hardwareId, self._driveSpeed, imagePath, entries, mediaLabel, dryRun=False) 745 (result, output) = executeCommand(command, args, returnOutput=True) 746 if result != 0: 747 DvdWriter._searchForOverburn(output) # throws own exception if overburn condition is found 748 raise IOError("Error (%d) executing command to write disc." % result) 749 self.refreshMedia()
750
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 _getEstimatedImageSize = staticmethod(_getEstimatedImageSize) 778
779 - def _retrieveSectorsUsed(self):
780 """ 781 Retrieves the number of sectors used on the current media. 782 783 This is a little ugly. We need to call growisofs in "dry-run" mode and 784 parse some information from its output. However, to do that, we need to 785 create a dummy file that we can pass to the command -- and we have to 786 make sure to remove it later. 787 788 Once growisofs has been run, then we call C{_parseSectorsUsed} to parse 789 the output and calculate the number of sectors used on the media. 790 791 @return: Number of sectors used on the media 792 """ 793 tempdir = tempfile.mkdtemp() 794 try: 795 entries = { tempdir: None } 796 args = DvdWriter._buildWriteArgs(False, self.hardwareId, self.driveSpeed, None, entries, None, dryRun=True) 797 command = resolveCommand(GROWISOFS_COMMAND) 798 (result, output) = executeCommand(command, args, returnOutput=True) 799 if result != 0: 800 logger.debug("Error (%d) calling growisofs to read sectors used." % result) 801 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.") 802 return 0.0 803 sectorsUsed = DvdWriter._parseSectorsUsed(output) 804 logger.debug("Determined sectors used as %s" % sectorsUsed) 805 return sectorsUsed 806 finally: 807 if os.path.exists(tempdir): 808 try: 809 os.rmdir(tempdir) 810 except: pass
811
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 _parseSectorsUsed = staticmethod(_parseSectorsUsed) 843
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 _searchForOverburn = staticmethod(_searchForOverburn) 879
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 _buildWriteArgs = staticmethod(_buildWriteArgs)
946