1# Copyright 2003 Ben Escoto 2# 3# This file is part of rdiff-backup. 4# 5# rdiff-backup is free software; you can redistribute it and/or modify 6# under the terms of the GNU General Public License as published by the 7# Free Software Foundation; either version 2 of the License, or (at your 8# option) any later version. 9# 10# rdiff-backup is distributed in the hope that it will be useful, but 11# WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with rdiff-backup; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18# 02110-1301, USA 19"""Determine the capabilities of given file system 20 21rdiff-backup needs to read and write to file systems with varying 22abilities. For instance, some file systems and not others have ACLs, 23are case-sensitive, or can store ownership information. The code in 24this module tests the file system for various features, and returns an 25FSAbilities object describing it. 26 27""" 28 29import errno 30import os 31from . import Globals, log, TempFile, selection, robust, SetConnections, \ 32 FilenameMapping, win_acls, Time 33 34 35class FSAbilities: 36 """Store capabilities of given file system""" 37 extended_filenames = None # True if filenames can have non-ASCII chars 38 win_reserved_filenames = None # True if filenames can't have ",*,: etc. 39 case_sensitive = None # True if "foobar" and "FoObAr" are different files 40 ownership = None # True if chown works on this filesystem 41 acls = None # True if access control lists supported 42 eas = None # True if extended attributes supported 43 win_acls = None # True if windows access control lists supported 44 hardlinks = None # True if hard linking supported 45 fsync_dirs = None # True if directories can be fsync'd 46 dir_inc_perms = None # True if regular files can have full permissions 47 resource_forks = None # True if system supports resource forks 48 carbonfile = None # True if Mac Carbon file data is supported. 49 name = None # Short string, not used for any technical purpose 50 read_only = None # True if capabilities were determined non-destructively 51 high_perms = None # True if suid etc perms are (read/write) supported 52 escape_dos_devices = None # True if dos device files can't be created (e.g., 53 # aux, con, com1, etc) 54 escape_trailing_spaces = None # True if trailing spaces or periods at the 55 # end of filenames aren't preserved 56 symlink_perms = None # True if symlink perms are affected by umask 57 58 def __init__(self, name=None): 59 """FSAbilities initializer. name is only used in logging""" 60 self.name = name 61 62 def __str__(self): 63 """Return pretty printable version of self""" 64 assert self.read_only == 0 or self.read_only == 1, self.read_only 65 s = ['-' * 65] 66 67 def addline(desc, val_text): 68 """Add description line to s""" 69 s.append(' %s%s%s' % (desc, ' ' * (45 - len(desc)), val_text)) 70 71 def add_boolean_list(pair_list): 72 """Add lines from list of (desc, boolean) pairs""" 73 for desc, boolean in pair_list: 74 if boolean: 75 val_text = 'On' 76 elif boolean is None: 77 val_text = 'N/A' 78 else: 79 assert boolean == 0 80 val_text = 'Off' 81 addline(desc, val_text) 82 83 def get_title_line(): 84 """Add the first line, mostly for decoration""" 85 read_string = self.read_only and "read only" or "read/write" 86 if self.name: 87 return ('Detected abilities for %s (%s) file system:' % 88 (self.name, read_string)) 89 else: 90 return ( 91 'Detected abilities for %s file system' % (read_string, )) 92 93 s.append(get_title_line()) 94 if not self.read_only: 95 add_boolean_list([ 96 ('Ownership changing', self.ownership), 97 ('Hard linking', self.hardlinks), 98 ('fsync() directories', self.fsync_dirs), 99 ('Directory inc permissions', self.dir_inc_perms), 100 ('High-bit permissions', self.high_perms), 101 ('Symlink permissions', self.symlink_perms), 102 ('Extended filenames', self.extended_filenames), 103 ('Windows reserved filenames', self.win_reserved_filenames), 104 ]) 105 add_boolean_list( 106 [('Access control lists', self.acls), 107 ('Extended attributes', self.eas), 108 ('Windows access control lists', self.win_acls), 109 ('Case sensitivity', self.case_sensitive), 110 ('Escape DOS devices', self.escape_dos_devices), 111 ('Escape trailing spaces', self.escape_trailing_spaces), 112 ('Mac OS X style resource forks', self.resource_forks), 113 ('Mac OS X Finder information', self.carbonfile)]) 114 s.append(s[0]) 115 return '\n'.join(s) 116 117 def init_readonly(self, rp): 118 """Set variables using fs tested at RPath rp. Run locally. 119 120 This method does not write to the file system at all, and 121 should be run on the file system when the file system will 122 only need to be read. 123 124 Only self.acls and self.eas are set. 125 126 """ 127 assert rp.conn is Globals.local_connection 128 self.root_rp = rp 129 self.read_only = 1 130 self.set_eas(rp, 0) 131 self.set_acls(rp) 132 self.set_win_acls(rp, 0) 133 self.set_resource_fork_readonly(rp) 134 self.set_carbonfile() 135 self.set_case_sensitive_readonly(rp) 136 self.set_escape_dos_devices(rp) 137 self.set_escape_trailing_spaces_readonly(rp) 138 return self 139 140 def init_readwrite(self, rbdir): 141 """Set variables using fs tested at rp_base. Run locally. 142 143 This method creates a temp directory in rp_base and writes to 144 it in order to test various features. Use on a file system 145 that will be written to. 146 147 """ 148 assert rbdir.conn is Globals.local_connection 149 if not rbdir.isdir(): 150 assert not rbdir.lstat(), (rbdir.path, rbdir.lstat()) 151 rbdir.mkdir() 152 self.root_rp = rbdir 153 self.read_only = 0 154 subdir = TempFile.new_in_dir(rbdir) 155 subdir.mkdir() 156 157 self.set_extended_filenames(subdir) 158 self.set_win_reserved_filenames(subdir) 159 self.set_case_sensitive_readwrite(subdir) 160 self.set_ownership(subdir) 161 self.set_hardlinks(subdir) 162 self.set_fsync_dirs(subdir) 163 self.set_eas(subdir, 1) 164 self.set_acls(subdir) 165 self.set_win_acls(subdir, 1) 166 self.set_dir_inc_perms(subdir) 167 self.set_resource_fork_readwrite(subdir) 168 self.set_carbonfile() 169 self.set_high_perms_readwrite(subdir) 170 self.set_symlink_perms(subdir) 171 self.set_escape_dos_devices(subdir) 172 self.set_escape_trailing_spaces_readwrite(subdir) 173 174 subdir.delete() 175 return self 176 177 def set_ownership(self, testdir): 178 """Set self.ownership to true iff testdir's ownership can be changed""" 179 tmp_rp = testdir.append("foo") 180 tmp_rp.touch() 181 uid, gid = tmp_rp.getuidgid() 182 try: 183 tmp_rp.chown(uid // 2 + 1, gid // 2 + 1) # just choose random uid/gid 184 tmp_rp.chown(0, 0) 185 except (IOError, OSError, AttributeError): 186 self.ownership = 0 187 else: 188 self.ownership = 1 189 tmp_rp.delete() 190 191 def set_hardlinks(self, testdir): 192 """Set self.hardlinks to true iff hard linked files can be made""" 193 hl_source = testdir.append("hardlinked_file1") 194 hl_dir = testdir.append("hl") 195 hl_dir.mkdir() 196 hl_dest = hl_dir.append("hardlinked_file2") 197 hl_source.touch() 198 try: 199 hl_dest.hardlink(hl_source.path) 200 if hl_source.getinode() != hl_dest.getinode(): 201 raise IOError(errno.EOPNOTSUPP, "Hard links don't compare") 202 except (IOError, OSError, AttributeError): 203 if Globals.preserve_hardlinks != 0: 204 log.Log( 205 "Warning: hard linking not supported by filesystem " 206 "at %s" % self.root_rp.get_safepath(), 3) 207 self.hardlinks = None 208 else: 209 self.hardlinks = 1 210 211 def set_fsync_dirs(self, testdir): 212 """Set self.fsync_dirs if directories can be fsync'd""" 213 assert testdir.conn is Globals.local_connection 214 try: 215 testdir.fsync() 216 except (IOError, OSError): 217 log.Log( 218 "Directories on file system at %s are not fsyncable.\n" 219 "Assuming it's unnecessary." % testdir.get_safepath(), 4) 220 self.fsync_dirs = 0 221 else: 222 self.fsync_dirs = 1 223 224 def set_extended_filenames(self, subdir): 225 """Set self.extended_filenames by trying to write a path""" 226 assert not self.read_only 227 228 # Make sure ordinary filenames ok 229 ordinary_filename = b'5-_ a.snapshot.gz' 230 ord_rp = subdir.append(ordinary_filename) 231 ord_rp.touch() 232 assert ord_rp.lstat() 233 ord_rp.delete() 234 235 # Try path with UTF-8 encoded character 236 extended_filename = ( 237 'uni' + chr(225) + chr(132) + chr(137)).encode('utf-8') 238 ext_rp = None 239 try: 240 ext_rp = subdir.append(extended_filename) 241 ext_rp.touch() 242 except (IOError, OSError): 243 if ext_rp: 244 assert not ext_rp.lstat() 245 self.extended_filenames = 0 246 else: 247 assert ext_rp.lstat() 248 try: 249 ext_rp.delete() 250 except (IOError, OSError): 251 # Broken CIFS setups will sometimes create UTF-8 files 252 # and even stat them, but not let us perform file operations 253 # on them. Test file cannot be deleted. UTF-8 chars not in the 254 # underlying codepage get translated to '?' 255 log.Log.FatalError( 256 "Could not delete extended filenames test " 257 "file. If you are using a CIFS share, please" 258 " see the FAQ entry about characters being " 259 "transformed to a '?'") 260 self.extended_filenames = 1 261 262 def set_win_reserved_filenames(self, subdir): 263 """Set self.win_reserved_filenames by trying to write a path""" 264 assert not self.read_only 265 266 # Try Windows reserved characters 267 win_reserved_filename = ':\\"' 268 win_rp = None 269 try: 270 win_rp = subdir.append(win_reserved_filename) 271 win_rp.touch() 272 except (IOError, OSError): 273 if win_rp: 274 assert not win_rp.lstat() 275 self.win_reserved_filenames = 1 276 else: 277 assert win_rp.lstat() 278 try: 279 win_rp.delete() 280 except (IOError, OSError): 281 self.win_reserved_filenames = 1 282 else: 283 self.win_reserved_filenames = 0 284 285 def set_acls(self, rp): 286 """Set self.acls based on rp. Does not write. Needs to be local""" 287 assert Globals.local_connection is rp.conn 288 assert rp.lstat() 289 if Globals.acls_active == 0: 290 log.Log( 291 "POSIX ACLs test skipped. rdiff-backup run " 292 "with --no-acls option.", 4) 293 self.acls = 0 294 return 295 296 try: 297 import posix1e 298 except ImportError: 299 log.Log( 300 "Unable to import module posix1e from pylibacl " 301 "package.\nPOSIX ACLs not supported on filesystem at %s" % 302 rp.get_safepath(), 4) 303 self.acls = 0 304 return 305 306 try: 307 posix1e.ACL(file=rp.path) 308 except IOError: 309 log.Log( 310 "POSIX ACLs not supported by filesystem at %s" % 311 rp.get_safepath(), 4) 312 self.acls = 0 313 else: 314 self.acls = 1 315 316 def set_case_sensitive_readwrite(self, subdir): 317 """Determine if directory at rp is case sensitive by writing""" 318 assert not self.read_only 319 upper_a = subdir.append("A") 320 upper_a.touch() 321 lower_a = subdir.append("a") 322 if lower_a.lstat(): 323 lower_a.delete() 324 upper_a.setdata() 325 if upper_a.lstat(): 326 # we know that (fuse-)exFAT 1.3.0 takes 1sec to register the 327 # deletion (July 2020) 328 log.Log.FatalError( 329 "We're sorry but the target file system at '%s' isn't " 330 "deemed reliable enough for a backup. It takes too long " 331 "or doesn't register case insensitive deletion of files." 332 % subdir.get_safepath()) 333 self.case_sensitive = 0 334 else: 335 upper_a.delete() 336 self.case_sensitive = 1 337 338 def set_case_sensitive_readonly(self, rp): 339 """Determine if directory at rp is case sensitive without writing""" 340 341 def find_letter(subdir): 342 """Find a (subdir_rp, dirlist) with a letter in it, or None 343 344 Recurse down the directory, looking for any file that has 345 a letter in it. Return the pair (rp, [list of filenames]) 346 where the list is of the directory containing rp. 347 348 """ 349 files_list = robust.listrp(subdir) 350 for filename in files_list: 351 if filename != filename.swapcase(): 352 return (subdir, files_list, filename) 353 for filename in files_list: 354 dir_rp = subdir.append(filename) 355 if dir_rp.isdir(): 356 subsearch = find_letter(dir_rp) 357 if subsearch: 358 return subsearch 359 return None 360 361 def test_triple(dir_rp, dirlist, filename): 362 """Return 1 if filename shows system case sensitive""" 363 try: 364 letter_rp = dir_rp.append(filename) 365 except OSError: 366 return 0 367 assert letter_rp.lstat(), letter_rp 368 swapped = filename.swapcase() 369 if swapped in dirlist: 370 return 1 371 372 swapped_rp = dir_rp.append(swapped) 373 if swapped_rp.lstat(): 374 return 0 375 return 1 376 377 triple = find_letter(rp) 378 if not triple: 379 log.Log( 380 "Warning: could not determine case sensitivity of " 381 "source directory at\n %s\n" 382 "because we can't find any files with letters in them.\n" 383 "It will be treated as case sensitive." % rp.get_safepath(), 2) 384 self.case_sensitive = 1 385 return 386 387 self.case_sensitive = test_triple(*triple) 388 389 def set_eas(self, rp, write): 390 """Set extended attributes from rp. Tests writing if write is true.""" 391 assert Globals.local_connection is rp.conn 392 assert rp.lstat() 393 if Globals.eas_active == 0: 394 log.Log( 395 "Extended attributes test skipped. rdiff-backup run " 396 "with --no-eas option.", 4) 397 self.eas = 0 398 return 399 try: 400 import xattr.pyxattr_compat as xattr 401 except ImportError: 402 try: 403 import xattr 404 except ImportError: 405 log.Log( 406 "Unable to import module (py)xattr.\nExtended attributes not " 407 "supported on filesystem at %s" % (rp.get_safepath(), ), 4) 408 self.eas = 0 409 return 410 411 try: 412 xattr.list(rp.path) 413 if write: 414 xattr.set(rp.path, b"user.test", b"test val") 415 assert xattr.get(rp.path, b"user.test") == b"test val" 416 except IOError: 417 log.Log( 418 "Extended attributes not supported by " 419 "filesystem at %s" % (rp.get_safepath(), ), 4) 420 self.eas = 0 421 except AssertionError: 422 log.Log( 423 "Extended attributes support is broken on filesystem at " 424 "%s.\nPlease upgrade the filesystem driver, contact the " 425 "developers,\nor use the --no-eas option to disable " 426 "extended attributes\nsupport and suppress this message." % 427 (rp.get_safepath(), ), 1) 428 self.eas = 0 429 else: 430 self.eas = 1 431 432 def set_win_acls(self, dir_rp, write): 433 """Test if windows access control lists are supported""" 434 assert Globals.local_connection is dir_rp.conn 435 assert dir_rp.lstat() 436 if Globals.win_acls_active == 0: 437 log.Log( 438 "Windows ACLs test skipped. rdiff-backup run " 439 "with --no-acls option.", 4) 440 self.win_acls = 0 441 return 442 443 try: 444 import win32security 445 import pywintypes 446 except ImportError: 447 log.Log( 448 "Unable to import win32security module. Windows ACLs\n" 449 "not supported by filesystem at %s" % dir_rp.get_safepath(), 4) 450 self.win_acls = 0 451 return 452 try: 453 sd = win32security.GetNamedSecurityInfo( 454 os.fsdecode(dir_rp.path), win32security.SE_FILE_OBJECT, 455 win32security.OWNER_SECURITY_INFORMATION 456 | win32security.GROUP_SECURITY_INFORMATION 457 | win32security.DACL_SECURITY_INFORMATION) 458 acl = sd.GetSecurityDescriptorDacl() 459 acl.GetAceCount() # to verify that it works 460 if write: 461 win32security.SetNamedSecurityInfo( 462 os.fsdecode(dir_rp.path), win32security.SE_FILE_OBJECT, 463 win32security.OWNER_SECURITY_INFORMATION 464 | win32security.GROUP_SECURITY_INFORMATION 465 | win32security.DACL_SECURITY_INFORMATION, 466 sd.GetSecurityDescriptorOwner(), 467 sd.GetSecurityDescriptorGroup(), 468 sd.GetSecurityDescriptorDacl(), None) 469 except (OSError, AttributeError, pywintypes.error): 470 log.Log( 471 "Unable to load a Windows ACL.\nWindows ACLs not supported " 472 "by filesystem at %s" % dir_rp.get_safepath(), 4) 473 self.win_acls = 0 474 return 475 476 try: 477 win_acls.init_acls() 478 except (OSError, AttributeError, pywintypes.error): 479 log.Log( 480 "Unable to init win_acls.\nWindows ACLs not supported by " 481 "filesystem at %s" % dir_rp.get_safepath(), 4) 482 self.win_acls = 0 483 return 484 self.win_acls = 1 485 486 def set_dir_inc_perms(self, rp): 487 """See if increments can have full permissions like a directory""" 488 test_rp = rp.append('dir_inc_check') 489 test_rp.touch() 490 try: 491 test_rp.chmod(0o7777, 4) 492 except OSError: 493 test_rp.delete() 494 self.dir_inc_perms = 0 495 return 496 test_rp.setdata() 497 assert test_rp.isreg() 498 if test_rp.getperms() == 0o7777 or test_rp.getperms() == 0o6777: 499 self.dir_inc_perms = 1 500 else: 501 self.dir_inc_perms = 0 502 test_rp.delete() 503 504 def set_carbonfile(self): 505 """Test for support of the Mac Carbon library. This library 506 can be used to obtain Finder info (creator/type).""" 507 try: 508 import Carbon.File 509 except (ImportError, AttributeError): 510 self.carbonfile = 0 511 return 512 513 try: 514 Carbon.File.FSSpec('.') # just to verify that it works 515 except BaseException: 516 self.carbonfile = 0 517 return 518 519 self.carbonfile = 1 520 521 def set_resource_fork_readwrite(self, dir_rp): 522 """Test for resource forks by writing to regular_file/..namedfork/rsrc""" 523 assert dir_rp.conn is Globals.local_connection 524 reg_rp = dir_rp.append('regfile') 525 reg_rp.touch() 526 527 s = b'test string---this should end up in resource fork' 528 try: 529 fp_write = open( 530 os.path.join(reg_rp.path, b'..namedfork', b'rsrc'), 'wb') 531 fp_write.write(s) 532 assert not fp_write.close() 533 534 fp_read = open( 535 os.path.join(reg_rp.path, b'..namedfork', b'rsrc'), 'rb') 536 s_back = fp_read.read() 537 assert not fp_read.close() 538 except (OSError, IOError): 539 self.resource_forks = 0 540 else: 541 self.resource_forks = (s_back == s) 542 reg_rp.delete() 543 544 def set_resource_fork_readonly(self, dir_rp): 545 """Test for resource fork support by testing an regular file 546 547 Launches search for regular file in given directory. If no 548 regular file is found, resource_fork support will be turned 549 off by default. 550 551 """ 552 for rp in selection.Select(dir_rp).set_iter(): 553 if rp.isreg(): 554 try: 555 rfork = rp.append(b'..namedfork', b'rsrc') 556 fp = rfork.open('rb') 557 fp.read() 558 assert not fp.close() 559 except (OSError, IOError): 560 self.resource_forks = 0 561 return 562 self.resource_forks = 1 563 return 564 self.resource_forks = 0 565 566 def set_high_perms_readwrite(self, dir_rp): 567 """Test for writing high-bit permissions like suid""" 568 tmpf_rp = dir_rp.append(b"high_perms_file") 569 tmpf_rp.touch() 570 tmpd_rp = dir_rp.append(b"high_perms_dir") 571 tmpd_rp.touch() 572 try: 573 tmpf_rp.chmod(0o7000, 4) 574 tmpf_rp.chmod(0o7777, 4) 575 tmpd_rp.chmod(0o7000, 4) 576 tmpd_rp.chmod(0o7777, 4) 577 except (OSError, IOError): 578 self.high_perms = 0 579 else: 580 self.high_perms = 1 581 tmpf_rp.delete() 582 tmpd_rp.delete() 583 584 def set_symlink_perms(self, dir_rp): 585 """Test if symlink permissions are affected by umask""" 586 sym_source = dir_rp.append(b"symlinked_file1") 587 sym_source.touch() 588 sym_dest = dir_rp.append(b"symlinked_file2") 589 try: 590 sym_dest.symlink(b"symlinked_file1") 591 except (OSError, AttributeError): 592 self.symlink_perms = 0 593 else: 594 sym_dest.setdata() 595 assert sym_dest.issym() 596 if sym_dest.getperms() == 0o700: 597 self.symlink_perms = 1 598 else: 599 self.symlink_perms = 0 600 sym_dest.delete() 601 sym_source.delete() 602 603 def set_escape_dos_devices(self, subdir): 604 """Test if DOS device files can be used as filenames. 605 606 This test must detect if the underlying OS is Windows, whether we are 607 running under Cygwin or natively. Cygwin allows these special files to 608 be stat'd from any directory. Native Windows returns OSError (like 609 non-Cygwin POSIX), but we can check for that using os.name. 610 611 Note that 'con' and 'aux' have some unusual behaviors as shown below. 612 613 os.lstat() | con aux prn 614 -------------+------------------------------------- 615 Unix | OSError,2 OSError,2 OSError,2 616 Cygwin/NTFS | -success- -success- -success- 617 Cygwin/FAT32 | -success- -HANGS- 618 Native Win | WinError,2 WinError,87 WinError,87 619 """ 620 if os.name == "nt": 621 self.escape_dos_devices = 1 622 return 623 624 try: 625 device_rp = subdir.append(b"con") 626 if device_rp.lstat(): 627 self.escape_dos_devices = 1 628 else: 629 self.escape_dos_devices = 0 630 except (OSError): 631 self.escape_dos_devices = 1 632 633 def set_escape_trailing_spaces_readwrite(self, testdir): 634 """ 635 Windows and Linux/FAT32 will not preserve trailing spaces or periods. 636 Linux/FAT32 behaves inconsistently: It will give an OSError,22 if 637 os.mkdir() is called on a directory name with a space at the end, but 638 will give an IOError("invalid mode") if you attempt to create a filename 639 with a space at the end. However, if a period is placed at the end of 640 the name, Linux/FAT32 is consistent with Cygwin and Native Windows. 641 """ 642 643 period_rp = testdir.append("foo.") 644 assert not period_rp.lstat() 645 646 tmp_rp = testdir.append("foo") 647 tmp_rp.touch() 648 assert tmp_rp.lstat() 649 650 period_rp.setdata() 651 if period_rp.lstat(): 652 self.escape_trailing_spaces = 1 653 else: 654 self.escape_trailing_spaces = 0 655 656 tmp_rp.delete() 657 658 def set_escape_trailing_spaces_readonly(self, rp): 659 """Determine if directory at rp permits filenames with trailing 660 spaces or periods without writing.""" 661 662 def test_period(dir_rp, dirlist): 663 """Return 1 if trailing spaces and periods should be escaped""" 664 filename = dirlist[0] 665 try: 666 test_rp = dir_rp.append(filename) 667 except OSError: 668 return 0 669 assert test_rp.lstat(), test_rp 670 period = filename + b'.' 671 if period in dirlist: 672 return 0 673 674 return 0 # FIXME the following lines fail if filename is almost too long 675 period_rp = dir_rp.append(period) 676 if period_rp.lstat(): 677 return 1 678 return 0 679 680 dirlist = robust.listrp(rp) 681 if len(dirlist): 682 self.escape_trailing_spaces = test_period(rp, dirlist) 683 else: 684 log.Log( 685 "Warning: could not determine if source directory at\n" 686 " %s\npermits trailing spaces or periods in " 687 "filenames because we can't find any files.\n" 688 "It will be treated as permitting such files." % 689 rp.get_safepath(), 2) 690 self.escape_trailing_spaces = 0 691 692 693def get_readonly_fsa(desc_string, rp): 694 """Return an fsa with given description_string 695 696 Will be initialized read_only with given RPath rp. We separate 697 this out into a separate function so the request can be vetted by 698 the security module. 699 700 """ 701 if os.name == 'nt': 702 log.Log("Hardlinks disabled by default on Windows", 4) 703 SetConnections.UpdateGlobal('preserve_hardlinks', 0) 704 return FSAbilities(desc_string).init_readonly(rp) 705 706 707class SetGlobals: 708 """Various functions for setting Globals vars given FSAbilities above 709 710 Container for BackupSetGlobals and RestoreSetGlobals (don't use directly) 711 712 """ 713 714 def __init__(self, in_conn, out_conn, src_fsa, dest_fsa): 715 """Just store some variables for use below""" 716 self.in_conn, self.out_conn = in_conn, out_conn 717 self.src_fsa, self.dest_fsa = src_fsa, dest_fsa 718 719 def set_eas(self): 720 self.update_triple(self.src_fsa.eas, self.dest_fsa.eas, 721 ('eas_active', 'eas_write', 'eas_conn')) 722 723 def set_acls(self): 724 self.update_triple(self.src_fsa.acls, self.dest_fsa.acls, 725 ('acls_active', 'acls_write', 'acls_conn')) 726 if Globals.never_drop_acls and not Globals.acls_active: 727 log.Log.FatalError("--never-drop-acls specified, but ACL support\n" 728 "missing from source filesystem") 729 730 def set_win_acls(self): 731 self.update_triple( 732 self.src_fsa.win_acls, self.dest_fsa.win_acls, 733 ('win_acls_active', 'win_acls_write', 'win_acls_conn')) 734 735 def set_resource_forks(self): 736 self.update_triple(self.src_fsa.resource_forks, 737 self.dest_fsa.resource_forks, 738 ('resource_forks_active', 'resource_forks_write', 739 'resource_forks_conn')) 740 741 def set_carbonfile(self): 742 self.update_triple( 743 self.src_fsa.carbonfile, self.dest_fsa.carbonfile, 744 ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn')) 745 746 def set_hardlinks(self): 747 if Globals.preserve_hardlinks != 0: 748 SetConnections.UpdateGlobal('preserve_hardlinks', 749 self.dest_fsa.hardlinks) 750 751 def set_fsync_directories(self): 752 SetConnections.UpdateGlobal('fsync_directories', 753 self.dest_fsa.fsync_dirs) 754 755 def set_change_ownership(self): 756 SetConnections.UpdateGlobal('change_ownership', 757 self.dest_fsa.ownership) 758 759 def set_high_perms(self): 760 if not self.dest_fsa.high_perms: 761 SetConnections.UpdateGlobal('permission_mask', 0o777) 762 763 def set_symlink_perms(self): 764 SetConnections.UpdateGlobal('symlink_perms', 765 self.dest_fsa.symlink_perms) 766 767 def set_compatible_timestamps(self): 768 if Globals.chars_to_quote.find(b":") > -1: 769 SetConnections.UpdateGlobal('use_compatible_timestamps', 1) 770 Time.setcurtime( 771 Time.curtime) # update Time.curtimestr on all conns 772 log.Log("Enabled use_compatible_timestamps", 4) 773 774 775class BackupSetGlobals(SetGlobals): 776 """Functions for setting fsa related globals for backup session""" 777 778 def update_triple(self, src_support, dest_support, attr_triple): 779 """Many of the settings have a common form we can handle here""" 780 active_attr, write_attr, conn_attr = attr_triple 781 if Globals.get(active_attr) == 0: 782 return # don't override 0 783 for attr in attr_triple: 784 SetConnections.UpdateGlobal(attr, None) 785 if not src_support: 786 return # if source doesn't support, nothing 787 SetConnections.UpdateGlobal(active_attr, 1) 788 self.in_conn.Globals.set_local(conn_attr, 1) 789 if dest_support: 790 SetConnections.UpdateGlobal(write_attr, 1) 791 self.out_conn.Globals.set_local(conn_attr, 1) 792 793 def set_special_escapes(self, rbdir): 794 """Escaping DOS devices and trailing periods/spaces works like 795 regular filename escaping. If only the destination requires it, 796 then we do it. Otherwise, it is not necessary, since the files 797 couldn't have been created in the first place. We also record 798 whether we have done it in order to handle the case where a 799 volume which was escaped is later restored by an OS that does 800 not require it. 801 802 """ 803 804 suggested_edd = (self.dest_fsa.escape_dos_devices 805 and not self.src_fsa.escape_dos_devices) 806 suggested_ets = (self.dest_fsa.escape_trailing_spaces 807 and not self.src_fsa.escape_trailing_spaces) 808 809 se_rp = rbdir.append("special_escapes") 810 if not se_rp.lstat(): 811 actual_edd, actual_ets = suggested_edd, suggested_ets 812 se = "" 813 if actual_edd: 814 se = se + "escape_dos_devices\n" 815 if actual_ets: 816 se = se + "escape_trailing_spaces\n" 817 se_rp.write_string(se) 818 else: 819 se = se_rp.get_string().split("\n") 820 actual_edd = ("escape_dos_devices" in se) 821 actual_ets = ("escape_trailing_spaces" in se) 822 823 if actual_edd != suggested_edd and not suggested_edd: 824 log.Log( 825 "Warning: System no longer needs DOS devices escaped, " 826 "but we will retain for backwards compatibility.", 2) 827 if actual_ets != suggested_ets and not suggested_ets: 828 log.Log( 829 "Warning: System no longer needs trailing spaces or " 830 "periods escaped, but we will retain for backwards " 831 "compatibility.", 2) 832 833 SetConnections.UpdateGlobal('escape_dos_devices', actual_edd) 834 log.Log("Backup: escape_dos_devices = %d" % actual_edd, 4) 835 836 SetConnections.UpdateGlobal('escape_trailing_spaces', actual_ets) 837 log.Log("Backup: escape_trailing_spaces = %d" % actual_ets, 4) 838 839 def set_chars_to_quote(self, rbdir, force): 840 """Set chars_to_quote setting for backup session 841 842 Unlike most other options, the chars_to_quote setting also 843 depends on the current settings in the rdiff-backup-data 844 directory, not just the current fs features. 845 846 """ 847 (ctq, update) = self.compare_ctq_file(rbdir, self.get_ctq_from_fsas(), 848 force) 849 850 SetConnections.UpdateGlobal('chars_to_quote', ctq) 851 if Globals.chars_to_quote: 852 FilenameMapping.set_init_quote_vals() 853 return update 854 855 def get_ctq_from_fsas(self): 856 """Determine chars_to_quote just from filesystems, no ctq file""" 857 ctq = [] 858 859 if self.src_fsa.case_sensitive and not self.dest_fsa.case_sensitive: 860 ctq.append(b"A-Z") # Quote upper case 861 if not self.dest_fsa.extended_filenames: 862 ctq.append(b'\000-\037') # Quote 0 - 31 863 ctq.append(b'\200-\377') # Quote non-ASCII characters 0x80 - 0xFF 864 if self.dest_fsa.win_reserved_filenames: 865 if self.dest_fsa.extended_filenames: 866 ctq.append(b'\000-\037') # Quote 0 - 31 867 # Quote ", *, /, :, <, >, ?, \, |, and 127 (DEL) 868 ctq.append(b'\"*/:<>?\\\\|\177') 869 870 # Quote quoting char if quoting anything 871 if ctq: 872 ctq.append(Globals.quoting_char) 873 return b"".join(ctq) 874 875 def compare_ctq_file(self, rbdir, suggested_ctq, force): 876 """Compare ctq file with suggested result, return actual ctq""" 877 ctq_rp = rbdir.append(b"chars_to_quote") 878 if not ctq_rp.lstat(): 879 if Globals.chars_to_quote is None: 880 actual_ctq = suggested_ctq 881 else: 882 actual_ctq = Globals.chars_to_quote 883 ctq_rp.write_bytes(actual_ctq) 884 return (actual_ctq, None) 885 886 if Globals.chars_to_quote is None: 887 actual_ctq = ctq_rp.get_bytes() 888 else: 889 actual_ctq = Globals.chars_to_quote # Globals override 890 891 if actual_ctq == suggested_ctq: 892 return (actual_ctq, None) 893 if suggested_ctq == b"": 894 log.Log( 895 "Warning: File system no longer needs quoting, " 896 "but we will retain for backwards compatibility.", 2) 897 return (actual_ctq, None) 898 if Globals.chars_to_quote is None: 899 if force: 900 log.Log( 901 "Warning: migrating rdiff-backup repository from" 902 "old quoting chars %r to new quoting chars %r" % 903 (actual_ctq, suggested_ctq), 2) 904 ctq_rp.delete() 905 ctq_rp.write_bytes(suggested_ctq) 906 return (suggested_ctq, 1) 907 else: 908 log.Log.FatalError( 909 """New quoting requirements! 910 911The quoting chars this session needs %r do not match 912the repository settings %r listed in 913 914%s 915 916This may be caused when you copy an rdiff-backup repository from a 917normal file system onto a windows one that cannot support the same 918characters, or if you backup a case-sensitive file system onto a 919case-insensitive one that previously only had case-insensitive ones 920backed up onto it. 921 922By specifying the --force option, rdiff-backup will migrate the 923repository from the old quoting chars to the new ones.""" % 924 (suggested_ctq, actual_ctq, ctq_rp.get_safepath())) 925 return (actual_ctq, None) # Maintain Globals override 926 927 928class RestoreSetGlobals(SetGlobals): 929 """Functions for setting fsa-related globals for restore session""" 930 931 def update_triple(self, src_support, dest_support, attr_triple): 932 """Update global settings for feature based on fsa results 933 934 This is slightly different from BackupSetGlobals.update_triple 935 because (using the mirror_metadata file) rpaths from the 936 source may have more information than the file system 937 supports. 938 939 """ 940 active_attr, write_attr, conn_attr = attr_triple 941 if Globals.get(active_attr) == 0: 942 return # don't override 0 943 for attr in attr_triple: 944 SetConnections.UpdateGlobal(attr, None) 945 if not dest_support: 946 return # if dest doesn't support, do nothing 947 SetConnections.UpdateGlobal(active_attr, 1) 948 self.out_conn.Globals.set_local(conn_attr, 1) 949 self.out_conn.Globals.set_local(write_attr, 1) 950 if src_support: 951 self.in_conn.Globals.set_local(conn_attr, 1) 952 953 def set_special_escapes(self, rbdir): 954 """Set escape_dos_devices and escape_trailing_spaces from 955 rdiff-backup-data dir, just like chars_to_quote""" 956 se_rp = rbdir.append("special_escapes") 957 if se_rp.lstat(): 958 se = se_rp.get_string().split("\n") 959 actual_edd = ("escape_dos_devices" in se) 960 actual_ets = ("escape_trailing_spaces" in se) 961 else: 962 log.Log( 963 "Warning: special_escapes file not found,\n" 964 "will assume need to escape DOS devices and trailing " 965 "spaces based on file systems.", 2) 966 if getattr(self, "src_fsa", None) is not None: 967 actual_edd = (self.src_fsa.escape_dos_devices 968 and not self.dest_fsa.escape_dos_devices) 969 actual_ets = (self.src_fsa.escape_trailing_spaces 970 and not self.dest_fsa.escape_trailing_spaces) 971 else: 972 # Single filesystem operation 973 actual_edd = self.dest_fsa.escape_dos_devices 974 actual_ets = self.dest_fsa.escape_trailing_spaces 975 976 SetConnections.UpdateGlobal('escape_dos_devices', actual_edd) 977 log.Log("Backup: escape_dos_devices = %d" % actual_edd, 4) 978 979 SetConnections.UpdateGlobal('escape_trailing_spaces', actual_ets) 980 log.Log("Backup: escape_trailing_spaces = %d" % actual_ets, 4) 981 982 def set_chars_to_quote(self, rbdir): 983 """Set chars_to_quote from rdiff-backup-data dir""" 984 if Globals.chars_to_quote is not None: 985 return # already overridden 986 987 ctq_rp = rbdir.append(b"chars_to_quote") 988 if ctq_rp.lstat(): 989 SetConnections.UpdateGlobal("chars_to_quote", ctq_rp.get_bytes()) 990 else: 991 log.Log( 992 "Warning: chars_to_quote file not found,\n" 993 "assuming no quoting in backup repository.", 2) 994 SetConnections.UpdateGlobal("chars_to_quote", b"") 995 996 997class SingleSetGlobals(RestoreSetGlobals): 998 """For setting globals when dealing only with one filesystem""" 999 1000 def __init__(self, conn, fsa): 1001 self.conn = conn 1002 self.dest_fsa = fsa 1003 1004 def update_triple(self, fsa_support, attr_triple): 1005 """Update global vars from single fsa test""" 1006 active_attr, write_attr, conn_attr = attr_triple 1007 if Globals.get(active_attr) == 0: 1008 return # don't override 0 1009 for attr in attr_triple: 1010 SetConnections.UpdateGlobal(attr, None) 1011 if not fsa_support: 1012 return 1013 SetConnections.UpdateGlobal(active_attr, 1) 1014 SetConnections.UpdateGlobal(write_attr, 1) 1015 self.conn.Globals.set_local(conn_attr, 1) 1016 1017 def set_eas(self): 1018 self.update_triple(self.dest_fsa.eas, 1019 ('eas_active', 'eas_write', 'eas_conn')) 1020 1021 def set_acls(self): 1022 self.update_triple(self.dest_fsa.acls, 1023 ('acls_active', 'acls_write', 'acls_conn')) 1024 1025 def set_win_acls(self): 1026 self.update_triple( 1027 self.src_fsa.win_acls, self.dest_fsa.win_acls, 1028 ('win_acls_active', 'win_acls_write', 'win_acls_conn')) 1029 1030 def set_resource_forks(self): 1031 self.update_triple(self.dest_fsa.resource_forks, 1032 ('resource_forks_active', 'resource_forks_write', 1033 'resource_forks_conn')) 1034 1035 def set_carbonfile(self): 1036 self.update_triple( 1037 self.dest_fsa.carbonfile, 1038 ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn')) 1039 1040 1041def backup_set_globals(rpin, force): 1042 """Given rps for source filesystem and repository, set fsa globals 1043 1044 This should be run on the destination connection, because we may 1045 need to write a new chars_to_quote file. 1046 1047 """ 1048 assert Globals.rbdir.conn is Globals.local_connection 1049 src_fsa = rpin.conn.fs_abilities.get_readonly_fsa('source', rpin) 1050 log.Log(str(src_fsa), 4) 1051 dest_fsa = FSAbilities('destination').init_readwrite(Globals.rbdir) 1052 log.Log(str(dest_fsa), 4) 1053 1054 bsg = BackupSetGlobals(rpin.conn, Globals.rbdir.conn, src_fsa, dest_fsa) 1055 bsg.set_eas() 1056 bsg.set_acls() 1057 bsg.set_win_acls() 1058 bsg.set_resource_forks() 1059 bsg.set_carbonfile() 1060 bsg.set_hardlinks() 1061 bsg.set_fsync_directories() 1062 bsg.set_change_ownership() 1063 bsg.set_high_perms() 1064 bsg.set_symlink_perms() 1065 update_quoting = bsg.set_chars_to_quote(Globals.rbdir, force) 1066 bsg.set_special_escapes(Globals.rbdir) 1067 bsg.set_compatible_timestamps() 1068 1069 if update_quoting and force: 1070 FilenameMapping.update_quoting(Globals.rbdir) 1071 1072 1073def restore_set_globals(rpout): 1074 """Set fsa related globals for restore session, given in/out rps""" 1075 assert rpout.conn is Globals.local_connection 1076 src_fsa = Globals.rbdir.conn.fs_abilities.get_readonly_fsa( 1077 'rdiff-backup repository', Globals.rbdir) 1078 log.Log(str(src_fsa), 4) 1079 dest_fsa = FSAbilities('restore target').init_readwrite(rpout) 1080 log.Log(str(dest_fsa), 4) 1081 1082 rsg = RestoreSetGlobals(Globals.rbdir.conn, rpout.conn, src_fsa, dest_fsa) 1083 rsg.set_eas() 1084 rsg.set_acls() 1085 rsg.set_win_acls() 1086 rsg.set_resource_forks() 1087 rsg.set_carbonfile() 1088 rsg.set_hardlinks() 1089 # No need to fsync anything when restoring 1090 rsg.set_change_ownership() 1091 rsg.set_high_perms() 1092 rsg.set_symlink_perms() 1093 rsg.set_chars_to_quote(Globals.rbdir) 1094 rsg.set_special_escapes(Globals.rbdir) 1095 rsg.set_compatible_timestamps() 1096 1097 1098def single_set_globals(rp, read_only=None): 1099 """Set fsa related globals for operation on single filesystem""" 1100 if read_only: 1101 fsa = rp.conn.fs_abilities.get_readonly_fsa(rp.path, rp) 1102 else: 1103 fsa = FSAbilities(rp.path).init_readwrite(rp) 1104 log.Log(str(fsa), 4) 1105 1106 ssg = SingleSetGlobals(rp.conn, fsa) 1107 ssg.set_eas() 1108 ssg.set_acls() 1109 ssg.set_resource_forks() 1110 ssg.set_carbonfile() 1111 if not read_only: 1112 ssg.set_hardlinks() 1113 ssg.set_change_ownership() 1114 ssg.set_high_perms() 1115 ssg.set_symlink_perms() 1116 ssg.set_chars_to_quote(Globals.rbdir) 1117 ssg.set_special_escapes(Globals.rbdir) 1118 ssg.set_compatible_timestamps() 1119