@@ -118,16 +118,13 @@ object zip {
118118 sources.foreach { source =>
119119 if (os.isDir(source.src)) {
120120 val contents = os.walk(source.src)
121- if (contents.isEmpty)
122- source.dest
123- .filter(_ => shouldInclude(source.src.toString + " /" , excludePatterns, includePatterns))
124- .foreach(makeZipEntry0(source.src, _))
121+ source.dest
122+ .filter(_ => shouldInclude(source.src.toString + " /" , excludePatterns, includePatterns))
123+ .foreach(makeZipEntry0(source.src, _))
125124 for (path <- contents) {
126125 if (
127126 (os.isFile(path) && shouldInclude(path.toString, excludePatterns, includePatterns)) ||
128- (os.isDir(path) &&
129- os.walk.stream(path).headOption.isEmpty &&
130- shouldInclude(path.toString + " /" , excludePatterns, includePatterns))
127+ (os.isDir(path) && shouldInclude(path.toString + " /" , excludePatterns, includePatterns))
131128 ) {
132129 makeZipEntry0(path, source.dest.getOrElse(os.sub) / path.subRelativeTo(source.src))
133130 }
@@ -330,19 +327,41 @@ object unzip {
330327 val zipFile = new apache.ZipFile (source.toIO)
331328 val zipEntryInputStreams = zipFile.getEntries.asScala
332329 .filter(ze => os.zip.shouldInclude(ze.getName, excludePatterns, includePatterns))
333- .map(ze => (ze, zipFile.getInputStream(ze)))
330+ .map(ze => {
331+ val mode = ze.getUnixMode
332+ (
333+ ze,
334+ os.SubPath (ze.getName),
335+ mode,
336+ ze.isDirectory,
337+ isSymLink(mode),
338+ zipFile.getInputStream(ze)
339+ )
340+ })
341+ .toList
342+ .sortBy { case (_, path, _, isDirectory, isSymLink, _) =>
343+ // Sort zip entries by
344+ // (1) file type: directories come first, then regular files, then symbolic links
345+ // (2) path: parent directories come before children
346+ // so that enclosing directories are unzipped before their contents.
347+ // This makes sure directory permissions are applied correctly.
348+ (! isDirectory, isSymLink, path)
349+ }
334350
335351 try {
336- for ((zipEntry, zipInputStream) <- zipEntryInputStreams) {
337- val newFile = dest / os.SubPath (zipEntry.getName)
338- val mode = zipEntry.getUnixMode
352+ for ((zipEntry, path, mode, isDirectory, isSymLink, zipInputStream) <- zipEntryInputStreams) {
353+ val newFile = dest / path
339354 val perms = if (mode > 0 && ! isWin) {
340355 os.PermSet .fromSet(apache.PermissionUtils .permissionsFromMode(mode))
341356 } else null
342357
343- if (zipEntry. isDirectory) {
358+ if (isDirectory) {
344359 os.makeDir.all(newFile, perms = perms)
345- } else if (isSymLink(mode)) {
360+ if (os.perms(newFile) != perms) {
361+ // because of umask
362+ os.perms.set(newFile, perms)
363+ }
364+ } else if (isSymLink) {
346365 val target = scala.io.Source .fromInputStream(zipInputStream).mkString
347366 val path = java.nio.file.Paths .get(target)
348367 val dest = if (path.isAbsolute) os.Path (path) else os.RelPath (path)
@@ -378,6 +397,11 @@ object unzip {
378397 /**
379398 * Unzips a ZIP data stream represented by a geny.Readable and extracts it to a destination directory.
380399 *
400+ * File permissions and symbolic links are not supported since permissions and symlink mode are stored
401+ * as external attributes which reside in the central directory located at the end of the zip archive.
402+ * For more a more detailed explanation see the `ZipArchiveInputStream` vs `ZipFile` section at
403+ * [[https://commons.apache.org/proper/commons-compress/zip.html ]]
404+ *
381405 * @param source A geny.Readable object representing the ZIP data stream.
382406 * @param dest The path to the destination directory for extracted files.
383407 * @param excludePatterns A list of regular expression patterns to exclude files during extraction. (Optional)
0 commit comments