Show More
@@ -16,10 +16,10 b' Contains writer for writing nbconvert output to filesystem.' | |||
|
16 | 16 | |
|
17 | 17 | import io |
|
18 | 18 | import os |
|
19 | import shutil | |
|
20 | 19 | import glob |
|
21 | 20 | |
|
22 | 21 | from IPython.utils.traitlets import Unicode |
|
22 | from IPython.utils.path import link_or_copy | |
|
23 | 23 | |
|
24 | 24 | from .base import WriterBase |
|
25 | 25 | |
@@ -90,7 +90,7 b' class FilesWriter(WriterBase):' | |||
|
90 | 90 | |
|
91 | 91 | # Copy if destination is different. |
|
92 | 92 | if not os.path.normpath(dest) == os.path.normpath(matching_filename): |
|
93 |
|
|
|
93 | link_or_copy(matching_filename, dest) | |
|
94 | 94 | |
|
95 | 95 | # Determine where to write conversion results. |
|
96 | 96 | dest = notebook_name + '.' + output_extension |
@@ -16,6 +16,9 b' Utilities for path handling.' | |||
|
16 | 16 | |
|
17 | 17 | import os |
|
18 | 18 | import sys |
|
19 | import errno | |
|
20 | import shutil | |
|
21 | import random | |
|
19 | 22 | import tempfile |
|
20 | 23 | import warnings |
|
21 | 24 | from hashlib import md5 |
@@ -510,3 +513,52 b" def get_security_file(filename, profile='default'):" | |||
|
510 | 513 | raise IOError("Profile %r not found") |
|
511 | 514 | return filefind(filename, ['.', pd.security_dir]) |
|
512 | 515 | |
|
516 | ||
|
517 | ENOLINK = 1998 | |
|
518 | ||
|
519 | def link(src, dst): | |
|
520 | """Hard links ``src`` to ``dst``, returning 0 or errno. | |
|
521 | ||
|
522 | Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't | |
|
523 | supported by the operating system. | |
|
524 | """ | |
|
525 | ||
|
526 | if not hasattr(os, "link"): | |
|
527 | return ENOLINK | |
|
528 | link_errno = 0 | |
|
529 | try: | |
|
530 | os.link(src, dst) | |
|
531 | except OSError as e: | |
|
532 | link_errno = e.errno | |
|
533 | return link_errno | |
|
534 | ||
|
535 | ||
|
536 | def link_or_copy(src, dst): | |
|
537 | """Attempts to hardlink ``src`` to ``dst``, copying if the link fails. | |
|
538 | ||
|
539 | Attempts to maintain the semantics of ``shutil.copy``. | |
|
540 | ||
|
541 | Because ``os.link`` does not overwrite files, a unique temporary file | |
|
542 | will be used if the target already exists, then that file will be moved | |
|
543 | into place. | |
|
544 | """ | |
|
545 | ||
|
546 | if os.path.isdir(dst): | |
|
547 | dst = os.path.join(dst, os.path.basename(src)) | |
|
548 | ||
|
549 | link_errno = link(src, dst) | |
|
550 | if link_errno == errno.EEXIST: | |
|
551 | new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), ) | |
|
552 | try: | |
|
553 | link_or_copy(src, new_dst) | |
|
554 | except: | |
|
555 | try: | |
|
556 | os.remove(new_dst) | |
|
557 | except OSError: | |
|
558 | pass | |
|
559 | raise | |
|
560 | os.rename(new_dst, dst) | |
|
561 | elif link_errno != 0: | |
|
562 | # Either link isn't supported, or the filesystem doesn't support | |
|
563 | # linking, or 'src' and 'dst' are on different filesystems. | |
|
564 | shutil.copy(src, dst) |
@@ -559,3 +559,69 b' def test_unescape_glob():' | |||
|
559 | 559 | nt.assert_equals(path.unescape_glob(r'\\\*'), r'\*') |
|
560 | 560 | nt.assert_equals(path.unescape_glob(r'\\a'), r'\a') |
|
561 | 561 | nt.assert_equals(path.unescape_glob(r'\a'), r'\a') |
|
562 | ||
|
563 | ||
|
564 | class TestLinkOrCopy(object): | |
|
565 | def setUp(self): | |
|
566 | self.tempdir = TemporaryDirectory() | |
|
567 | self.src = self.dst("src") | |
|
568 | with open(self.src, "w") as f: | |
|
569 | f.write("Hello, world!") | |
|
570 | ||
|
571 | def tearDown(self): | |
|
572 | self.tempdir.cleanup() | |
|
573 | ||
|
574 | def dst(self, *args): | |
|
575 | return os.path.join(self.tempdir.name, *args) | |
|
576 | ||
|
577 | def assert_inode_not_equal(self, a, b): | |
|
578 | nt.assert_not_equals(os.stat(a).st_ino, os.stat(b).st_ino, | |
|
579 | "%r and %r do reference the same indoes" %(a, b)) | |
|
580 | ||
|
581 | def assert_inode_equal(self, a, b): | |
|
582 | nt.assert_equals(os.stat(a).st_ino, os.stat(b).st_ino, | |
|
583 | "%r and %r do not reference the same indoes" %(a, b)) | |
|
584 | ||
|
585 | def assert_content_equal(self, a, b): | |
|
586 | with open(a) as a_f: | |
|
587 | with open(b) as b_f: | |
|
588 | nt.assert_equals(a_f.read(), b_f.read()) | |
|
589 | ||
|
590 | @skip_win32 | |
|
591 | def test_link_successful(self): | |
|
592 | dst = self.dst("target") | |
|
593 | path.link_or_copy(self.src, dst) | |
|
594 | self.assert_inode_equal(self.src, dst) | |
|
595 | ||
|
596 | @skip_win32 | |
|
597 | def test_link_into_dir(self): | |
|
598 | dst = self.dst("some_dir") | |
|
599 | os.mkdir(dst) | |
|
600 | path.link_or_copy(self.src, dst) | |
|
601 | expected_dst = self.dst("some_dir", os.path.basename(self.src)) | |
|
602 | self.assert_inode_equal(self.src, expected_dst) | |
|
603 | ||
|
604 | @skip_win32 | |
|
605 | def test_target_exists(self): | |
|
606 | dst = self.dst("target") | |
|
607 | open(dst, "w").close() | |
|
608 | path.link_or_copy(self.src, dst) | |
|
609 | self.assert_inode_equal(self.src, dst) | |
|
610 | ||
|
611 | @skip_win32 | |
|
612 | def test_no_link(self): | |
|
613 | real_link = os.link | |
|
614 | try: | |
|
615 | del os.link | |
|
616 | dst = self.dst("target") | |
|
617 | path.link_or_copy(self.src, dst) | |
|
618 | self.assert_content_equal(self.src, dst) | |
|
619 | self.assert_inode_not_equal(self.src, dst) | |
|
620 | finally: | |
|
621 | os.link = real_link | |
|
622 | ||
|
623 | @skip_if_not_win32 | |
|
624 | def test_windows(self): | |
|
625 | dst = self.dst("target") | |
|
626 | path.link_or_copy(self.src, dst) | |
|
627 | self.assert_content_equal(self.src, dst) |
General Comments 0
You need to be logged in to leave comments.
Login now