Skip to content

Commit 580ebfe

Browse files
committed
Preserve zipped directory permissions
During zipping, add zip entries for all directories (not just empty ones), with permissions set. During unzipping, unzipping enclosing directories before their contents.
1 parent 92c325a commit 580ebfe

File tree

1 file changed

+33
-12
lines changed

1 file changed

+33
-12
lines changed

os/src/ZipOps.scala

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)