//scalastyle:off magic.number
package utils
import java.nio.file.{ Files, Paths }
import org.apache.commons.io.FileUtils
import org.specs2.mock.Mockito
import org.specs2.specification.AfterAll
import play.api.test.PlaySpecification
import scala.collection.JavaConverters._
import scala.concurrent.ExecutionContext.Implicits.global
class ZipUtilSpec extends PlaySpecification with Mockito with AfterAll {
private val testDir = Paths.get("test")
private val resourceDir = testDir.resolve("resource")
private val zipTestDir = resourceDir.resolve("zipTest")
private val txtDir = zipTestDir.resolve("txt")
private val testTxt = txtDir.resolve("test.txt")
private val emptyDir = zipTestDir.resolve("empty")
private val hierarchyDir = zipTestDir.resolve("hierarchy")
private val hierarchyEmptyDir = hierarchyDir.resolve("empty")
private val tempDir = Files.createTempDirectory("sre-ad-sync-batch.ZipUtilSpec")
override def afterAll(): Unit = FileUtil.deleteRecursively(tempDir)
"ZipUtil" should {
"#zip => #unzip" should {
"正常系:Zip 圧縮=>解凍成功 (ソース = Dir > 1 File 有り)" in {
val zipFile = Files.createTempFile(tempDir, "ZipUtilSpec", ".zip")
// 出力先ファイルが存在する場合もエラーとならない(上書き)
zipFile.toFile.exists must beTrue
val res = await { ZipUtil.zip(txtDir, zipFile) }
res must_=== 1L
zipFile.toFile.exists must beTrue
zipFile.toFile.length must greaterThan(0L)
val unzipPath = tempDir.resolve("unzip").resolve(zipFile.getFileName)
val res2 = await { ZipUtil.unzip(zipFile, unzipPath) }
res2 must_=== 1L
unzipPath.toFile.exists must beTrue
unzipPath.toFile.isDirectory must beTrue
val unzipList = unzipPath.toFile.listFiles().toList
unzipList must haveSize(1)
FileUtils.checksumCRC32(unzipList.head) must_=== FileUtils.checksumCRC32(testTxt.toFile)
}
"正常系:Zip 圧縮=>解凍成功 (ソース = Dir > 空ディレクトリ)" in {
Files.createDirectories(emptyDir)
emptyDir.toFile.exists must beTrue
val zipFile = Files.createTempFile(tempDir, "ZipUtilSpec", ".zip")
val res = await { ZipUtil.zip(emptyDir, zipFile) }
res must_=== 0L
zipFile.toFile.exists must beTrue
zipFile.toFile.length must greaterThan(0L)
val unzipPath = tempDir.resolve("unzip").resolve(zipFile.getFileName)
val res2 = await { ZipUtil.unzip(zipFile, unzipPath) }
res2 must_=== 0L
unzipPath.toFile.exists must beFalse
unzipPath.toFile.isDirectory must beFalse
}
"正常系:Zip 圧縮=>解凍成功 (ソース = File)" in {
val zipFile = Files.createTempFile(tempDir, "ZipUtilSpec", ".zip")
val res = await { ZipUtil.zip(testTxt, zipFile) }
res must_=== 1L
zipFile.toFile.exists must beTrue
zipFile.toFile.length must greaterThan(0L)
val unzipPath = tempDir.resolve("unzip").resolve(zipFile.getFileName)
val res2 = await { ZipUtil.unzip(zipFile, unzipPath) }
res2 must_=== 1L
unzipPath.toFile.exists must beTrue
unzipPath.toFile.isDirectory must beTrue
val unzipList = unzipPath.toFile.listFiles().toList
unzipList must haveSize(1)
FileUtils.checksumCRC32(unzipList.head) must_=== FileUtils.checksumCRC32(testTxt.toFile)
}
"正常系:Zip 圧縮=>解凍成功 (ソース = Dir > 二階層以上、複数 File、空フォルダ 有り)" in {
Files.createDirectories(hierarchyEmptyDir)
hierarchyEmptyDir.toFile.exists must beTrue
val zipFile = Files.createTempFile(tempDir, "ZipUtilSpec", ".zip")
val res = await { ZipUtil.zip(hierarchyDir, zipFile) }
res must_=== 11L
zipFile.toFile.exists must beTrue
zipFile.toFile.length must greaterThan(0L)
val unzipPath = tempDir.resolve("unzip").resolve(zipFile.getFileName)
val res2 = await { ZipUtil.unzip(zipFile, unzipPath) }
res2 must_=== 11L
unzipPath.toFile.exists must beTrue
unzipPath.toFile.isDirectory must beTrue
// ディレクトリツリー・ファイル構成が元と同一かチェック
val unzipList = Files.walk(unzipPath).iterator().asScala.map(unzipPath.relativize).toList // フォルダ内の全要素の相対パス取得
val expectedList = Files.walk(hierarchyDir).iterator().asScala.map(hierarchyDir.relativize).toList
unzipList must containTheSameElementsAs(expectedList)
}
}
"#zip" should {
"異常系:存在しないソースパス" in {
val zipFile = Files.createTempFile(tempDir, "ZipUtilSpec", "nonExist.zip")
val nonExistSrc = emptyDir.resolve("nonExistPath")
nonExistSrc.toFile.exists must beFalse
await { ZipUtil.zip(nonExistSrc, zipFile) } must throwA[IllegalArgumentException]
}
}
"#unzip" should {
"異常系:存在しないZipファイルパス" in {
val nonExistZip = tempDir.resolve("ZipUtilSpec").resolve("nonExist.zip")
nonExistZip.toFile.exists must beFalse
val trgPath = tempDir.resolve(nonExistZip.getFileName)
await { ZipUtil.unzip(nonExistZip, trgPath) } must throwA[IllegalArgumentException]
}
}
}
}
//scalastyle:off magic.number
package utils
import java.io.{ BufferedInputStream, Closeable }
import java.nio.file._
import org.apache.commons.io.FileUtils
import org.specs2.mock.Mockito
import org.specs2.specification.BeforeAfterEach
import play.api.test.PlaySpecification
import scala.concurrent.ExecutionContext.Implicits.global
class GPGUtilSpec extends PlaySpecification with Mockito with BeforeAfterEach {
private val testDir = Paths.get("test")
private val resourceDir = testDir.resolve("resource")
private val gpgTestDir = resourceDir.resolve("gpgTest")
private val exportPrivateKey = gpgTestDir.resolve("export.asc.private")
private val passphrase = "test"
private val genKeyID: Long = 4764940382025454192L // 暗号鍵のキーID
private val testOdinGpg = gpgTestDir.resolve("ODIN.GPG")
private val tmpDir = Files.createTempDirectory("sre-ad-sync-batch.GPGUtilSpec")
override def before: Any = Files.createDirectories(tmpDir)
override def after: Any = FileUtil.deleteRecursively(tmpDir) must beTrue.setMessage(
s"テスト用一時ディレクトリの削除に失敗しました。[$tmpDir]"
)
"GPGUtil" should {
val keyConfig: KeyConfig = KeyConfig(secretKeyPath = exportPrivateKey, passphrase = Some(passphrase))
"#decryptGpgFile" should {
"正常系:復号成功" in {
val srcPath = Files.copy(testOdinGpg, Files.createTempFile(tmpDir, "copyOdinGpg", ".gpg"), StandardCopyOption.REPLACE_EXISTING)
val destPath = Files.createTempFile(tmpDir, "decryptGpgFile", ".tmp")
// 出力先ファイルが存在する場合もエラーとならない(上書き)
destPath.toFile.exists must beTrue
val result = await { GPGUtil.decryptGpgFile(srcPath, destPath, keyConfig) }
result must greaterThan(0L)
Files.exists(destPath) must beTrue
FileUtils.checksumCRC32(destPath.toFile) must_=== 604502380L // 復号ファイルハッシュ値
}
"異常系:復号失敗(秘密鍵なし)" in {
val srcPath = Files.copy(testOdinGpg, Files.createTempFile(tmpDir, "copyOdinGpg", ".gpg"), StandardCopyOption.REPLACE_EXISTING)
val destPath = Files.createTempFile(tmpDir, "decryptGpgFile", ".tmp")
await { GPGUtil.decryptGpgFile(srcPath, destPath, keyConfig.copy(secretKeyPath = tmpDir.resolve("nonExist.gpg"))) } must throwA
}
}
"#decryptGpgData" should {
"正常系:GPGファイル復号データStream => ZipInputStream として ZipUtil へ直接受渡し => Zip解凍成功 " in {
val srcPath = Files.copy(testOdinGpg, Files.createTempFile(tmpDir, "copyOdinGpg", ".gpg"), StandardCopyOption.REPLACE_EXISTING)
val destPath = tmpDir.resolve("decryptGpgWithZipUtil")
val res = await {
import ResourceUtil._
usingAsync(new BufferedInputStream(Files.newInputStream(srcPath))) { fin =>
usingAsync(GPGUtil.createDecryptedStream(fin, keyConfig)) { decIn =>
ZipUtil.unzip(destPath)(new BufferedInputStream(decIn))
}
}
}
res must_=== 14L // ODIN.ZIP 内のファイル(エントリ)数
destPath.toFile.exists must beTrue
destPath.toFile.isDirectory must beTrue
val unzipList = destPath.toFile.listFiles().toList.map(_.toPath).map(destPath.relativize) // 解凍先フォルダ内の全要素取得
unzipList.map(_.toString) must containAllOf(Seq("100086327810.jpg", "100086327810.TXT", "100086694700.jpg",
"5356975421371.JPG", "5356975511364.JPG", "5356975520439.JPG", "5356975527948.JPG", "5356975546249.JPG",
"5356975554289.JPG", "5356975561343.JPG", "5356975568438.jpg", "5356975575278.JPG", "5356975582172.JPG", "ODIN.txt"))
}
}
}
"KeyConfig$" should {
"#createKeyringConfig" in {
val result = KeyConfig(secretKeyPath = exportPrivateKey, passphrase = Some(passphrase)).createKeyringConfig
result.getSecretKeyRings.contains(genKeyID) must beTrue
}
}
}
package utils
import javax.xml.stream.XMLStreamWriter
import org.junit.runner._
import org.specs2.mock.Mockito
import org.specs2.runner._
import play.api.test.PlaySpecification
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
@RunWith(classOf[JUnitRunner])
class ResourceUtilSpec extends PlaySpecification with Mockito {
"ResourceUtil" should {
val testError = new RuntimeException("test error")
"using" should {
"正常系: AutoCloseable" in {
val resource = mock[AutoCloseable]
ResourceUtil.using(resource)(identity) must_=== resource
there was one(resource).close()
}
"正常系: XMLStreamWriter" in {
val resource = mock[XMLStreamWriter]
ResourceUtil.using(resource)(identity) must_=== resource
there was one(resource).close()
}
"異常系" in {
val resource = mock[AutoCloseable]
ResourceUtil.using(resource)(r => { throw testError; r }) must throwA(testError)
there was one(resource).close()
}
}
"usingAsync" should {
"正常系: AutoCloseable" in {
val resource = mock[AutoCloseable]
await {
ResourceUtil.usingAsync(resource)(Future.successful)
} must_=== resource
there was one(resource).close()
}
"正常系: XMLStreamWriter" in {
val resource = mock[XMLStreamWriter]
await {
ResourceUtil.usingAsync(resource)(Future.successful)
} must_=== resource
there was one(resource).close()
}
"異常系: 処理関数評価時に例外発生" in {
val resource = mock[AutoCloseable]
await {
ResourceUtil.usingAsync(resource)(r => { throw testError; Future.successful(r) })
} must throwA(testError)
there was one(resource).close()
}
"異常系: 処理関数戻り値の Future が Failed" in {
val resource = mock[AutoCloseable]
await {
ResourceUtil.usingAsync(resource)(_ => { Future.failed[Unit](testError) })
} must throwA(testError)
there was one(resource).close()
}
}
}
}
package utils
import java.io._
import java.net.URI
import java.nio.file._
import java.util.zip.ZipInputStream
import javax.inject.Singleton
import scala.collection.JavaConverters.{ asScalaIteratorConverter, mapAsJavaMapConverter }
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.{ Failure, Try }
/**
* Zip ファイル Util クラス
*/
@Singleton
class ZipUtil {
/** ZIP 解凍時 Stream バッファサイズ */
private val BUFFER_SIZE = 8 * 1024
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Zip 圧縮処理
/**
* Zip ファイル作成処理
*
* 対象パス内の全ファイル/ディレクトリを圧縮し、Zip ファイルとして出力する。
* ファイル生成先に既存ファイルがある場合は削除してから新規作成する。
*
* @param srcPath 圧縮対象ディレクトリパス(存在しない場合はエラー)
* @param zipFile Zip 生成先ファイルパス
* @return 出力した Entry 数
*/
def zip(srcPath: Path, zipFile: Path)(implicit ec: ExecutionContext): Future[Long] = if (Files.notExists(srcPath)) {
Future.failed(new IllegalArgumentException(s"srcPath not exists : $srcPath"))
} else {
Future.fromTry {
FileUtil.deleteRecursively(zipFile) // 生成先ファイルがある場合は洗い替え
import ResourceUtil._
using(getZipFileSystem(zipFile.toFile.toURI)) { addToFileSystem(getSrcList(srcPath)) }
} andThen {
case Failure(_) => FileUtil.deleteRecursively(zipFile) // エラーが発生した場合は念のため生成先ファイルを削除
}
}
/**
* Zip ファイル用 FileSystem を作成する
*
* @param uri Zip 作成先 URI (スキームは含まない)
* @return ZipFileSystem
*/
private def getZipFileSystem(uri: URI): FileSystem = {
val zipUri = new URI(s"jar:$uri")
val env = Map("create" -> "true").asJava
FileSystems.newFileSystem(zipUri, env)
}
/**
* 対象の全ファイル/ディレクトリを FileSystem に追加する。
*
* @param srcIterator 対象要素のイテレータ: Tuple2(コピー元への参照パス, FileSystem への Entry パス)
* @param fs 追加先 FileSystem
* @return 出力した Entry 数
*/
private def addToFileSystem(srcIterator: List[(Path, Path)])(fs: FileSystem)(implicit ec: ExecutionContext): Try[Long] = Try {
srcIterator.foldLeft(0L) {
case (cnt, (src, entryPath)) if Files.isDirectory(src) => // ディレクトリの場合
Files.createDirectories(fs.getPath(entryPath.toString)) // フォルダ作成のみ
cnt + 1
case (cnt, (src, entryPath)) => // ファイルの場合
// 親フォルダを作成してから FileSystem 内にコピー
val fsPath = fs.getPath(entryPath.toString)
val parent = fsPath.getParent
if (Option(parent).isDefined) Files.createDirectories(parent)
Files.copy(src, fsPath, StandardCopyOption.COPY_ATTRIBUTES)
cnt + 1
}
}
/**
* Zip 化対象要素取得処理
*
* 対象パス内の全ファイル/ディレクトリを走査し、絶対パスと探索開始ルートからの相対パスを返す。
*
* @param srcPath 探索対象パス
* @return List[ (絶対パス, ルートからの相対パス) ]
*/
private def getSrcList(srcPath: Path): List[(Path, Path)] = {
if (Files.isDirectory(srcPath)) { // ディレクトリの場合
import ResourceUtil._
using(Files.walk(srcPath)) {
_.iterator().asScala // フォルダ内全要素探索
.filterNot(p => Files.isSameFile(p, srcPath)) // ソースディレクトリ自身は除外
.map(p => (p.toAbsolutePath, srcPath.relativize(p))).toList
}
} else { // ファイルの場合
List((srcPath.toAbsolutePath, srcPath.getFileName)) // ソースファイル自身のみ返却
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Zip 解凍処理
/**
* Zip ファイル解凍処理
*
* Zip ファイルを読み込み、解凍先ディレクトリに全要素を展開する。
* 解凍先ディレクトリに既存フォルダがある場合は削除を試みてから展開する。
*
* @param zipFile 解凍する Zip ファイル
* @param trgPath 解凍先ディレクトリパス
* @return 出力した Entry 数
*/
def unzip(zipFile: Path, trgPath: Path)(implicit ec: ExecutionContext): Future[Long] = if (Files.notExists(zipFile)) {
Future.failed(new IllegalArgumentException(s"zipFile not exists : $zipFile"))
} else {
Future.fromTry(Try(Files.newInputStream(zipFile))) flatMap {
import ResourceUtil._
usingAsync(_) { in =>
ZipUtil.unzip(trgPath)(new BufferedInputStream(in))
}
}
}
/**
* Zip データ解凍処理
*
* ZipInputStream から ZipEntry を読み込み、解凍先ディレクトリに全要素を展開する。
* 解凍先ディレクトリに既存フォルダがある場合は削除を試みてから展開する。
*
* @param trgPath 解凍先ディレクトリパス
* @param is Zip 入力データストリーム
* @return 出力した Entry 数
*/
def unzip(trgPath: Path)(is: InputStream)(implicit ec: ExecutionContext): Future[Long] = Future {
val zis = new ZipInputStream(is)
FileUtil.deleteRecursively(trgPath) // 生成先フォルダがある場合は洗い替え
val zipEntryIterator = // ZipInputStream から ZipEntry を取得する Iterator
Iterator.continually(zis.getNextEntry).takeWhile(Option(_).isDefined) // #getNextEntry の読み込みがNullになるまでループ
zipEntryIterator.foldLeft(0L) {
case (cnt, ze) if ze.isDirectory => // ディレクトリの場合はフォルダ作成のみ
trgPath.resolve(ze.getName).toFile.mkdirs()
zis.closeEntry()
cnt + 1
case (cnt, ze) => // ファイルの場合は親フォルダを作成してからファイルデータを書き出し
val file = trgPath.resolve(ze.getName).toFile
file.getParentFile.mkdirs()
import ResourceUtil._
using(new BufferedOutputStream(new FileOutputStream(file))) { out =>
val buffer: Array[Byte] = new Array[Byte](BUFFER_SIZE)
Iterator.continually(zis.read(buffer)).takeWhile(_ != -1).foreach(len => out.write(buffer, 0, len))
zis.closeEntry()
}
cnt + 1
}
} andThen {
case Failure(_) => FileUtil.deleteRecursively(trgPath) // エラーが発生した場合は念のため生成先フォルダを削除
}
}
object ZipUtil extends ZipUtil
package utils
import java.io._
import java.nio.file.{ Files, Path, StandardCopyOption }
import java.security.Security
import javax.inject.Singleton
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks
import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.{ KeyringConfig, KeyringConfigs }
import org.bouncycastle.jce.provider.BouncyCastleProvider
import scala.concurrent.{ ExecutionContext, Future }
/**
* GPG暗号 Utilクラス
*/
@Singleton
class GPGUtil {
/**
* GPG暗号化ファイルを復号して別ファイルに書き出す。
* 出力先ファイルが存在する場合は上書きする。
*
* @param encInFile 入力ファイル(暗号データ)
* @param decOutFile 出力ファイル(復号データ)
* @param keyConfig 復号に使用する共通鍵ファイル情報
* @return Success(ファイル出力 Byte 数) / Failure(エラー情報)
*/
def decryptGpgFile(encInFile: Path, decOutFile: Path, keyConfig: KeyConfig)(implicit ec: ExecutionContext): Future[Long] = Future {
import ResourceUtil._
using(new BufferedInputStream(Files.newInputStream(encInFile))) { fin =>
using(createDecryptedStream(fin, keyConfig)) { decIn =>
Files.copy(decIn, decOutFile, StandardCopyOption.REPLACE_EXISTING)
}
}
}
/**
* GPGデータ復号用入力ストリームを生成する。
* 引数のストリームの入力値がGPG暗号データとして復号され、変換後の平文データが入力値として戻り値のストリームに渡される。
*
* @param encryptedData GPG暗号データ(入力ストリーム ※使用後は要close)
* @param keyConfig 復号に使用する共通鍵ファイル情報
* @return 復号データ(入力ストリーム ※使用後は要close)
*/
def createDecryptedStream(encryptedData: InputStream, keyConfig: KeyConfig): InputStream = {
GPGUtil.installBCProvider
BouncyGPG.decryptAndVerifyStream
.withConfig(keyConfig.createKeyringConfig)
.andIgnoreSignatures
.fromEncryptedInputStream(encryptedData)
}
}
object GPGUtil extends GPGUtil {
/**
* SecurityProvider に BouncyCastle(PGP暗号ライブラリ) を追加する。
* (※1度のみ実行必要。未追加で BouncyCastle の暗号処理を使用するとエラーとなる。)
*/
lazy val installBCProvider: Unit = {
if (Option(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)).isEmpty) { Security.addProvider(new BouncyCastleProvider()) }
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* GPG暗号鍵ファイル情報
*
* @param secretKeyPath 秘密鍵(鍵束)ファイルパス
* @param passphrase 鍵ファイルパスフレーズ
*/
case class KeyConfig(secretKeyPath: Path, passphrase: Option[String]) {
/** bouncy-gpg用 KeyringConfig オブジェクト生成 */
private[utils] def createKeyringConfig: KeyringConfig = {
val conf = KeyringConfigs.forGpgExportedKeys(
passphrase.map(KeyringConfigCallbacks.withPassword)
.getOrElse(KeyringConfigCallbacks.withUnprotectedKeys)
)
conf.addSecretKey(Files.readAllBytes(secretKeyPath))
conf
}
}
package utils
import javax.xml.stream.XMLStreamWriter
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.control.Exception.ultimately
object ResourceUtil {
/** Closable ローンパターンメソッド用クローズ処理実装ケースクラス */
case class Closable[T](close: T => _) extends AnyVal
/** クローズ処理実装関数: for [[AutoCloseable]] */
implicit val closingAutoCloseable: Closable[AutoCloseable] = Closable { (t: AutoCloseable) => t.close() }
/** クローズ処理実装関数: for [[XMLStreamWriter]] */
implicit val closingXMLStreamWriter: Closable[XMLStreamWriter] = Closable { (t: XMLStreamWriter) => t.close() }
//////////////////////////////////////////////////////
/**
* Closeable ローンパターンメソッド
*
* 処理関数 `f` の処理後にターゲットリソース `t` をクローズする
* @param t クローズターゲットリソース
* @param f ターゲットリソースを使用する処理関数
* @param c クローズ処理実装関数
* @tparam T Closeable Type
* @tparam R Return Type
* @return 非同期処理関数の評価結果(Future)
*/
def using[T, R](t: T)(f: T => R)(implicit c: Closable[_ >: T]): R = {
ultimately { c.close(t) } apply { f(t) }
}
/**
* 非同期用 Closeable ローンパターンメソッド
*
* 処理関数 `f` により生成された `Future` の処理後にターゲットリソース `t` をクローズする
* @param t クローズターゲットリソース
* @param f ターゲットリソースを使用する非同期処理関数
* @param c クローズ処理実装関数
* @tparam T Closeable Type
* @tparam R Return Type
* @return 非同期処理関数の評価結果(Future)
*/
def usingAsync[T, R](t: T)(f: T => Future[R])(implicit c: Closable[_ >: T], ec: ExecutionContext): Future[R] = {
Future.successful(t) flatMap f andThen { case _ => c.close(t) }
}
}