3131import java .nio .file .attribute .FileTime ;
3232import java .util .Arrays ;
3333import java .util .Collection ;
34+ import java .util .Collections ;
3435import java .util .HashMap ;
36+ import java .util .HashSet ;
3537import java .util .List ;
3638import java .util .Map ;
3739import java .util .NoSuchElementException ;
40+ import java .util .Set ;
3841import java .util .stream .Stream ;
3942import java .util .zip .ZipEntry ;
4043import java .util .zip .ZipInputStream ;
@@ -156,25 +159,31 @@ public static boolean isArchive(File file) {
156159 * @param dir directory to zip
157160 * @param zip zip to populate
158161 * @param glob glob to apply to filenames
162+ * @param preserveTimestamps whether to preserve file and directory timestamps in the zip
159163 * @return true if at least one file has been included in the zip.
160164 * @throws IOException
161165 */
162- public static boolean zip (final Path dir , final Path zip , final String glob ) throws IOException {
166+ public static boolean zip (final Path dir , final Path zip , final String glob , boolean preserveTimestamps )
167+ throws IOException {
163168 final MutableBoolean hasFiles = new MutableBoolean ();
164169 try (ZipOutputStream zipOutputStream = new ZipOutputStream (Files .newOutputStream (zip ))) {
165170
166171 PathMatcher matcher =
167172 "*" .equals (glob ) ? null : FileSystems .getDefault ().getPathMatcher ("glob:" + glob );
173+
174+ // Track directories that contain matching files for glob filtering
175+ final Set <Path > directoriesWithMatchingFiles = new HashSet <>();
176+ // Track directory attributes for timestamp preservation
177+ final Map <Path , BasicFileAttributes > directoryAttributes =
178+ preserveTimestamps ? new HashMap <>() : Collections .emptyMap ();
179+
168180 Files .walkFileTree (dir , new SimpleFileVisitor <Path >() {
169181
170182 @ Override
171183 public FileVisitResult preVisitDirectory (Path path , BasicFileAttributes attrs ) throws IOException {
172- if (!path .equals (dir )) {
173- String relativePath = dir .relativize (path ).toString () + "/" ;
174- ZipEntry zipEntry = new ZipEntry (relativePath );
175- zipEntry .setTime (attrs .lastModifiedTime ().toMillis ());
176- zipOutputStream .putNextEntry (zipEntry );
177- zipOutputStream .closeEntry ();
184+ if (preserveTimestamps ) {
185+ // Store attributes for use in postVisitDirectory
186+ directoryAttributes .put (path , attrs );
178187 }
179188 return FileVisitResult .CONTINUE ;
180189 }
@@ -184,23 +193,59 @@ public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttribu
184193 throws IOException {
185194
186195 if (matcher == null || matcher .matches (path .getFileName ())) {
196+ if (preserveTimestamps ) {
197+ // Mark all parent directories as containing matching files
198+ Path parent = path .getParent ();
199+ while (parent != null && !parent .equals (dir )) {
200+ directoriesWithMatchingFiles .add (parent );
201+ parent = parent .getParent ();
202+ }
203+ }
204+
187205 final ZipEntry zipEntry =
188206 new ZipEntry (dir .relativize (path ).toString ());
189- zipEntry .setTime (basicFileAttributes .lastModifiedTime ().toMillis ());
207+ if (preserveTimestamps ) {
208+ zipEntry .setTime (basicFileAttributes .lastModifiedTime ().toMillis ());
209+ }
190210 zipOutputStream .putNextEntry (zipEntry );
191211 Files .copy (path , zipOutputStream );
192212 hasFiles .setTrue ();
193213 zipOutputStream .closeEntry ();
194214 }
195215 return FileVisitResult .CONTINUE ;
196216 }
217+
218+ @ Override
219+ public FileVisitResult postVisitDirectory (Path path , IOException exc ) throws IOException {
220+ // Propagate any exception that occurred during directory traversal
221+ if (exc != null ) {
222+ throw exc ;
223+ }
224+
225+ // Add directory entry only if preserving timestamps and:
226+ // 1. It's not the root directory, AND
227+ // 2. Either no glob filter (matcher is null) OR directory contains matching files
228+ if (preserveTimestamps
229+ && !path .equals (dir )
230+ && (matcher == null || directoriesWithMatchingFiles .contains (path ))) {
231+ BasicFileAttributes attrs = directoryAttributes .get (path );
232+ if (attrs != null ) {
233+ String relativePath = dir .relativize (path ).toString () + "/" ;
234+ ZipEntry zipEntry = new ZipEntry (relativePath );
235+ zipEntry .setTime (attrs .lastModifiedTime ().toMillis ());
236+ zipOutputStream .putNextEntry (zipEntry );
237+ zipOutputStream .closeEntry ();
238+ }
239+ }
240+ return FileVisitResult .CONTINUE ;
241+ }
197242 });
198243 }
199244 return hasFiles .booleanValue ();
200245 }
201246
202- public static void unzip (Path zip , Path out ) throws IOException {
203- Map <Path , Long > directoryTimestamps = new HashMap <>();
247+ public static void unzip (Path zip , Path out , boolean preserveTimestamps ) throws IOException {
248+ Map <Path , Long > directoryTimestamps = preserveTimestamps ? new HashMap <>() : Collections . emptyMap ();
204249 try (ZipInputStream zis = new ZipInputStream (Files .newInputStream (zip ))) {
205250 ZipEntry entry = zis .getNextEntry ();
206251 while (entry != null ) {
@@ -209,26 +254,42 @@ public static void unzip(Path zip, Path out) throws IOException {
209254 throw new RuntimeException ("Bad zip entry" );
210255 }
211256 if (entry .isDirectory ()) {
212- if (!Files .exists (file )) {
213- Files .createDirectories (file );
257+ Files .createDirectories (file );
258+ if (preserveTimestamps ) {
259+ directoryTimestamps .put (file , entry .getTime ());
214260 }
215- directoryTimestamps .put (file , entry .getTime ());
216261 } else {
217262 Path parent = file .getParent ();
218- if (! Files . exists ( parent ) ) {
263+ if (parent != null ) {
219264 Files .createDirectories (parent );
220265 }
221266 Files .copy (zis , file , StandardCopyOption .REPLACE_EXISTING );
222- Files .setLastModifiedTime (file , FileTime .fromMillis (entry .getTime ()));
267+
268+ if (preserveTimestamps ) {
269+ // Set file timestamp with error handling
270+ try {
271+ Files .setLastModifiedTime (file , FileTime .fromMillis (entry .getTime ()));
272+ } catch (IOException e ) {
273+ // Timestamp setting is best-effort; log but don't fail extraction
274+ // This can happen on filesystems that don't support modification times
275+ }
276+ }
223277 }
224278 entry = zis .getNextEntry ();
225279 }
226280 }
227281
228- // Set directory timestamps after all files have been extracted to avoid them being
229- // updated by file creation operations
230- for (Map .Entry <Path , Long > dirEntry : directoryTimestamps .entrySet ()) {
231- Files .setLastModifiedTime (dirEntry .getKey (), FileTime .fromMillis (dirEntry .getValue ()));
282+ if (preserveTimestamps ) {
283+ // Set directory timestamps after all files have been extracted to avoid them being
284+ // updated by file creation operations
285+ for (Map .Entry <Path , Long > dirEntry : directoryTimestamps .entrySet ()) {
286+ try {
287+ Files .setLastModifiedTime (dirEntry .getKey (), FileTime .fromMillis (dirEntry .getValue ()));
288+ } catch (IOException e ) {
289+ // Timestamp setting is best-effort; log but don't fail extraction
290+ // This can happen on filesystems that don't support modification times
291+ }
292+ }
232293 }
233294 }
234295
0 commit comments