Skip to content

Commit bb0e38f

Browse files
committed
Upgrading to java 7 and using posix nio API
1 parent bc565ad commit bb0e38f

File tree

7 files changed

+261
-53
lines changed

7 files changed

+261
-53
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ language: scala
22
os:
33
- linux
44
script:
5+
- sbt ++$TRAVIS_SCALA_VERSION "test"
56
- sbt ++$TRAVIS_SCALA_VERSION "scripted rpm/* debian/* universal/*"
67
scala:
78
- 2.10.3
89
jdk:
9-
- openjdk6
1010
- openjdk7
1111
- oraclejdk8
1212
notifications:

build.sbt

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ scalacOptions in Compile ++= Seq("-deprecation", "-target:jvm-1.6")
1212

1313
libraryDependencies ++= Seq(
1414
"org.apache.commons" % "commons-compress" % "1.4.1",
15-
"org.vafer" % "jdeb" % "1.3" artifacts (Artifact("jdeb", "jar", "jar"))
15+
"org.vafer" % "jdeb" % "1.3" artifacts (Artifact("jdeb", "jar", "jar")),
16+
"org.scalatest" %% "scalatest" % "2.2.4" % "test"
1617
)
1718

1819
site.settings
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,61 @@
11
package com.typesafe.sbt
22
package packager
33

4-
import java.io.File
5-
import sbt.Process
4+
import java.io.{ File, IOException }
5+
import java.nio.file.{ Paths, Files }
6+
import java.nio.file.attribute.{ PosixFilePermission, PosixFilePermissions }
67

