diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index c0f2a89a3a1..a2fa9666acc 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -368,7 +368,7 @@ ZipFile Objects .. method:: ZipFile.write(filename, arcname=None, compress_type=None, \ - compresslevel=None) + compresslevel=None, *, strict_timestamps=True) Write the file named *filename* to the archive, giving it the archive name *arcname* (by default, this will be the same as *filename*, but without a drive @@ -377,6 +377,11 @@ ZipFile Objects the new entry. Similarly, *compresslevel* will override the constructor if given. The archive must be open with mode ``'w'``, ``'x'`` or ``'a'``. + The *strict_timestamps* argument, when set to ``False``, allows to + zip files older than 1980-01-01 at the cost of setting the + timestamp to 1980-01-01. + Similar behavior occurs with files newer than 2107-12-31, + the timestamp is also set to the limit. .. note:: @@ -400,6 +405,9 @@ ZipFile Objects a closed ZipFile will raise a :exc:`ValueError`. Previously, a :exc:`RuntimeError` was raised. + .. versionadded:: 3.8 + The *strict_timestamps* keyword-only argument + .. method:: ZipFile.writestr(zinfo_or_arcname, data, compress_type=None, \ compresslevel=None) @@ -540,7 +548,8 @@ information about a single member of the ZIP archive. There is one classmethod to make a :class:`ZipInfo` instance for a filesystem file: -.. classmethod:: ZipInfo.from_file(filename, arcname=None) +.. classmethod:: ZipInfo.from_file(filename, arcname=None, *, \ + strict_timestamps=True) Construct a :class:`ZipInfo` instance for a file on the filesystem, in preparation for adding it to a zip file. @@ -551,11 +560,20 @@ file: If *arcname* is not specified, the name will be the same as *filename*, but with any drive letter and leading path separators removed. + The *strict_timestamps* argument, when set to ``False``, allows to + zip files older than 1980-01-01 at the cost of setting the + timestamp to 1980-01-01. + Similar behavior occurs with files newer than 2107-12-31, + the timestamp is also set to the limit. + .. versionadded:: 3.6 .. versionchanged:: 3.6.2 The *filename* parameter accepts a :term:`path-like object`. + .. versionadded:: 3.8 + The *strict_timestamps* keyword-only argument + Instances have the following methods and attributes: diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index ac9a4ff6fef..3b78b7f1675 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -549,6 +549,22 @@ class StoredTestsWithSourceFile(AbstractTestsWithSourceFile, with zipfile.ZipFile(TESTFN2, "w") as zipfp: self.assertRaises(ValueError, zipfp.write, TESTFN) + with zipfile.ZipFile(TESTFN2, "w") as zipfp: + zipfp.write(TESTFN, strict_timestamps=False) + zinfo = zipfp.getinfo(TESTFN) + self.assertEqual(zinfo.date_time, (1980, 1, 1, 0, 0, 0)) + + def test_add_file_after_2107(self): + # Set atime and mtime to 2108-12-30 + os.utime(TESTFN, (4386268800, 4386268800)) + with zipfile.ZipFile(TESTFN2, "w") as zipfp: + self.assertRaises(struct.error, zipfp.write, TESTFN) + + with zipfile.ZipFile(TESTFN2, "w") as zipfp: + zipfp.write(TESTFN, strict_timestamps=False) + zinfo = zipfp.getinfo(TESTFN) + self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59)) + @requires_zlib class DeflateTestsWithSourceFile(AbstractTestsWithSourceFile, diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 2757ce91cf4..6da1778873d 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -469,7 +469,7 @@ class ZipInfo (object): extra = extra[ln+4:] @classmethod - def from_file(cls, filename, arcname=None): + def from_file(cls, filename, arcname=None, *, strict_timestamps=True): """Construct an appropriate ZipInfo for a file on the filesystem. filename should be the path to a file or directory on the filesystem. @@ -484,6 +484,10 @@ class ZipInfo (object): isdir = stat.S_ISDIR(st.st_mode) mtime = time.localtime(st.st_mtime) date_time = mtime[0:6] + if not strict_timestamps and date_time[0] < 1980: + date_time = (1980, 1, 1, 0, 0, 0) + elif not strict_timestamps and date_time[0] > 2107: + date_time = (2107, 12, 31, 23, 59, 59) # Create ZipInfo instance to store file information if arcname is None: arcname = filename @@ -1673,7 +1677,8 @@ class ZipFile: " would require ZIP64 extensions") def write(self, filename, arcname=None, - compress_type=None, compresslevel=None): + compress_type=None, compresslevel=None, *, + strict_timestamps=True): """Put the bytes from filename into the archive under the name arcname.""" if not self.fp: @@ -1684,7 +1689,8 @@ class ZipFile: "Can't write to ZIP archive while an open writing handle exists" ) - zinfo = ZipInfo.from_file(filename, arcname) + zinfo = ZipInfo.from_file(filename, arcname, + strict_timestamps=strict_timestamps) if zinfo.is_dir(): zinfo.compress_size = 0 diff --git a/Misc/NEWS.d/next/Library/2018-07-13-13-42-10.bpo-34097.F5Dk5o.rst b/Misc/NEWS.d/next/Library/2018-07-13-13-42-10.bpo-34097.F5Dk5o.rst new file mode 100644 index 00000000000..397149559af --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-07-13-13-42-10.bpo-34097.F5Dk5o.rst @@ -0,0 +1,3 @@ +ZipFile can zip files older than 1980-01-01 and newer than 2107-12-31 using +a new ``strict_timestamps`` parameter at the cost of setting the timestamp +to the limit.