@@ -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,38 @@ 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+ isSymLink(mode),
337+ zipFile.getInputStream(ze)
338+ )
339+ })
340+ .toList
341+ .sortBy { case (_, path, _, isSymLink, _) =>
342+ // Unzipping symbolic links last.
343+ // Enclosing directories are unzipped before their contents.
344+ // This makes sure directory permissions are applied correctly.
345+ (isSymLink, path)
346+ }
334347
335348 try {
336- for ((zipEntry, zipInputStream) <- zipEntryInputStreams) {
337- val newFile = dest / os.SubPath (zipEntry.getName)
338- val mode = zipEntry.getUnixMode
349+ for ((zipEntry, path, mode, isSymLink, zipInputStream) <- zipEntryInputStreams) {
350+ val newFile = dest / path
339351 val perms = if (mode > 0 && ! isWin) {
340352 os.PermSet .fromSet(apache.PermissionUtils .permissionsFromMode(mode))
341353 } else null
342354
343355 if (zipEntry.isDirectory) {
344356 os.makeDir.all(newFile, perms = perms)
345- } else if (isSymLink(mode)) {
357+ if (perms != null && os.perms(newFile) != perms) {
358+ // because of umask
359+ os.perms.set(newFile, perms)
360+ }
361+ } else if (isSymLink) {
346362 val target = scala.io.Source .fromInputStream(zipInputStream).mkString
347363 val path = java.nio.file.Paths .get(target)
348364 val dest = if (path.isAbsolute) os.Path (path) else os.RelPath (path)
@@ -378,6 +394,11 @@ object unzip {
378394 /**
379395 * Unzips a ZIP data stream represented by a geny.Readable and extracts it to a destination directory.
380396 *
397+ * File permissions and symbolic links are not supported since permissions and symlink mode are stored
398+ * as external attributes which reside in the central directory located at the end of the zip archive.
399+ * For more a more detailed explanation see the `ZipArchiveInputStream` vs `ZipFile` section at
400+ * [[https://commons.apache.org/proper/commons-compress/zip.html ]]
401+ *
381402 * @param source A geny.Readable object representing the ZIP data stream.
382403 * @param dest The path to the destination directory for extracted files.
383404 * @param excludePatterns A list of regular expression patterns to exclude files during extraction. (Optional)
0 commit comments