diff --git a/util/io/IO.scala b/util/io/IO.scala index 3a99b49e6..68f3030a3 100644 --- a/util/io/IO.scala +++ b/util/io/IO.scala @@ -525,11 +525,21 @@ object IO def copyFile(sourceFile: File, targetFile: File, preserveLastModified: Boolean = false) { + // NOTE: when modifying this code, test with larger values of CopySpec.MaxFileSizeBits than default + require(sourceFile.exists, "Source file '" + sourceFile.getAbsolutePath + "' does not exist.") require(!sourceFile.isDirectory, "Source file '" + sourceFile.getAbsolutePath + "' is a directory.") fileInputChannel(sourceFile) { in => fileOutputChannel(targetFile) { out => - val copied = out.transferFrom(in, 0, in.size) + // maximum bytes per transfer according to from http://dzone.com/snippets/java-filecopy-using-nio + val max = (64 * 1024 * 1024) - (32 * 1024) + val total = in.size + def loop(offset: Long): Long = + if(offset < total) + loop( offset + out.transferFrom(in, offset, max) ) + else + offset + val copied = loop(0) if(copied != in.size) error("Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)") } diff --git a/util/io/src/test/scala/CopySpec.scala b/util/io/src/test/scala/CopySpec.scala new file mode 100644 index 000000000..6cdf5eaf3 --- /dev/null +++ b/util/io/src/test/scala/CopySpec.scala @@ -0,0 +1,70 @@ +package sbt + +import java.io.File +import java.util.Arrays +import org.scalacheck._ +import Prop._ +import Arbitrary.arbLong + +object CopySpec extends Properties("Copy") +{ + // set to 0.25 GB by default for success on most systems without running out of space. + // when modifying IO.copyFile, verify against 1 GB or higher, preferably > 4 GB + final val MaxFileSizeBits = 28 + final val BufferSize = 1*1024*1024 + + val randomSize = Gen.choose(0, MaxFileSizeBits).map( 1L << _ ) + val pow2Size = (0 to (MaxFileSizeBits - 1)).toList.map( 1L << _ ) + val derivedSize = pow2Size.map(_ - 1) ::: pow2Size.map(_ + 1) ::: pow2Size + + val fileSizeGen: Gen[Long] = + Gen.frequency( + 80 -> Gen.oneOf(derivedSize), + 8 -> randomSize, + 1 -> Gen.value(0) + ) + + property("same contents") = forAll(fileSizeGen, arbLong.arbitrary) { (size: Long, seed: Long) => + IO.withTemporaryDirectory { dir => + val f1 = new File(dir, "source") + val f2 = new File(dir, "dest") + generate(seed = seed, size = size, file = f1) + IO.copyFile(f1, f2) + checkContentsSame(f1, f2) + true + } + } + + def generate(seed: Long, size: Long, file: File) { + val rnd = new java.util.Random(seed) + + val buffer = new Array[Byte](BufferSize) + def loop(offset: Long) { + val len = math.min(size - offset, BufferSize) + if(len > 0) { + rnd.nextBytes(buffer) + IO.append(file, buffer) + loop(offset + len) + } + } + if(size == 0L) IO.touch(file) else loop(0) + } + def checkContentsSame(f1: File, f2: File) { + val len = f1.length + assert(len == f2.length, "File lengths differ: " + (len, f2.length).toString + " for " + (f1, f2).toString) + Using.fileInputStream(f1) { in1 => + Using.fileInputStream(f2) { in2 => + val buffer1 = new Array[Byte](BufferSize) + val buffer2 = new Array[Byte](BufferSize) + def loop(offset: Long): Unit = if(offset < len) { + val read1 = in1.read(buffer1) + val read2 = in2.read(buffer2) + assert(read1 == read2, "Read " + (read1, read2).toString + " bytes from " + (f1, f2).toString) + assert(Arrays.equals(buffer1, buffer2), "Contents differed.") + loop(offset + read1) + } + loop(0) + } + } + } +} \ No newline at end of file