Skip to content

Commit 828d36d

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 828d36d

File tree

1 file changed

+37
-13
lines changed

1 file changed

+37
-13
lines changed

os/src/ZipOps.scala

Lines changed: 37 additions & 13 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,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 (perms != null && 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

Comments
 (0)