8+
/**
9+
* Setting the file permissions
10+
*/
711
object chmod {
12+
13+
/**
14+
* Using java 7 nio API to set the permissions.
15+
*
16+
* @param file
17+
* @param perms in octal format
18+
*/
819
def apply(file: File, perms: String): Unit =
9-
Process(Seq("chmod", perms, file.getAbsolutePath)).! match {
10-
case 0 => ()
11-
case n => sys.error("Error running chmod " + perms + " " + file)
12-
}
13-
def safe(file: File, perms: String): Unit =
14-
try apply(file, perms)
15-
catch {
16-
case e: RuntimeException => ()
20+
try {
21+
Files.setPosixFilePermissions(file.toPath, permissions(perms))
22+
} catch {
23+
case e: IOException => sys.error("Error setting permissions " + perms + " on " + file.getAbsolutePath + ": " + e.getMessage)
1724
}
25+
26+
}
27+
28+
/**
29+
* Converts a octal unix permission representation into
30+
* a java `PosiFilePermissions` compatible string.
31+
*/
32+
object permissions {
33+
34+
/**
35+
* @param perms in octal format
36+
* @return java 7 posix file permissions
37+
*/
38+
def apply(perms: String): java.util.Set[PosixFilePermission] = PosixFilePermissions fromString convert(perms)
39+
40+
def convert(perms: String): String = {
41+
require(perms.length == 4 || perms.length == 3, s"Permissions must have 3 or 4 digits, got [$perms]")
42+
// ignore setuid/setguid/sticky bit
43+
val i = if (perms.length == 3) 0 else 1
44+
val user = Character getNumericValue (perms charAt i)
45+
val group = Character getNumericValue (perms charAt i + 1)
46+
val other = Character getNumericValue (perms charAt i + 2)
47+
48+
asString(user) + asString(group) + asString(other)
49+
}
50+
51+
private def asString(perm: Int): String = perm match {
52+
case 0 => "---"
53+
case 1 => "--x"
54+
case 2 => "-w-"
55+
case 3 => "-wx"
56+
case 4 => "r--"
57+
case 5 => "r-x"
58+
case 6 => "rw-"
59+
case 7 => "rwx"
60+
}
1861
}

src/main/scala/com/typesafe/sbt/packager/universal/ZipHelper.scala

+50-41
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,22 @@ import org.apache.commons.compress.compressors.{
1010
}
1111
import java.util.zip.Deflater
1212
import org.apache.commons.compress.utils.IOUtils
13+
import java.nio.file.{ Paths, Files, FileSystems, FileSystem, StandardCopyOption }
14+
import java.nio.file.attribute.{ PosixFilePermission, PosixFilePermissions }
15+
import java.net.URI
16+
import scala.collection.JavaConverters._
1317

18+
/**
19+
*
20+
*
21+
*
22+
* @see http://stackoverflow.com/questions/17888365/file-permissions-are-not-being-preserved-while-after-zip
23+
* @see http://stackoverflow.com/questions/3450250/is-it-possible-to-create-a-script-to-save-and-restore-permissions
24+
* @see http://stackoverflow.com/questions/1050560/maintain-file-permissions-when-extracting-from-a-zip-file-using-jdk-5-api
25+
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html
26+
*/
1427
object ZipHelper {
15-
case class FileMapping(file: File, name: String, unixMode: Option[Int] = None)
28+
case class FileMapping(file: File, name: String)
1629

1730
/**
1831
* Creates a zip file attempting to give files the appropriate unix permissions using Java 6 APIs.
@@ -41,36 +54,22 @@ object ZipHelper {
4154
}
4255

4356
/**
44-
* Creates a zip file attempting to give files the appropriate unix permissions using Java 6 APIs.
57+
* Creates a zip file attempting to give files the appropriate unix permissions using Java 7 APIs.
58+
*
4559
* Note: This is known to have some odd issues on MacOSX whereby executable permissions
4660
* are not actually discovered, even though the Info-Zip headers exist and work on
4761
* many variants of linux. Yay Apple.
62+
*
4863
* @param sources The files to include in the zip file.
4964
* @param outputZip The location of the output file.
5065
*/
5166
def zip(sources: Traversable[(File, String)], outputZip: File): Unit = {
52-
val mappings =
53-
for {
54-
(file, name) <- sources.toSeq
55-
// TODO - Figure out if this is good enough....
56-
perm = if (file.isDirectory || file.canExecute) 0755 else 0644
57-
} yield FileMapping(file, name, Some(perm))
67+
val mappings = sources.toSeq.map {
68+
case (file, name) => FileMapping(file, name)
69+
}
5870
archive(mappings, outputZip)
5971
}
6072

61-
/**
62-
* Creates a zip file using the given set of filters
63-
* @param sources The files to include in the zip file. A File, Location, Permission pairing.
64-
* @param outputZip The location of the output file.
65-
*/
66-
def zipWithPerms(sources: Traversable[(File, String, Int)], outputZip: File): Unit = {
67-
val mappings =
68-
for {
69-
(file, name, perm) <- sources
70-
} yield FileMapping(file, name, Some(perm))
71-
archive(mappings.toSeq, outputZip)
72-
}
73-
7473
/**
7574
* Replaces windows backslash file separator with a forward slash, this ensures the zip file entry is correct for
7675
* any system it is extracted on.
@@ -84,31 +83,41 @@ object ZipHelper {
8483
path.replace(sep, '/')
8584
}
8685

86+
/**
87+
*
88+
*/
8789
private def archive(sources: Seq[FileMapping], outputFile: File): Unit = {
88-
if (outputFile.isDirectory) sys.error("Specified output file " + outputFile + " is a directory.")
89-
else {
90-
val outputDir = outputFile.getParentFile
91-
IO createDirectory outputDir
92-
withZipOutput(outputFile) { output =>
93-
for (FileMapping(file, name, mode) <- sources; if !file.isDirectory) {
94-
val entry = new ZipArchiveEntry(file, normalizePath(name))
95-
// Now check to see if we have permissions for this sucker.
96-
mode foreach (entry.setUnixMode)
97-
output putArchiveEntry entry
98-
// TODO - Write file into output?
99-
IOUtils.copy(new java.io.FileInputStream(file), output)
100-
output.closeArchiveEntry()
101-
}
90+
require(!outputFile.isDirectory, "Specified output file " + outputFile + " is a directory.")
91+
92+
// make sure everything is available
93+
val outputDir = outputFile.getParentFile
94+
IO createDirectory outputDir
95+
96+
// zipping the sources into the output zip
97+
withZipFilesystem(outputFile) { system =>
98+
sources foreach {
99+
case FileMapping(dir, name) if dir.isDirectory => Files createDirectories (system getPath name)
100+
case FileMapping(file, name) => Files copy (file.toPath, system getPath name, StandardCopyOption.COPY_ATTRIBUTES)
102101
}
103102
}
103+
104104
}
105105

106-
private def withZipOutput(file: File)(f: ZipArchiveOutputStream => Unit): Unit = {
107-
val zipOut = new ZipArchiveOutputStream(file)
108-
zipOut setLevel Deflater.BEST_COMPRESSION
109-
try { f(zipOut) }
110-
finally {
111-
zipOut.close()
106+
/**
107+
* Opens a zip filesystem and creates the file if neccessary
108+
*
109+
* @param zipFile
110+
* @param f: FileSystem => Unit, logic working in the filesystem
111+
*/
112+
def withZipFilesystem(zipFile: File)(f: FileSystem => Unit) {
113+
val env = Map("create" -> "true").asJava
114+
val uri = URI.create("jar:file:" + zipFile.getAbsolutePath)
115+
116+
val system = FileSystems.newFileSystem(uri, env)
117+
try {
118+
f(system)
119+
} finally {
120+
system.close()
112121
}
113122
}
114123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.typesafe.sbt.packager
2+
3+
import java.nio.file._
4+
import java.nio.file.attribute.BasicFileAttributes
5+
import java.io.IOException
6+
7+
class DeleteDirectoryVisitor extends SimpleFileVisitor[Path] {
8+
9+
override def visitFile(file: Path, attrs: BasicFileAttributes) = {
10+
Files delete file
11+
FileVisitResult.CONTINUE
12+
}
13+
14+
override def postVisitDirectory(dir: Path, exc: IOException) = {
15+
Files delete dir
16+
FileVisitResult.CONTINUE
17+
}
18+
19+
}
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.typesafe.sbt.packager
2+
3+
import org.scalatest._
4+
import java.nio.file.attribute.PosixFilePermission._
5+
6+
class FileUtilSpec extends FlatSpec with Matchers {
7+
8+
"permissions" should "convert octal to symbolic correctly" in {
9+
permissions convert "0000" should be("---------")
10+
permissions convert "0600" should be("rw-------")
11+
permissions convert "0755" should be("rwxr-xr-x")
12+
permissions convert "0777" should be("rwxrwxrwx")
13+
}
14+
15+
it should "generate valid java PosixFilePermission" in {
16+
permissions("0000") should be(empty)
17+
18+
val perm1 = permissions("0600")
19+
perm1 should not be (empty)
20+
perm1 should contain only (OWNER_READ, OWNER_WRITE)
21+
22+
val perm2 = permissions("0755")
23+
perm2 should not be (empty)
24+
perm2 should contain only (OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE)
25+
26+
}
27+
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.typesafe.sbt.packager.universal
2+
3+
import com.typesafe.sbt.packager.DeleteDirectoryVisitor
4+
import com.typesafe.sbt.packager.permissions
5+
import org.scalatest._
6+
import java.nio.file.{ Path, Paths, Files }
7+
import java.nio.file.attribute.PosixFilePermission._
8+
9+
class ZipHelperSpec extends FlatSpec with Matchers with BeforeAndAfterAll {
10+
11+
var tmp: Path = _
12+
13+
override def beforeAll {
14+
tmp = Files createTempDirectory "_sbt-native-packager"
15+
}
16+
17+
override def afterAll {
18+
Files.walkFileTree(tmp, new DeleteDirectoryVisitor)
19+
}
20+
21+
"The zip helper" should "create a zip with a single file" in {
22+
// setup
23+
val out = tmp resolve "single.zip"
24+
val file = tmp resolve "single.txt"
25+
Files createFile file
26+
27+
ZipHelper.zip(List(file.toFile -> "single.txt"), out.toFile)
28+
Files exists out should be(true)
29+
30+
ZipHelper.withZipFilesystem(out.toFile) { system =>
31+
val zippedFile = system getPath "single.txt"
32+
Files exists zippedFile should be(true)
33+
}
34+
}
35+
36+
it should "create a zip with nested directories" in {
37+
// setup
38+
val out = tmp resolve "single.zip"
39+
val dir = tmp resolve "dir"
40+
val nested = dir resolve "nested"
41+
Files createDirectories nested
42+
43+
ZipHelper.zip(List(nested.toFile -> "dir/nested"), out.toFile)
44+
45+
ZipHelper.withZipFilesystem(out.toFile) { system =>
46+
val zDir = system getPath "dir"
47+
Files exists zDir should be(true)
48+
Files isDirectory zDir should be(true)
49+
50+
val zNested = zDir resolve "nested"
51+
Files exists zNested should be(true)
52+
Files isDirectory zNested should be(true)
53+
}
54+
}
55+
56+
it should "create a zip with nested directories containing file" in {
57+
// setup
58+
val out = tmp resolve "single.zip"
59+
val dir = tmp resolve "dir"
60+
val file = dir resolve "file.txt"
61+
Files createDirectories dir
62+
Files createFile file
63+
64+
ZipHelper.zip(List(file.toFile -> "dir/file.txt"), out.toFile)
65+
66+
ZipHelper.withZipFilesystem(out.toFile) { system =>
67+
val zDir = system getPath "dir"
68+
Files exists zDir should be(true)
69+
Files isDirectory zDir should be(true)
70+
71+
val zFile = zDir resolve "file.txt"
72+
Files exists zFile should be(true)
73+
Files isDirectory zFile should be(false)
74+
}
75+
}
76+
77+
/*
78+
* This is currently not possible.
79+
*/
80+
it should "preserve the executable bit" ignore {
81+
// setup
82+
val out = tmp resolve "exec.zip"
83+
val exec = tmp resolve "exec"
84+
Files createFile exec
85+
Files.setPosixFilePermissions(exec, permissions("0755"))
86+
87+
val perms = Files getPosixFilePermissions exec
88+
perms should contain only (OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE)
89+
90+
ZipHelper.zip(List(exec.toFile -> "exec"), out.toFile)
91+
Files exists out should be(true)
92+
93+
val unzipped = tmp resolve "unzipped-exec"
94+
ZipHelper.withZipFilesystem(out.toFile) { system =>
95+
val zippedFile = system getPath "exec"
96+
Files exists zippedFile should be(true)
97+
98+
Files.copy(zippedFile, unzipped)
99+
}
100+
101+
// checking permissions
102+
val unzippedPerms = Files getPosixFilePermissions unzipped
103+
unzippedPerms should contain only (OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE, OTHERS_READ, OTHERS_EXECUTE)
104+
105+
}
106+
107+
}

0 commit comments

Comments
 (0)