This repository has been archived by the owner on Oct 13, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
IO.scala
938 lines (837 loc) · 38.3 KB
/
IO.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
/* sbt -- Simple Build Tool
* Copyright 2008, 2009, 2010 Mark Harrah, Viktor Klang, Ross McDonald
*/
package sbt
import Using._
import ErrorHandling.translate
import java.io.{ BufferedReader, ByteArrayOutputStream, BufferedWriter, File, FileInputStream, InputStream, OutputStream, PrintWriter }
import java.io.{ ObjectInputStream, ObjectStreamClass }
import java.net.{ URI, URISyntaxException, URL }
import java.nio.charset.Charset
import java.util.Properties
import java.util.jar.{ Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest }
import java.util.zip.{ CRC32, GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream }
import scala.collection.immutable.TreeSet
import scala.collection.mutable.{ HashMap, HashSet }
import scala.reflect.{ Manifest => SManifest }
import Function.tupled
/** A collection of File, URL, and I/O utility methods.*/
object IO {
/** The maximum number of times a unique temporary filename is attempted to be created.*/
private val MaximumTries = 10
/** The producer of randomness for unique name generation.*/
private lazy val random = new java.util.Random
val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))
/** The size of the byte or char buffer used in various methods.*/
private val BufferSize = 8192
/** File scheme name */
private[sbt] val FileScheme = "file"
/** The newline string for this system, as obtained by the line.separator system property. */
val Newline = System.getProperty("line.separator")
val utf8 = Charset.forName("UTF-8")
/**
* Returns a URL for the directory or jar containing the class file for type `T` (as determined by an implicit Manifest).
* If the location cannot be determined, an error is generated.
* Note that Java standard library classes typically do not have a location associated with them.
*/
@deprecated("Use classfileLocation or classLocationFile", "0.13.10")
def classLocation[T](implicit mf: SManifest[T]): URL = classLocation(mf.runtimeClass)
/**
* Returns a URL for the directory or jar containing the the class file `cl`.
* If the location cannot be determined, an error is generated.
*/
@deprecated("Use classfileLocation or classLocationFile", "0.13.10")
def classLocation(cl: Class[_]): URL = {
val codeSource = cl.getProtectionDomain.getCodeSource
if (codeSource ne null) {
codeSource.getLocation
} else {
// NB: This assumes that classes without code sources are System classes, and thus located in
// jars. It assumes that `urlAsFile` will truncate to the containing jar file.
val clsfile = s"${cl.getName.replace('.', '/')}.class"
Option(ClassLoader.getSystemClassLoader.getResource(clsfile))
.flatMap {
urlAsFile
}.getOrElse {
sys.error("No class location for " + cl)
}.toURI.toURL
}
}
/**
* Returns the directory or jar file containing the the class file for type `T` (as determined by an implicit Manifest).
* If the location cannot be determined, an error is generated.
* Note that Java standard library classes typically do not have a location associated with them.
*/
def classLocationFile[T](implicit mf: SManifest[T]): File = classLocationFile(mf.runtimeClass)
/**
* Returns the directory or jar file containing the class file `cl`.
* If the location cannot be determined or if it is not a file, an error is generated.
* Note that Java standard library classes typically do not have a location associated with them.
*/
def classLocationFile(cl: Class[_]): File =
Option(cl.getProtectionDomain.getCodeSource) match {
case Some(codeSource) =>
val classURL = codeSource.getLocation
toFile(classURL)
case None =>
// NB: This assumes that classes without code sources are System classes, and thus located in
// jars. It assumes that `urlAsFile` will truncate to the containing jar file.
val clsfile = s"${cl.getName.replace('.', '/')}.class"
Option(ClassLoader.getSystemClassLoader.getResource(clsfile))
.flatMap {
urlAsFile
}.getOrElse {
sys.error("No class location for " + cl)
}
}
/**
* Returns a URL for the classfile containing the given class file for type `T` (as determined by an implicit Manifest).
* If the location cannot be determined, an error is generated.
*/
def classfileLocation[T](implicit mf: SManifest[T]): URL = classfileLocation(mf.runtimeClass)
/**
* Returns a URL for the classfile containing the given class
* If the location cannot be determined, an error is generated.
*/
def classfileLocation(cl: Class[_]): URL = {
val clsfile = s"${cl.getName.replace('.', '/')}.class"
try {
Stream(Option(cl.getClassLoader), Some(ClassLoader.getSystemClassLoader)).flatten.flatMap { classLoader =>
Option(classLoader.getResource(clsfile))
}.headOption.getOrElse {
sys.error("No class location for " + cl)
}
} catch {
case e: Throwable =>
e.printStackTrace()
throw e
}
}
/**
* Constructs a File corresponding to `url`, which must have a scheme of `file`.
* This method properly works around an issue with a simple conversion to URI and then to a File.
*
* On Windows this can accept the following patterns of URLs:
*
* `val u0 = new URL("file:C:\\Users\\foo/.sbt/preloaded")`,
* `val u1 = new URL("file:/C:\\Users\\foo/.sbt/preloaded")`,
* `val u2 = new URL("file://unc/Users/foo/.sbt/preloaded")`,
* `val u3 = new URL("file:///C:\\Users\\foo/.sbt/preloaded")`, and
* `val u4 = new URL("file:////unc/Users/foo/.sbt/preloaded")`.
*/
def toFile(url: URL): File =
try { uriToFile(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
/** Converts the given URL to a File. If the URL is for an entry in a jar, the File for the jar is returned. */
def asFile(url: URL): File = urlAsFile(url) getOrElse sys.error("URL is not a file: " + url)
def urlAsFile(url: URL): Option[File] =
url.getProtocol match {
case FileScheme => Some(toFile(url))
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
Some(uriToFile(if (end == -1) path else path.substring(0, end)))
case _ => None
}
private[this] def uriToFile(uriString: String): File = uriToFile(new URI(uriString))
/**
* Converts the given file URI to a File.
*/
private[this] def uriToFile(uri: URI): File =
{
assert(uri.getScheme == FileScheme, "Expected protocol to be '" + FileScheme + "' in URI " + uri)
val part = uri.getSchemeSpecificPart
Option(uri.getAuthority) match {
case None if part startsWith "/" => new File(uri)
case _ =>
// https://github.com/sbt/sbt/issues/564
// https://github.com/sbt/sbt/issues/3086
// http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
// The specific problem here is that `uri` will have a defined authority component for UNC names like //foo/bar/some/path.jar
// but the File constructor requires URIs with an undefined authority component.
if (part startsWith "/") new File(part)
else new File("//" + part)
}
}
def assertDirectory(file: File): Unit =
assert(file.isDirectory, (if (file.exists) "Not a directory: " else "Directory not found: ") + file)
def assertDirectories(file: File*): Unit = file.foreach(assertDirectory)
// "base.extension" -> (base, extension)
/**
* Splits the given string into base and extension strings.
* If `name` contains no period, the base string is the input string and the extension is the empty string.
* Otherwise, the base is the substring up until the last period (exclusive) and
* the extension is the substring after the last period.
*
* For example, `split("Build.scala") == ("Build", "scala")`
*/
def split(name: String): (String, String) =
{
val lastDot = name.lastIndexOf('.')
if (lastDot >= 0)
(name.substring(0, lastDot), name.substring(lastDot + 1))
else
(name, "")
}
/**
* Each input file in `files` is created if it doesn't exist.
* If a file already exists, the last modified time is set to the current time.
* It is not guaranteed that all files will have the same last modified time after this call.
*/
def touch(files: Traversable[File]): Unit = files.foreach(f => touch(f))
/**
* Creates a file at the given location if it doesn't exist.
* If the file already exists and `setModified` is true, this method sets the last modified time to the current time.
*/
def touch(file: File, setModified: Boolean = true): Unit = {
val absFile = file.getAbsoluteFile
createDirectory(absFile.getParentFile)
val created = translate("Could not create file " + absFile) { absFile.createNewFile() }
if (created || absFile.isDirectory)
()
else if (setModified && !absFile.setLastModified(System.currentTimeMillis))
sys.error("Could not update last modified time for file " + absFile)
}
/** Creates directories `dirs` and all parent directories. It tries to work around a race condition in `File.mkdirs()` by retrying up to a limit.*/
def createDirectories(dirs: Traversable[File]): Unit =
dirs.foreach(createDirectory)
/** Creates directory `dir` and all parent directories. It tries to work around a race condition in `File.mkdirs()` by retrying up to a limit.*/
def createDirectory(dir: File): Unit =
{
def failBase = "Could not create directory " + dir
// Need a retry because mkdirs() has a race condition
var tryCount = 0
while (!dir.exists && !dir.mkdirs() && tryCount < 100) { tryCount += 1 }
if (dir.isDirectory)
()
else if (dir.exists) {
sys.error(failBase + ": file exists and is not a directory.")
} else
sys.error(failBase)
}
/** Gzips the file 'in' and writes it to 'out'. 'in' cannot be the same file as 'out'. */
def gzip(in: File, out: File): Unit = {
require(in != out, "Input file cannot be the same as the output file.")
Using.fileInputStream(in) { inputStream =>
Using.fileOutputStream()(out) { outputStream =>
gzip(inputStream, outputStream)
}
}
}
/** Gzips the InputStream 'in' and writes it to 'output'. Neither stream is closed.*/
def gzip(input: InputStream, output: OutputStream): Unit =
gzipOutputStream(output) { gzStream => transfer(input, gzStream) }
/** Gunzips the file 'in' and writes it to 'out'. 'in' cannot be the same file as 'out'. */
def gunzip(in: File, out: File): Unit = {
require(in != out, "Input file cannot be the same as the output file.")
Using.fileInputStream(in) { inputStream =>
Using.fileOutputStream()(out) { outputStream =>
gunzip(inputStream, outputStream)
}
}
}
/** Gunzips the InputStream 'input' and writes it to 'output'. Neither stream is closed.*/
def gunzip(input: InputStream, output: OutputStream): Unit =
gzipInputStream(input) { gzStream => transfer(gzStream, output) }
def unzip(from: File, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
fileInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))
def unzipURL(from: URL, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
urlInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))
def unzipStream(from: InputStream, toDirectory: File, filter: NameFilter = AllPassFilter, preserveLastModified: Boolean = true): Set[File] =
{
createDirectory(toDirectory)
zipInputStream(from) { zipInput => extract(zipInput, toDirectory, filter, preserveLastModified) }
}
private def extract(from: ZipInputStream, toDirectory: File, filter: NameFilter, preserveLastModified: Boolean) =
{
val set = new HashSet[File]
def next(): Unit = {
val entry = from.getNextEntry
if (entry == null)
()
else {
val name = entry.getName
if (filter.accept(name)) {
val target = new File(toDirectory, name)
//log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
if (entry.isDirectory)
createDirectory(target)
else {
set += target
translate("Error extracting zip entry '" + name + "' to '" + target + "': ") {
fileOutputStream(false)(target) { out => transfer(from, out) }
}
}
if (preserveLastModified)
target.setLastModified(entry.getTime)
} else {
//log.debug("Ignoring zip entry '" + name + "'")
}
from.closeEntry()
next()
}
}
next()
Set() ++ set
}
/** Retrieves the content of the given URL and writes it to the given File. */
def download(url: URL, to: File) =
Using.urlInputStream(url) { inputStream =>
transfer(inputStream, to)
}
/** Copies the contents of `in` to `out`.*/
def transfer(in: File, out: File): Unit =
fileInputStream(in) { in => transfer(in, out) }
/**
* Copies the contents of the input file `in` to the `out` stream.
* The output stream is not closed by this method.
*/
def transfer(in: File, out: OutputStream): Unit =
fileInputStream(in) { in => transfer(in, out) }
/** Copies all bytes from the given input stream to the given File. The input stream is not closed by this method.*/
def transfer(in: InputStream, to: File): Unit =
Using.fileOutputStream()(to) { outputStream =>
transfer(in, outputStream)
}
/**
* Copies all bytes from the given input stream to the given output stream.
* Neither stream is closed.
*/
def transfer(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, false)
/**
* Copies all bytes from the given input stream to the given output stream. The
* input stream is closed after the method completes.
*/
def transferAndClose(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, true)
private def transferImpl(in: InputStream, out: OutputStream, close: Boolean): Unit = {
try {
val buffer = new Array[Byte](BufferSize)
def read(): Unit = {
val byteCount = in.read(buffer)
if (byteCount >= 0) {
out.write(buffer, 0, byteCount)
read()
}
}
read()
} finally { if (close) in.close }
}
/**
* Creates a temporary directory and provides its location to the given function. The directory
* is deleted after the function returns.
*/
def withTemporaryDirectory[T](action: File => T): T =
{
val dir = createTemporaryDirectory
try { action(dir) }
finally { delete(dir) }
}
/** Creates a directory in the default temporary directory with a name generated from a random integer. */
def createTemporaryDirectory: File = createUniqueDirectory(temporaryDirectory)
/** Creates a directory in `baseDirectory` with a name generated from a random integer */
def createUniqueDirectory(baseDirectory: File): File =
{
def create(tries: Int): File =
{
if (tries > MaximumTries)
sys.error("Could not create temporary directory.")
else {
val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
val f = new File(baseDirectory, randomName)
try { createDirectory(f); f }
catch { case e: Exception => create(tries + 1) }
}
}
create(0)
}
/**
* Creates a file in the default temporary directory, calls `action` with the file, deletes the file, and returns the result of calling `action`.
* The name of the file will begin with `prefix`, which must be at least three characters long, and end with `postfix`, which has no minimum length.
*/
def withTemporaryFile[T](prefix: String, postfix: String)(action: File => T): T =
{
val file = File.createTempFile(prefix, postfix)
try { action(file) }
finally { file.delete() }
}
private[sbt] def jars(dir: File): Iterable[File] = listFiles(dir, GlobFilter("*.jar"))
/** Deletes all empty directories in the set. Any non-empty directories are ignored. */
def deleteIfEmpty(dirs: collection.Set[File]): Unit =
{
val isEmpty = new HashMap[File, Boolean]
def visit(f: File): Boolean = isEmpty.getOrElseUpdate(f, dirs(f) && f.isDirectory && (f.listFiles forall visit))
dirs foreach visit
for ((f, true) <- isEmpty) f.delete
}
/** Deletes each file or directory (recursively) in `files`.*/
def delete(files: Iterable[File]): Unit = files.foreach(delete)
/** Deletes each file or directory in `files` recursively. Any empty parent directories are deleted, recursively.*/
def deleteFilesEmptyDirs(files: Iterable[File]): Unit =
{
def isEmptyDirectory(dir: File) = dir.isDirectory && listFiles(dir).isEmpty
def parents(fs: Set[File]) = fs flatMap { f => Option(f.getParentFile) }
def deleteEmpty(dirs: Set[File]): Unit = {
val empty = dirs filter isEmptyDirectory
if (empty.nonEmpty) // looks funny, but this is true if at least one of `dirs` is an empty directory
{
empty foreach { _.delete() }
deleteEmpty(parents(empty))
}
}
delete(files)
deleteEmpty(parents(files.toSet))
}
/** Deletes `file`, recursively if it is a directory. */
def delete(file: File): Unit = {
translate("Error deleting file " + file + ": ") {
val deleted = file.delete()
if (!deleted && file.isDirectory) {
delete(listFiles(file))
file.delete
}
}
}
/** Returns the children of directory `dir` that match `filter` in a non-null array.*/
def listFiles(filter: java.io.FileFilter)(dir: File): Array[File] = wrapNull(dir.listFiles(filter))
/** Returns the children of directory `dir` that match `filter` in a non-null array.*/
def listFiles(dir: File, filter: java.io.FileFilter): Array[File] = wrapNull(dir.listFiles(filter))
/** Returns the children of directory `dir` in a non-null array.*/
def listFiles(dir: File): Array[File] = wrapNull(dir.listFiles())
private[sbt] def wrapNull(a: Array[File]) =
if (a == null)
new Array[File](0)
else
a
/**
* Creates a jar file.
* @param sources The files to include in the jar file paired with the entry name in the jar. Only the pairs explicitly listed are included.
* @param outputJar The file to write the jar to.
* @param manifest The manifest for the jar.
*/
def jar(sources: Traversable[(File, String)], outputJar: File, manifest: Manifest): Unit =
archive(sources.toSeq, outputJar, Some(manifest))
/**
* Creates a zip file.
* @param sources The files to include in the zip file paired with the entry name in the zip. Only the pairs explicitly listed are included.
* @param outputZip The file to write the zip to.
*/
def zip(sources: Traversable[(File, String)], outputZip: File): Unit =
archive(sources.toSeq, outputZip, None)
private def archive(sources: Seq[(File, String)], outputFile: File, manifest: Option[Manifest]): Unit = {
if (outputFile.isDirectory)
sys.error("Specified output file " + outputFile + " is a directory.")
else {
val outputDir = outputFile.getParentFile
createDirectory(outputDir)
withZipOutput(outputFile, manifest) { output =>
val createEntry: (String => ZipEntry) = if (manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
writeZip(sources, output)(createEntry)
}
}
}
private def writeZip(sources: Seq[(File, String)], output: ZipOutputStream)(createEntry: String => ZipEntry): Unit = {
val files = sources.flatMap { case (file, name) => if (file.isFile) (file, normalizeName(name)) :: Nil else Nil }
val now = System.currentTimeMillis
// The CRC32 for an empty value, needed to store directories in zip files
val emptyCRC = new CRC32().getValue()
def addDirectoryEntry(name: String): Unit = {
output putNextEntry makeDirectoryEntry(name)
output.closeEntry()
}
def makeDirectoryEntry(name: String) =
{
// log.debug("\tAdding directory " + relativePath + " ...")
val e = createEntry(name)
e setTime now
e setSize 0
e setMethod ZipEntry.STORED
e setCrc emptyCRC
e
}
def makeFileEntry(file: File, name: String) =
{
// log.debug("\tAdding " + file + " as " + name + " ...")
val e = createEntry(name)
e setTime file.lastModified
e
}
def addFileEntry(file: File, name: String): Unit = {
output putNextEntry makeFileEntry(file, name)
transfer(file, output)
output.closeEntry()
}
//Calculate directories and add them to the generated Zip
allDirectoryPaths(files) foreach addDirectoryEntry
//Add all files to the generated Zip
files foreach { case (file, name) => addFileEntry(file, name) }
}
// map a path a/b/c to List("a", "b")
private def relativeComponents(path: String): List[String] =
path.split("/").toList.dropRight(1)
// map components List("a", "b", "c") to List("a/b/c/", "a/b/", "a/", "")
private def directories(path: List[String]): List[String] =
path.foldLeft(List(""))((e, l) => (e.head + l + "/") :: e)
// map a path a/b/c to List("a/b/", "a/")
private def directoryPaths(path: String): List[String] =
directories(relativeComponents(path)).filter(_.length > 1)
// produce a sorted list of all the subdirectories of all provided files
private def allDirectoryPaths(files: Iterable[(File, String)]) =
TreeSet[String]() ++ (files flatMap { case (file, name) => directoryPaths(name) })
private def normalizeDirName(name: String) =
{
val norm1 = normalizeName(name)
if (norm1.endsWith("/")) norm1 else (norm1 + "/")
}
private def normalizeName(name: String) =
{
val sep = File.separatorChar
if (sep == '/') name else name.replace(sep, '/')
}
private def withZipOutput(file: File, manifest: Option[Manifest])(f: ZipOutputStream => Unit): Unit = {
fileOutputStream(false)(file) { fileOut =>
val (zipOut, ext) =
manifest match {
case Some(mf) =>
{
import Attributes.Name.MANIFEST_VERSION
val main = mf.getMainAttributes
if (!main.containsKey(MANIFEST_VERSION))
main.put(MANIFEST_VERSION, "1.0")
(new JarOutputStream(fileOut, mf), "jar")
}
case None => (new ZipOutputStream(fileOut), "zip")
}
try { f(zipOut) }
finally { zipOut.close }
}
}
/**
* Returns the relative file for `file` relative to directory `base` or None if `base` is not a parent of `file`.
* If `file` or `base` are not absolute, they are first resolved against the current working directory.
*/
def relativizeFile(base: File, file: File): Option[File] = relativize(base, file).map { path => new File(path) }
/**
* Returns the path for `file` relative to directory `base` or None if `base` is not a parent of `file`.
* If `file` or `base` are not absolute, they are first resolved against the current working directory.
*/
def relativize(base: File, file: File): Option[String] =
{
val pathString = file.getAbsolutePath
baseFileString(base) flatMap
{
baseString =>
{
if (pathString.startsWith(baseString))
Some(pathString.substring(baseString.length))
else
None
}
}
}
private def baseFileString(baseFile: File): Option[String] =
{
if (baseFile.isDirectory) {
val cp = baseFile.getAbsolutePath
assert(cp.length > 0)
val normalized = if (cp.charAt(cp.length - 1) == File.separatorChar) cp else cp + File.separatorChar
Some(normalized)
} else
None
}
/**
* For each pair in `sources`, copies the contents of the first File (the source) to the location of the second File (the target).
*
* A source file is always copied if `overwrite` is true.
* If `overwrite` is false, the source is only copied if the target is missing or is older than the source file according to last modified times.
* If the source is a directory, the corresponding directory is created.
*
* If `preserveLastModified` is `true`, the last modified times are transferred as well.
* Any parent directories that do not exist are created.
* The set of all target files is returned, whether or not they were updated by this method.
*/
def copy(sources: Traversable[(File, File)], overwrite: Boolean = false, preserveLastModified: Boolean = false): Set[File] =
sources.map(tupled(copyImpl(overwrite, preserveLastModified))).toSet
private def copyImpl(overwrite: Boolean, preserveLastModified: Boolean)(from: File, to: File): File =
{
if (overwrite || !to.exists || from.lastModified > to.lastModified) {
if (from.isDirectory)
createDirectory(to)
else {
createDirectory(to.getParentFile)
copyFile(from, to, preserveLastModified)
}
}
to
}
/**
* Copies the contents of each file in the `source` directory to the corresponding file in the `target` directory.
* A source file is always copied if `overwrite` is true.
* If `overwrite` is false, the source is only copied if the target is missing or is older than the source file according to last modified times.
* Files in `target` without a corresponding file in `source` are left unmodified in any case.
* If `preserveLastModified` is `true`, the last modified times are transferred as well.
* Any parent directories that do not exist are created.
*/
def copyDirectory(source: File, target: File, overwrite: Boolean = false, preserveLastModified: Boolean = false): Unit =
copy((PathFinder(source) ***) pair Path.rebase(source, target), overwrite, preserveLastModified)
/**
* Copies the contents of `sourceFile` to the location of `targetFile`, overwriting any existing content.
* If `preserveLastModified` is `true`, the last modified time is transferred as well.
*/
def copyFile(sourceFile: File, targetFile: File, preserveLastModified: Boolean = false): Unit = {
// 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 =>
// 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)
sys.error("Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)")
}
}
if (preserveLastModified)
copyLastModified(sourceFile, targetFile)
}
/** Transfers the last modified time of `sourceFile` to `targetFile`. */
def copyLastModified(sourceFile: File, targetFile: File) = {
val last = sourceFile.lastModified
// lastModified can return a negative number, but setLastModified doesn't accept it
// see Java bug #6791812
targetFile.setLastModified(math.max(last, 0L))
}
/** The default Charset used when not specified: UTF-8. */
def defaultCharset = utf8
/**
* Writes `content` to `file` using `charset` or UTF-8 if `charset` is not explicitly specified.
* If `append` is `false`, the existing contents of `file` are overwritten.
* If `append` is `true`, the new `content` is appended to the existing contents.
* If `file` or any parent directories do not exist, they are created.
*/
def write(file: File, content: String, charset: Charset = defaultCharset, append: Boolean = false): Unit =
writer(file, content, charset, append) { _.write(content) }
def writer[T](file: File, content: String, charset: Charset, append: Boolean = false)(f: BufferedWriter => T): T =
if (charset.newEncoder.canEncode(content))
fileWriter(charset, append)(file) { f }
else
sys.error("String cannot be encoded by charset " + charset.name)
def reader[T](file: File, charset: Charset = defaultCharset)(f: BufferedReader => T): T =
fileReader(charset)(file) { f }
/** Reads the full contents of `file` into a String using `charset` or UTF-8 if `charset` is not explicitly specified. */
def read(file: File, charset: Charset = defaultCharset): String =
{
val out = new ByteArrayOutputStream(file.length.toInt)
transfer(file, out)
out.toString(charset.name)
}
/** Reads the full contents of `in` into a byte array. This method does not close `in`.*/
def readStream(in: InputStream, charset: Charset = defaultCharset): String =
{
val out = new ByteArrayOutputStream
transfer(in, out)
out.toString(charset.name)
}
/** Reads the full contents of `in` into a byte array. */
def readBytes(file: File): Array[Byte] = fileInputStream(file)(readBytes)
/** Reads the full contents of `in` into a byte array. This method does not close `in`. */
def readBytes(in: InputStream): Array[Byte] =
{
val out = new ByteArrayOutputStream
transfer(in, out)
out.toByteArray
}
/**
* Appends `content` to the existing contents of `file` using `charset` or UTF-8 if `charset` is not explicitly specified.
* If `file` does not exist, it is created, as are any parent directories.
*/
def append(file: File, content: String, charset: Charset = defaultCharset): Unit =
write(file, content, charset, true)
/**
* Appends `bytes` to the existing contents of `file`.
* If `file` does not exist, it is created, as are any parent directories.
*/
def append(file: File, bytes: Array[Byte]): Unit =
writeBytes(file, bytes, true)
/**
* Writes `bytes` to `file`, overwriting any existing content.
* If any parent directories do not exist, they are first created.
*/
def write(file: File, bytes: Array[Byte]): Unit =
writeBytes(file, bytes, false)
private def writeBytes(file: File, bytes: Array[Byte], append: Boolean): Unit =
fileOutputStream(append)(file) { _.write(bytes) }
/** Reads all of the lines from `url` using the provided `charset` or UTF-8 if `charset` is not explicitly specified. */
def readLinesURL(url: URL, charset: Charset = defaultCharset): List[String] =
urlReader(charset)(url)(readLines)
/** Reads all of the lines in `file` using the provided `charset` or UTF-8 if `charset` is not explicitly specified. */
def readLines(file: File, charset: Charset = defaultCharset): List[String] =
fileReader(charset)(file)(readLines)
/** Reads all of the lines from `in`. This method does not close `in`.*/
def readLines(in: BufferedReader): List[String] =
foldLines[List[String]](in, Nil)((accum, line) => line :: accum).reverse
/** Applies `f` to each line read from `in`. This method does not close `in`.*/
def foreachLine(in: BufferedReader)(f: String => Unit): Unit =
foldLines(in, ())((_, line) => f(line))
/**
* Applies `f` to each line read from `in` and the accumulated value of type `T`, with initial value `init`.
* This method does not close `in`.
*/
def foldLines[T](in: BufferedReader, init: T)(f: (T, String) => T): T =
{
def readLine(accum: T): T =
{
val line = in.readLine()
if (line eq null) accum else readLine(f(accum, line))
}
readLine(init)
}
/**
* Writes `lines` to `file` using the given `charset` or UTF-8 if `charset` is not explicitly specified.
* If `append` is `false`, the contents of the file are overwritten.
* If `append` is `true`, the lines are appended to the file.
* A newline is written after each line and NOT before the first line.
* If any parent directories of `file` do not exist, they are first created.
*/
def writeLines(file: File, lines: Seq[String], charset: Charset = defaultCharset, append: Boolean = false): Unit =
writer(file, lines.headOption.getOrElse(""), charset, append) { w =>
lines.foreach { line => w.write(line); w.newLine() }
}
/** Writes `lines` to `writer` using `writer`'s `println` method. */
def writeLines(writer: PrintWriter, lines: Seq[String]): Unit =
lines foreach writer.println
/**
* Writes `properties` to the File `to`, using `label` as the comment on the first line.
* If any parent directories of `to` do not exist, they are first created.
*/
def write(properties: Properties, label: String, to: File) =
fileOutputStream()(to) { output => properties.store(output, label) }
/** Reads the properties in `from` into `properties`. If `from` does not exist, `properties` is left unchanged.*/
def load(properties: Properties, from: File): Unit =
if (from.exists)
fileInputStream(from) { input => properties.load(input) }
/** A pattern used to split a String by path separator characters.*/
private val PathSeparatorPattern = java.util.regex.Pattern.compile(File.pathSeparator)
/** Splits a String around the platform's path separator characters. */
def pathSplit(s: String) = PathSeparatorPattern.split(s)
/**
* Move the provided files to a temporary location.
* If 'f' returns normally, delete the files.
* If 'f' throws an Exception, return the files to their original location.
*/
def stash[T](files: Set[File])(f: => T): T =
withTemporaryDirectory { dir =>
val stashed = stashLocations(dir, files.toArray)
move(stashed)
try { f } catch {
case e: Exception =>
try { move(stashed.map(_.swap)); throw e }
catch { case _: Exception => throw e }
}
}
private def stashLocations(dir: File, files: Array[File]) =
for ((file, index) <- files.zipWithIndex) yield (file, new File(dir, index.toHexString))
// TODO: the reference to the other move overload does not resolve, probably due to a scaladoc bug
/**
* For each pair in `files`, moves the contents of the first File to the location of the second.
* See [[move(File,File)]] for the behavior of the individual move operations.
*/
def move(files: Traversable[(File, File)]): Unit =
files.foreach(Function.tupled(move))
/**
* Moves the contents of `a` to the location specified by `b`.
* This method deletes any content already at `b` and creates any parent directories of `b` if they do not exist.
* It will first try `File.renameTo` and if that fails, resort to copying and then deleting the original file.
* In either case, the original File will not exist on successful completion of this method.
*/
def move(a: File, b: File): Unit =
{
if (b.exists)
delete(b)
createDirectory(b.getParentFile)
if (!a.renameTo(b)) {
copyFile(a, b, true)
delete(a)
}
}
/**
* Applies `f` to a buffered gzip `OutputStream` for `file`.
* The streams involved are opened before calling `f` and closed after it returns.
* The result is the result of `f`.
*/
def gzipFileOut[T](file: File)(f: OutputStream => T): T =
Using.fileOutputStream()(file) { fout =>
Using.gzipOutputStream(fout) { outg =>
Using.bufferedOutputStream(outg)(f)
}
}
/**
* Applies `f` to a buffered gzip `InputStream` for `file`.
* The streams involved are opened before calling `f` and closed after it returns.
* The result is the result of `f`.
*/
def gzipFileIn[T](file: File)(f: InputStream => T): T =
Using.fileInputStream(file) { fin =>
Using.gzipInputStream(fin) { ing =>
Using.bufferedInputStream(ing)(f)
}
}
/**
* Converts an absolute File to a URI. The File is converted to a URI (toURI),
* normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
* it does not already end with a slash.
*/
def directoryURI(dir: File): URI =
{
assertAbsolute(dir)
directoryURI(dir.toURI.normalize)
}
/**
* Converts an absolute File to a URI. The File is converted to a URI (toURI),
* normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
* it does not already end with a slash.
*/
def directoryURI(uri: URI): URI =
{
if (!uri.isAbsolute) return uri; //assertAbsolute(uri)
val str = uri.toASCIIString
val dirStr = if (str.endsWith("/") || uri.getScheme != FileScheme || Option(uri.getRawFragment).isDefined) str else str + "/"
(new URI(dirStr)).normalize
}
/** Converts the given File to a URI. If the File is relative, the URI is relative, unlike File.toURI*/
def toURI(f: File): URI =
// need to use the three argument URI constructor because the single argument version doesn't encode
if (f.isAbsolute) f.toURI else new URI(null, normalizeName(f.getPath), null)
/**
* Resolves `f` against `base`, which must be an absolute directory.
* The result is guaranteed to be absolute.
* If `f` is absolute, it is returned without changes.
*/
def resolve(base: File, f: File): File =
{
assertAbsolute(base)
val fabs = if (f.isAbsolute) f else new File(directoryURI(new File(base, f.getPath)))
assertAbsolute(fabs)
fabs
}
def assertAbsolute(f: File) = assert(f.isAbsolute, "Not absolute: " + f)
def assertAbsolute(uri: URI) = assert(uri.isAbsolute, "Not absolute: " + uri)
/** Parses a classpath String into File entries according to the current platform's path separator.*/
def parseClasspath(s: String): Seq[File] = IO.pathSplit(s).map(new File(_)).toSeq
/**
* Constructs an `ObjectInputStream` on `wrapped` that uses `loader` to load classes.
* See also [[https://github.com/sbt/sbt/issues/136 issue 136]].
*/
def objectInputStream(wrapped: InputStream, loader: ClassLoader): ObjectInputStream = new ObjectInputStream(wrapped) {
override def resolveClass(osc: ObjectStreamClass): Class[_] = {
val c = Class.forName(osc.getName, false, loader)
if (c eq null) super.resolveClass(osc) else c
}
}
}