Skip to content

Commit 341e86b

Browse files
authored
Support zipping empty directories (#369)
Currently empty directories are excluded when zipping.
1 parent c7098a4 commit 341e86b

File tree

2 files changed

+93
-21
lines changed

2 files changed

+93
-21
lines changed

os/src/ZipOps.scala

+24-21
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,18 @@ object zip {
9797
): Unit = {
9898
sources.foreach { source =>
9999
if (os.isDir(source.src)) {
100-
for (path <- os.walk(source.src)) {
101-
if (os.isFile(path) && shouldInclude(path.toString, excludePatterns, includePatterns)) {
100+
val contents = os.walk(source.src)
101+
if (contents.isEmpty)
102+
source.dest
103+
.filter(_ => shouldInclude(source.src.toString + "/", excludePatterns, includePatterns))
104+
.foreach(makeZipEntry0(source.src, _))
105+
for (path <- contents) {
106+
if (
107+
(os.isFile(path) && shouldInclude(path.toString, excludePatterns, includePatterns)) ||
108+
(os.isDir(path) &&
109+
os.walk.stream(path).headOption.isEmpty &&
110+
shouldInclude(path.toString + "/", excludePatterns, includePatterns))
111+
) {
102112
makeZipEntry0(path, source.dest.getOrElse(os.sub) / path.subRelativeTo(source.src))
103113
}
104114
}
@@ -125,6 +135,7 @@ object zip {
125135
includePatterns,
126136
(path, sub) => makeZipEntry(path, sub, preserveMtimes, zipOut)
127137
)
138+
zipOut.finish()
128139
} finally {
129140
zipOut.close()
130141
}
@@ -149,29 +160,21 @@ object zip {
149160
preserveMtimes: Boolean,
150161
zipOut: ZipOutputStream
151162
) = {
163+
val name =
164+
if (os.isDir(file)) sub.toString + "/"
165+
else sub.toString
166+
val zipEntry = new ZipEntry(name)
152167

153-
val mtimeOpt = if (preserveMtimes) Some(os.mtime(file)) else None
168+
val mtime = if (preserveMtimes) os.mtime(file) else 0
169+
zipEntry.setTime(mtime)
154170

155171
val fis = if (os.isFile(file)) Some(os.read.inputStream(file)) else None
156-
try makeZipEntry0(sub, fis, mtimeOpt, zipOut)
157-
finally fis.foreach(_.close())
158-
}
159-
160-
private def makeZipEntry0(
161-
sub: os.SubPath,
162-
is: Option[java.io.InputStream],
163-
preserveMtimes: Option[Long],
164-
zipOut: ZipOutputStream
165-
) = {
166-
val zipEntry = new ZipEntry(sub.toString)
167-
168-
preserveMtimes match {
169-
case Some(mtime) => zipEntry.setTime(mtime)
170-
case None => zipEntry.setTime(0)
171-
}
172172

173-
zipOut.putNextEntry(zipEntry)
174-
is.foreach(os.Internals.transfer(_, zipOut, close = false))
173+
try {
174+
zipOut.putNextEntry(zipEntry)
175+
fis.foreach(os.Internals.transfer(_, zipOut, close = false))
176+
zipOut.closeEntry()
177+
} finally fis.foreach(_.close())
175178
}
176179

177180
/**

os/test/src/ZipOpTests.scala

+69
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,75 @@ object ZipOpTests extends TestSuite {
109109
assert(paths == expected)
110110
}
111111

112+
test("zipEmptyDir") {
113+
def prepare(wd: os.Path) = {
114+
val zipFileName = "zipEmptyDirs"
115+
116+
val emptyDir = wd / "empty"
117+
os.makeDir(emptyDir)
118+
119+
val containsEmptyDir = wd / "outer"
120+
os.makeDir.all(containsEmptyDir)
121+
os.makeDir(containsEmptyDir / "emptyInnerDir")
122+
123+
(zipFileName, emptyDir, containsEmptyDir)
124+
}
125+
126+
test("zipEmptyDir") - prep { wd =>
127+
val (zipFileName, emptyDir, containsEmptyDir) = prepare(wd)
128+
129+
val zipped = os.zip(
130+
dest = wd / s"${zipFileName}.zip",
131+
sources = Seq(emptyDir, containsEmptyDir)
132+
)
133+
134+
val unzipped = os.unzip(zipped, wd / zipFileName)
135+
// should include empty dirs inside source
136+
assert(os.isDir(unzipped / "emptyInnerDir"))
137+
// should ignore empty dirs specified in sources without dest
138+
assert(!os.exists(unzipped / "empty"))
139+
}
140+
141+
test("includePatterns") - prep { wd =>
142+
val (zipFileName, _, containsEmptyDir) = prepare(wd)
143+
144+
val zipped = os.zip(
145+
dest = wd / s"${zipFileName}.zip",
146+
sources = Seq(containsEmptyDir),
147+
includePatterns = Seq(raw".*Inner.*".r)
148+
)
149+
150+
val unzipped = os.unzip(zipped, wd / zipFileName)
151+
assert(os.isDir(unzipped / "emptyInnerDir"))
152+
}
153+
154+
test("excludePatterns") - prep { wd =>
155+
val (zipFileName, _, containsEmptyDir) = prepare(wd)
156+
157+
val zipped = os.zip(
158+
dest = wd / s"${zipFileName}.zip",
159+
sources = Seq(containsEmptyDir),
160+
excludePatterns = Seq(raw".*Inner.*".r)
161+
)
162+
163+
val unzipped = os.unzip(zipped, wd / zipFileName)
164+
assert(!os.exists(unzipped / "emptyInnerDir"))
165+
}
166+
167+
test("withDest") - prep { wd =>
168+
val (zipFileName, emptyDir, _) = prepare(wd)
169+
170+
val zipped = os.zip(
171+
dest = wd / s"${zipFileName}.zip",
172+
sources = Seq((emptyDir, os.sub / "empty"))
173+
)
174+
175+
val unzipped = os.unzip(zipped, wd / zipFileName)
176+
// should include empty dirs specified in sources with dest
177+
assert(os.isDir(unzipped / "empty"))
178+
}
179+
}
180+
112181
test("zipStream") - prep { wd =>
113182
val zipFileName = "zipStreamFunction.zip"
114183

0 commit comments

Comments
 (0)