-
Couldn't load subscription status.
- Fork 55
Fix timestamp preservation when extracting cached files #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
bdb7fe5
6eee251
a1321b5
d9fc24f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,12 +27,18 @@ | |
| import java.nio.file.PathMatcher; | ||
| import java.nio.file.SimpleFileVisitor; | ||
| import java.nio.file.StandardCopyOption; | ||
| import java.nio.file.attribute.BasicFileAttributeView; | ||
| import java.nio.file.attribute.BasicFileAttributes; | ||
| import java.nio.file.attribute.FileTime; | ||
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.NoSuchElementException; | ||
| import java.util.Set; | ||
| import java.util.stream.Stream; | ||
| import java.util.zip.ZipEntry; | ||
| import java.util.zip.ZipInputStream; | ||
|
|
@@ -154,37 +160,98 @@ public static boolean isArchive(File file) { | |
| * @param dir directory to zip | ||
| * @param zip zip to populate | ||
| * @param glob glob to apply to filenames | ||
| * @param preserveTimestamps whether to preserve file and directory timestamps in the zip. | ||
| * <p><b>Important:</b> When {@code true}, timestamps are stored in ZIP entry headers, | ||
| * which means they become part of the ZIP file's binary content. As a result, hashing | ||
| * the ZIP file (e.g., for cache keys) will include timestamp information, ensuring | ||
| * cache invalidation when file timestamps change. This behavior is similar to how Git | ||
| * includes file mode in tree hashes.</p> | ||
| * @return true if at least one file has been included in the zip. | ||
| * @throws IOException | ||
| */ | ||
| public static boolean zip(final Path dir, final Path zip, final String glob) throws IOException { | ||
| public static boolean zip(final Path dir, final Path zip, final String glob, boolean preserveTimestamps) | ||
| throws IOException { | ||
| final MutableBoolean hasFiles = new MutableBoolean(); | ||
| try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zip))) { | ||
|
|
||
| PathMatcher matcher = | ||
| "*".equals(glob) ? null : FileSystems.getDefault().getPathMatcher("glob:" + glob); | ||
|
|
||
| // Track directories that contain matching files for glob filtering | ||
| final Set<Path> directoriesWithMatchingFiles = new HashSet<>(); | ||
| // Track directory attributes for timestamp preservation | ||
| final Map<Path, BasicFileAttributes> directoryAttributes = | ||
| preserveTimestamps ? new HashMap<>() : Collections.emptyMap(); | ||
|
|
||
| Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { | ||
|
|
||
| @Override | ||
| public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { | ||
| if (preserveTimestamps) { | ||
| // Store attributes for use in postVisitDirectory | ||
| directoryAttributes.put(path, attrs); | ||
| } | ||
| return FileVisitResult.CONTINUE; | ||
| } | ||
|
|
||
| @Override | ||
| public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) | ||
| throws IOException { | ||
|
|
||
| if (matcher == null || matcher.matches(path.getFileName())) { | ||
| if (preserveTimestamps) { | ||
| // Mark all parent directories as containing matching files | ||
| Path parent = path.getParent(); | ||
| while (parent != null && !parent.equals(dir)) { | ||
| directoriesWithMatchingFiles.add(parent); | ||
| parent = parent.getParent(); | ||
| } | ||
| } | ||
|
|
||
| final ZipEntry zipEntry = | ||
| new ZipEntry(dir.relativize(path).toString()); | ||
| if (preserveTimestamps) { | ||
| zipEntry.setTime(basicFileAttributes.lastModifiedTime().toMillis()); | ||
| } | ||
| zipOutputStream.putNextEntry(zipEntry); | ||
| Files.copy(path, zipOutputStream); | ||
| hasFiles.setTrue(); | ||
| zipOutputStream.closeEntry(); | ||
| } | ||
| return FileVisitResult.CONTINUE; | ||
| } | ||
|
|
||
| @Override | ||
| public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException { | ||
| // Propagate any exception that occurred during directory traversal | ||
| if (exc != null) { | ||
| throw exc; | ||
| } | ||
|
|
||
| // Add directory entry only if preserving timestamps and: | ||
| // 1. It's not the root directory, AND | ||
| // 2. Either no glob filter (matcher is null) OR directory contains matching files | ||
| if (preserveTimestamps | ||
| && !path.equals(dir) | ||
| && (matcher == null || directoriesWithMatchingFiles.contains(path))) { | ||
| BasicFileAttributes attrs = directoryAttributes.get(path); | ||
| if (attrs != null) { | ||
| String relativePath = dir.relativize(path).toString() + "/"; | ||
| ZipEntry zipEntry = new ZipEntry(relativePath); | ||
| zipEntry.setTime(attrs.lastModifiedTime().toMillis()); | ||
| zipOutputStream.putNextEntry(zipEntry); | ||
| zipOutputStream.closeEntry(); | ||
| } | ||
| } | ||
| return FileVisitResult.CONTINUE; | ||
| } | ||
| }); | ||
| } | ||
| return hasFiles.booleanValue(); | ||
| } | ||
|
|
||
| public static void unzip(Path zip, Path out) throws IOException { | ||
| public static void unzip(Path zip, Path out, boolean preserveTimestamps) throws IOException { | ||
| Map<Path, Long> directoryTimestamps = preserveTimestamps ? new HashMap<>() : Collections.emptyMap(); | ||
| try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip))) { | ||
| ZipEntry entry = zis.getNextEntry(); | ||
| while (entry != null) { | ||
|
|
@@ -193,16 +260,53 @@ public static void unzip(Path zip, Path out) throws IOException { | |
| throw new RuntimeException("Bad zip entry"); | ||
| } | ||
| if (entry.isDirectory()) { | ||
| Files.createDirectory(file); | ||
| Files.createDirectories(file); | ||
| if (preserveTimestamps) { | ||
| directoryTimestamps.put(file, entry.getTime()); | ||
| } | ||
| } else { | ||
| Path parent = file.getParent(); | ||
| Files.createDirectories(parent); | ||
| if (parent != null) { | ||
| Files.createDirectories(parent); | ||
| } | ||
| Files.copy(zis, file, StandardCopyOption.REPLACE_EXISTING); | ||
|
|
||
| if (preserveTimestamps) { | ||
| setAllTimestamps(file, entry.getTime()); | ||
| } | ||
| } | ||
| Files.setLastModifiedTime(file, FileTime.fromMillis(entry.getTime())); | ||
| entry = zis.getNextEntry(); | ||
| } | ||
| } | ||
|
|
||
| if (preserveTimestamps) { | ||
| // Set directory timestamps after all files have been extracted to avoid them being | ||
| // updated by file creation operations | ||
| for (Map.Entry<Path, Long> dirEntry : directoryTimestamps.entrySet()) { | ||
| setAllTimestamps(dirEntry.getKey(), dirEntry.getValue()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sets all timestamps (lastModifiedTime, lastAccessTime, and creationTime) for a path | ||
| * to the same value to ensure consistency. This is a best-effort operation that silently | ||
| * ignores errors on filesystems that don't support timestamp modification. | ||
| * | ||
| * @param path the path to update | ||
| * @param timestampMillis the timestamp in milliseconds since epoch | ||
| */ | ||
| private static void setAllTimestamps(Path path, long timestampMillis) { | ||
| try { | ||
| BasicFileAttributeView attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class); | ||
| if (attributes != null) { | ||
| FileTime time = FileTime.fromMillis(timestampMillis); | ||
| attributes.setTimes(time, time, time); | ||
| } | ||
| } catch (IOException e) { | ||
| // Timestamp setting is best-effort; log but don't fail extraction | ||
| // This can happen on filesystems that don't support modification times | ||
| } | ||
|
Comment on lines
+306
to
+309
|
||
| } | ||
|
|
||
| public static <T> void debugPrintCollection( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed from
Files.createDirectory(file)toFiles.createDirectories(file). This creates parent directories automatically, but if the directory already exists, it won't throw an exception. However, if a directory entry appears in the ZIP before its parent directory entry (which can happen with certain ZIP creation tools), this could create directories that weren't explicitly in the original ZIP, potentially with incorrect timestamps. Consider checking if the directory already exists before creating it to ensure only directories explicitly in the ZIP are created.