Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(10)

Delta Between Two Patch Sets: Lib/shutil.py

Issue 21697: shutil.copytree() handles symbolic directory incorrectly
Left Patch Set: Created 5 years, 8 months ago
Right Patch Set: Created 4 years, 7 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | Lib/test/test_shutil.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 """Utility functions for copying and archiving files and directory trees. 1 """Utility functions for copying and archiving files and directory trees.
2 2
3 XXX The functions here don't copy the resource fork or other metadata on Mac. 3 XXX The functions here don't copy the resource fork or other metadata on Mac.
4 4
5 """ 5 """
6 6
7 import os 7 import os
8 import sys 8 import sys
9 import stat 9 import stat
10 from os.path import abspath
11 import fnmatch 10 import fnmatch
12 import collections 11 import collections
13 import errno 12 import errno
14 import tarfile 13 import tarfile
15 14
16 try: 15 try:
17 import bz2 16 import bz2
18 del bz2 17 del bz2
19 _BZ2_SUPPORTED = True 18 _BZ2_SUPPORTED = True
20 except ImportError: 19 except ImportError:
21 _BZ2_SUPPORTED = False 20 _BZ2_SUPPORTED = False
22 21
23 try: 22 try:
23 import lzma
24 del lzma
25 _LZMA_SUPPORTED = True
26 except ImportError:
27 _LZMA_SUPPORTED = False
28
29 try:
24 from pwd import getpwnam 30 from pwd import getpwnam
25 except ImportError: 31 except ImportError:
26 getpwnam = None 32 getpwnam = None
27 33
28 try: 34 try:
29 from grp import getgrnam 35 from grp import getgrnam
30 except ImportError: 36 except ImportError:
31 getgrnam = None 37 getgrnam = None
32 38
33 __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", 39 __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2",
34 "copytree", "move", "rmtree", "Error", "SpecialFileError", 40 "copytree", "move", "rmtree", "Error", "SpecialFileError",
35 "ExecError", "make_archive", "get_archive_formats", 41 "ExecError", "make_archive", "get_archive_formats",
36 "register_archive_format", "unregister_archive_format", 42 "register_archive_format", "unregister_archive_format",
37 "get_unpack_formats", "register_unpack_format", 43 "get_unpack_formats", "register_unpack_format",
38 "unregister_unpack_format", "unpack_archive", 44 "unregister_unpack_format", "unpack_archive",
39 "ignore_patterns", "chown", "which"] 45 "ignore_patterns", "chown", "which", "get_terminal_size",
46 "SameFileError"]
40 # disk_usage is added later, if available on the platform 47 # disk_usage is added later, if available on the platform
41 48
42 class Error(OSError): 49 class Error(OSError):
43 pass 50 pass
44 51
45 class SameFileError(Error): 52 class SameFileError(Error):
46 """Raised when source and destination are the same file.""" 53 """Raised when source and destination are the same file."""
47 54
48 class SpecialFileError(OSError): 55 class SpecialFileError(OSError):
49 """Raised when trying to do a kind of operation (e.g. copying) which is 56 """Raised when trying to do a kind of operation (e.g. copying) which is
(...skipping 250 matching lines...) Expand 10 before | Expand all | Expand 10 after
300 ignored_names = set() 307 ignored_names = set()
301 308
302 os.makedirs(dst) 309 os.makedirs(dst)
303 errors = [] 310 errors = []
304 for name in names: 311 for name in names:
305 if name in ignored_names: 312 if name in ignored_names:
306 continue 313 continue
307 srcname = os.path.join(src, name) 314 srcname = os.path.join(src, name)
308 dstname = os.path.join(dst, name) 315 dstname = os.path.join(dst, name)
309 try: 316 try:
310 if os.path.islink(srcname) and not os.path.isdir(srcname): 317 if os.path.islink(srcname):
311 linkto = os.readlink(srcname) 318 linkto = os.readlink(srcname)
312 if symlinks: 319 if symlinks:
313 # We can't just leave it to `copy_function` because legacy 320 # We can't just leave it to `copy_function` because legacy
314 # code with a custom `copy_function` may rely on copytree 321 # code with a custom `copy_function` may rely on copytree
315 # doing the right thing. 322 # doing the right thing.
316 os.symlink(linkto, dstname) 323 os.symlink(linkto, dstname)
317 copystat(srcname, dstname, follow_symlinks=not symlinks) 324 copystat(srcname, dstname, follow_symlinks=not symlinks)
318 else: 325 else:
319 # ignore dangling symlink if the flag is on 326 # ignore dangling symlink if the flag is on
320 if not os.path.exists(linkto) and ignore_dangling_symlinks: 327 if not os.path.exists(linkto) and ignore_dangling_symlinks:
321 continue 328 continue
322 # otherwise let the copy occurs. copy2 will raise an error 329 # otherwise let the copy occurs. copy2 will raise an error
323 copy_function(srcname, dstname) 330 if os.path.isdir(srcname):
331 copytree(srcname, dstname, symlinks, ignore,
332 copy_function)
333 else:
334 copy_function(srcname, dstname)
324 elif os.path.isdir(srcname): 335 elif os.path.isdir(srcname):
325 copytree(srcname, dstname, symlinks, ignore, copy_function) 336 copytree(srcname, dstname, symlinks, ignore, copy_function)
326 else: 337 else:
327 # Will raise a SpecialFileError for unsupported file types 338 # Will raise a SpecialFileError for unsupported file types
328 copy_function(srcname, dstname) 339 copy_function(srcname, dstname)
329 # catch the Error from the recursive copytree so that we can 340 # catch the Error from the recursive copytree so that we can
330 # continue with other files 341 # continue with other files
331 except Error as err: 342 except Error as err:
332 errors.extend(err.args[0]) 343 errors.extend(err.args[0])
333 except OSError as why: 344 except OSError as why:
334 errors.append((srcname, dstname, str(why))) 345 errors.append((srcname, dstname, str(why)))
335 try: 346 try:
336 copystat(src, dst) 347 copystat(src, dst)
337 except OSError as why: 348 except OSError as why:
338 # Copying file access times may fail on Windows 349 # Copying file access times may fail on Windows
339 if why.winerror is None: 350 if getattr(why, 'winerror', None) is None:
340 errors.append((src, dst, str(why))) 351 errors.append((src, dst, str(why)))
341 if errors: 352 if errors:
342 raise Error(errors) 353 raise Error(errors)
343 return dst 354 return dst
344 355
345 # version vulnerable to race conditions 356 # version vulnerable to race conditions
346 def _rmtree_unsafe(path, onerror): 357 def _rmtree_unsafe(path, onerror):
347 try: 358 try:
348 if os.path.islink(path): 359 if os.path.islink(path):
349 # symlinks to directories are forbidden, see bug #1669 360 # symlinks to directories are forbidden, see bug #1669
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
536 " '%s'." % (src, dst)) 547 " '%s'." % (src, dst))
537 copytree(src, real_dst, copy_function=copy_function, 548 copytree(src, real_dst, copy_function=copy_function,
538 symlinks=True) 549 symlinks=True)
539 rmtree(src) 550 rmtree(src)
540 else: 551 else:
541 copy_function(src, real_dst) 552 copy_function(src, real_dst)
542 os.unlink(src) 553 os.unlink(src)
543 return real_dst 554 return real_dst
544 555
545 def _destinsrc(src, dst): 556 def _destinsrc(src, dst):
546 src = abspath(src) 557 src = os.path.abspath(src)
547 dst = abspath(dst) 558 dst = os.path.abspath(dst)
548 if not src.endswith(os.path.sep): 559 if not src.endswith(os.path.sep):
549 src += os.path.sep 560 src += os.path.sep
550 if not dst.endswith(os.path.sep): 561 if not dst.endswith(os.path.sep):
551 dst += os.path.sep 562 dst += os.path.sep
552 return dst.startswith(src) 563 return dst.startswith(src)
553 564
554 def _get_gid(name): 565 def _get_gid(name):
555 """Returns a gid, given a group name.""" 566 """Returns a gid, given a group name."""
556 if getgrnam is None or name is None: 567 if getgrnam is None or name is None:
557 return None 568 return None
(...skipping 15 matching lines...) Expand all
573 result = None 584 result = None
574 if result is not None: 585 if result is not None:
575 return result[2] 586 return result[2]
576 return None 587 return None
577 588
578 def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0, 589 def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0,
579 owner=None, group=None, logger=None): 590 owner=None, group=None, logger=None):
580 """Create a (possibly compressed) tar file from all the files under 591 """Create a (possibly compressed) tar file from all the files under
581 'base_dir'. 592 'base_dir'.
582 593
583 'compress' must be "gzip" (the default), "bzip2", or None. 594 'compress' must be "gzip" (the default), "bzip2", "xz", or None.
584 595
585 'owner' and 'group' can be used to define an owner and a group for the 596 'owner' and 'group' can be used to define an owner and a group for the
586 archive that is being built. If not provided, the current owner and group 597 archive that is being built. If not provided, the current owner and group
587 will be used. 598 will be used.
588 599
589 The output tar file will be named 'base_name' + ".tar", possibly plus 600 The output tar file will be named 'base_name' + ".tar", possibly plus
590 the appropriate compression extension (".gz", or ".bz2"). 601 the appropriate compression extension (".gz", ".bz2", or ".xz").
591 602
592 Returns the output filename. 603 Returns the output filename.
593 """ 604 """
594 tar_compression = {'gzip': 'gz', None: ''} 605 tar_compression = {'gzip': 'gz', None: ''}
595 compress_ext = {'gzip': '.gz'} 606 compress_ext = {'gzip': '.gz'}
596 607
597 if _BZ2_SUPPORTED: 608 if _BZ2_SUPPORTED:
598 tar_compression['bzip2'] = 'bz2' 609 tar_compression['bzip2'] = 'bz2'
599 compress_ext['bzip2'] = '.bz2' 610 compress_ext['bzip2'] = '.bz2'
611
612 if _LZMA_SUPPORTED:
613 tar_compression['xz'] = 'xz'
614 compress_ext['xz'] = '.xz'
600 615
601 # flags for compression program, each element of list will be an argument 616 # flags for compression program, each element of list will be an argument
602 if compress is not None and compress not in compress_ext: 617 if compress is not None and compress not in compress_ext:
603 raise ValueError("bad value for 'compress', or compression format not " 618 raise ValueError("bad value for 'compress', or compression format not "
604 "supported : {0}".format(compress)) 619 "supported : {0}".format(compress))
605 620
606 archive_name = base_name + '.tar' + compress_ext.get(compress, '') 621 archive_name = base_name + '.tar' + compress_ext.get(compress, '')
607 archive_dir = os.path.dirname(archive_name) 622 archive_dir = os.path.dirname(archive_name)
608 623
609 if not os.path.exists(archive_dir): 624 if archive_dir and not os.path.exists(archive_dir):
610 if logger is not None: 625 if logger is not None:
611 logger.info("creating %s", archive_dir) 626 logger.info("creating %s", archive_dir)
612 if not dry_run: 627 if not dry_run:
613 os.makedirs(archive_dir) 628 os.makedirs(archive_dir)
614 629
615 # creating the tarball 630 # creating the tarball
616 if logger is not None: 631 if logger is not None:
617 logger.info('Creating tar archive') 632 logger.info('Creating tar archive')
618 633
619 uid = _get_uid(owner) 634 uid = _get_uid(owner)
(...skipping 24 matching lines...) Expand all
644 "zipfile" Python module (if available) or the InfoZIP "zip" utility 659 "zipfile" Python module (if available) or the InfoZIP "zip" utility
645 (if installed and found on the default search path). If neither tool is 660 (if installed and found on the default search path). If neither tool is
646 available, raises ExecError. Returns the name of the output zip 661 available, raises ExecError. Returns the name of the output zip
647 file. 662 file.
648 """ 663 """
649 import zipfile 664 import zipfile
650 665
651 zip_filename = base_name + ".zip" 666 zip_filename = base_name + ".zip"
652 archive_dir = os.path.dirname(base_name) 667 archive_dir = os.path.dirname(base_name)
653 668
654 if not os.path.exists(archive_dir): 669 if archive_dir and not os.path.exists(archive_dir):
655 if logger is not None: 670 if logger is not None:
656 logger.info("creating %s", archive_dir) 671 logger.info("creating %s", archive_dir)
657 if not dry_run: 672 if not dry_run:
658 os.makedirs(archive_dir) 673 os.makedirs(archive_dir)
659 674
660 if logger is not None: 675 if logger is not None:
661 logger.info("creating '%s' and adding '%s' to it", 676 logger.info("creating '%s' and adding '%s' to it",
662 zip_filename, base_dir) 677 zip_filename, base_dir)
663 678
664 if not dry_run: 679 if not dry_run:
(...skipping 11 matching lines...) Expand all
676 691
677 _ARCHIVE_FORMATS = { 692 _ARCHIVE_FORMATS = {
678 'gztar': (_make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"), 693 'gztar': (_make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
679 'tar': (_make_tarball, [('compress', None)], "uncompressed tar file"), 694 'tar': (_make_tarball, [('compress', None)], "uncompressed tar file"),
680 'zip': (_make_zipfile, [], "ZIP file") 695 'zip': (_make_zipfile, [], "ZIP file")
681 } 696 }
682 697
683 if _BZ2_SUPPORTED: 698 if _BZ2_SUPPORTED:
684 _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')], 699 _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')],
685 "bzip2'ed tar-file") 700 "bzip2'ed tar-file")
701
702 if _LZMA_SUPPORTED:
703 _ARCHIVE_FORMATS['xztar'] = (_make_tarball, [('compress', 'xz')],
704 "xz'ed tar-file")
686 705
687 def get_archive_formats(): 706 def get_archive_formats():
688 """Returns a list of supported formats for archiving and unarchiving. 707 """Returns a list of supported formats for archiving and unarchiving.
689 708
690 Each element of the returned sequence is a tuple (name, description) 709 Each element of the returned sequence is a tuple (name, description)
691 """ 710 """
692 formats = [(name, registry[2]) for name, registry in 711 formats = [(name, registry[2]) for name, registry in
693 _ARCHIVE_FORMATS.items()] 712 _ARCHIVE_FORMATS.items()]
694 formats.sort() 713 formats.sort()
695 return formats 714 return formats
(...skipping 169 matching lines...) Expand 10 before | Expand all | Expand 10 after
865 f = open(target, 'wb') 884 f = open(target, 'wb')
866 try: 885 try:
867 f.write(data) 886 f.write(data)
868 finally: 887 finally:
869 f.close() 888 f.close()
870 del data 889 del data
871 finally: 890 finally:
872 zip.close() 891 zip.close()
873 892
874 def _unpack_tarfile(filename, extract_dir): 893 def _unpack_tarfile(filename, extract_dir):
875 """Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir` 894 """Unpack tar/tar.gz/tar.bz2/tar.xz `filename` to `extract_dir`
876 """ 895 """
877 try: 896 try:
878 tarobj = tarfile.open(filename) 897 tarobj = tarfile.open(filename)
879 except tarfile.TarError: 898 except tarfile.TarError:
880 raise ReadError( 899 raise ReadError(
881 "%s is not a compressed or uncompressed tar file" % filename) 900 "%s is not a compressed or uncompressed tar file" % filename)
882 try: 901 try:
883 tarobj.extractall(extract_dir) 902 tarobj.extractall(extract_dir)
884 finally: 903 finally:
885 tarobj.close() 904 tarobj.close()
886 905
887 _UNPACK_FORMATS = { 906 _UNPACK_FORMATS = {
888 'gztar': (['.tar.gz', '.tgz'], _unpack_tarfile, [], "gzip'ed tar-file"), 907 'gztar': (['.tar.gz', '.tgz'], _unpack_tarfile, [], "gzip'ed tar-file"),
889 'tar': (['.tar'], _unpack_tarfile, [], "uncompressed tar file"), 908 'tar': (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
890 'zip': (['.zip'], _unpack_zipfile, [], "ZIP file") 909 'zip': (['.zip'], _unpack_zipfile, [], "ZIP file")
891 } 910 }
892 911
893 if _BZ2_SUPPORTED: 912 if _BZ2_SUPPORTED:
894 _UNPACK_FORMATS['bztar'] = (['.bz2'], _unpack_tarfile, [], 913 _UNPACK_FORMATS['bztar'] = (['.tar.bz2', '.tbz2'], _unpack_tarfile, [],
895 "bzip2'ed tar-file") 914 "bzip2'ed tar-file")
915
916 if _LZMA_SUPPORTED:
917 _UNPACK_FORMATS['xztar'] = (['.tar.xz', '.txz'], _unpack_tarfile, [],
918 "xz'ed tar-file")
896 919
897 def _find_unpack_format(filename): 920 def _find_unpack_format(filename):
898 for name, info in _UNPACK_FORMATS.items(): 921 for name, info in _UNPACK_FORMATS.items():
899 for extension in info[0]: 922 for extension in info[0]:
900 if filename.endswith(extension): 923 if filename.endswith(extension):
901 return name 924 return name
902 return None 925 return None
903 926
904 def unpack_archive(filename, extract_dir=None, format=None): 927 def unpack_archive(filename, extract_dir=None, format=None):
905 """Unpack an archive. 928 """Unpack an archive.
(...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after
1100 seen = set() 1123 seen = set()
1101 for dir in path: 1124 for dir in path:
1102 normdir = os.path.normcase(dir) 1125 normdir = os.path.normcase(dir)
1103 if not normdir in seen: 1126 if not normdir in seen:
1104 seen.add(normdir) 1127 seen.add(normdir)
1105 for thefile in files: 1128 for thefile in files:
1106 name = os.path.join(dir, thefile) 1129 name = os.path.join(dir, thefile)
1107 if _access_check(name, mode): 1130 if _access_check(name, mode):
1108 return name 1131 return name
1109 return None 1132 return None
LEFTRIGHT

RSS Feeds Recent Issues | This issue
This is Rietveld 894c83f36cb7+