-
Notifications
You must be signed in to change notification settings - Fork 46
/
IO.scala
1497 lines (1338 loc) · 57.2 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
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* sbt IO
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package sbt.io
import java.io._
import java.net.{ URI, URISyntaxException, URL }
import java.nio.charset.Charset
import java.nio.file.attribute.PosixFilePermissions
import java.nio.file.{ Path => NioPath, _ }
import java.util.{ Locale, Properties }
import java.util.jar.{ Attributes, JarEntry, JarFile, JarOutputStream, Manifest }
import java.util.zip.{ CRC32, ZipEntry, ZipInputStream, ZipOutputStream }
import sbt.internal.io.ErrorHandling.translate
import sbt.internal.io.{ Milli, Retry }
import sbt.io.Using._
import sbt.nio.file.FileTreeView
import scala.Function.tupled
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection.immutable
import scala.collection.immutable.TreeSet
import scala.collection.mutable.{ HashMap, HashSet }
import scala.reflect.{ Manifest => SManifest }
import scala.util.control.Exception._
import scala.util.control.NonFatal
/** 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")
private lazy val jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"))
/**
* Returns the NIO Path to the directory, Java module, or the JAR file containing the class file `cl`.
* If the location cannot be determined, an error is generated.
* Note that for JDK 11 onwards, a module will return a jrt path.
*/
def classLocationPath(cl: Class[?]): NioPath = {
val u = classLocation(cl)
val p = u.getProtocol match {
case FileScheme => Option(toFile(u).toPath)
case "jar" => urlAsFile(u) map { _.toPath }
case "jrt" => Option(IO.jrtFs.getPath(u.getPath))
case _ => None
}
p.getOrElse(sys.error(s"Unable to create File from $u for $cl"))
}
/**
* Returns a NIO Path to the directory, Java module, or the JAR file for type `A` (as determined by an implicit Manifest).
* If the location cannot be determined, an error is generated.
* Note that for JDK 11 onwards, a module will return a jrt path.
*/
def classLocationPath[A](implicit mf: SManifest[A]): NioPath =
classLocationPath(mf.runtimeClass)
/**
* Returns the directory, Java module, or the JAR containing the class file `cl`.
* If the location cannot be determined or it is not a file, an error is generated.
* Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`, so it will return `None`.
*/
def classLocationFileOption(cl: Class[?]): Option[File] = {
val u = classLocation(cl)
urlAsFile(u)
}
/**
* Returns the directory, Java module, or the JAR containing the class file for type `T` (as determined by an implicit Manifest).
* If the location cannot be determined or it is not a file, an error is generated.
* Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`, so it will return `None`.
*/
def classLocationFileOption[A](implicit mf: SManifest[A]): Option[File] =
classLocationFileOption(mf.runtimeClass)
/**
* Returns the directory, Java module, or the JAR file containing the class file `cl`.
* If the location cannot be determined or it is not a file, an error is generated.
* Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`.
*/
@deprecated(
"classLocationFile may not work on JDK 11. Use classfileLocation, classLocationFileOption, or classLocationPath instead.",
"1.3.0"
)
def classLocationFile(cl: Class[?]): File =
classLocationFileOption(cl).getOrElse(sys.error(s"Unable to create File from $cl"))
/**
* Returns the directory, Java module, or the JAR file 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 for JDK 11 onwards, the returned module path cannot be expressed as `File`.
*/
@deprecated(
"classLocationFile may not work on JDK 11. Use classfileLocation, classLocationFileOption, or classLocationPath instead.",
"1.3.0"
)
def classLocationFile[T](implicit mf: SManifest[T]): File = classLocationFile(mf.runtimeClass)
/**
* Returns the URL to the directory, Java module, or the JAR file containing the class file `cl`.
* If the location cannot be determined or it is not a file, an error is generated.
* Note that for JDK 11 onwards, a module will return a jrt URL such as `jrt:/java.base`.
*/
def classLocation(cl: Class[?]): URL = {
def localcl: Option[URL] =
Option(cl.getProtectionDomain.getCodeSource) flatMap { codeSource =>
Option(codeSource.getLocation)
}
// This assumes that classes without code sources are System classes, and thus located in jars.
// It returns a URL that looks like jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar!/java/lang/Integer.class
val clsfile = s"${cl.getName.replace('.', '/')}.class"
def syscl: Option[URL] =
Option(ClassLoader.getSystemClassLoader) flatMap { classLoader =>
Option(classLoader.getResource(clsfile))
}
try {
localcl
.orElse(syscl)
.map(url =>
url.getProtocol match {
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
new URI(
if (end == -1) path
else path.substring(0, end)
).toURL
case "jrt" =>
val path = url.getPath
val end = path.indexOf('/', 1)
new URI(
s"jrt://${if (end == -1) path else path.substring(0, end)}"
).toURL
case _ => url
}
)
.getOrElse(sys.error("No class location for " + cl))
} catch {
case NonFatal(e) =>
e.printStackTrace()
throw e
}
}
/**
* Returns the URL to the directory, Java module, or the JAR file containing the class file `cl`.
* If the location cannot be determined or it is not a file, an error is generated.
* Note that for JDK 11 onwards, a module will return a jrt path.
*/
def classLocation[A](implicit mf: SManifest[A]): URL =
classLocation(mf.runtimeClass)
/**
* 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"
def localcl: Option[URL] =
Option(cl.getClassLoader) flatMap { classLoader =>
Option(classLoader.getResource(clsfile))
}
def syscl: Option[URL] =
Option(ClassLoader.getSystemClassLoader) flatMap { classLoader =>
Option(classLoader.getResource(clsfile))
}
try {
localcl
.orElse(syscl)
.getOrElse(sys.error("No class location for " + cl))
} catch {
case NonFatal(e) =>
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) }
def toFile(uri: URI): File =
try {
uriToFile(uri)
} catch { case _: URISyntaxException => new File(uri.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 = {
val part = uri.getSchemeSpecificPart
// scheme might be omitted for relative URI reference.
assert(
Option(uri.getScheme) match {
case None | Some(FileScheme) => true
case _ => false
},
s"Expected protocol to be '$FileScheme' or empty in URI $uri"
)
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 "/") && (part contains ":")) new File("//" + part)
else new File(part)
}
}
def assertDirectory(file: File) =
assert(
file.isDirectory,
(if (file.exists) "Not a directory: " else "Directory not found: ") + file
)
def assertDirectories(file: File*) = 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 = Retry {
val absFile = file.getAbsoluteFile
createDirectory(absFile.getParentFile)
val created = translate("Could not create file " + absFile) { absFile.createNewFile() }
if (created || absFile.isDirectory)
()
else if (setModified && !setModifiedTimeOrFalse(absFile, 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 Files.createDirectories may fail before succeeding on (at least) windows.
val path = dir.toPath
try
Retry(
try Files.createDirectories(path)
catch { case _: IOException if Files.isDirectory(path) => },
excludedExceptions = classOf[FileAlreadyExistsException]
)
catch { case e: IOException => throw new IOException(failBase + ": " + e, e) }
()
}
/** 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.toPath, filter, preserveLastModified)
)
}
private def extract(
from: ZipInputStream,
toDirectory: NioPath,
filter: NameFilter,
preserveLastModified: Boolean
) = {
val set = new HashSet[NioPath]
val canonicalDirPath = toDirectory.normalize().toString
def validateExtractPath(name: String, target: NioPath): Unit =
if (!target.normalize().toString.startsWith(canonicalDirPath)) {
throw new RuntimeException(s"Entry ($name) is outside of the target directory")
}
@tailrec def next(): Unit = {
val entry = from.getNextEntry
if (entry == null)
()
else {
val name = entry.getName
if (filter.accept(name)) {
val target = toDirectory.resolve(name)
validateExtractPath(name, target)
// log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
if (entry.isDirectory)
createDirectory(target.toFile)
else {
set += target
translate("Error extracting zip entry '" + name + "' to '" + target + "': ") {
fileOutputStream(false)(target.toFile)(out => transfer(from, out))
}
}
if (preserveLastModified)
setModifiedTimeOrFalse(target.toFile, entry.getTime)
} else {
// log.debug("Ignoring zip entry '" + name + "'")
}
from.closeEntry()
next()
}
}
next()
(Set() ++ set).map(_.toFile)
}
// TODO: provide a better API to download things.
// /** 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) = {
try {
val buffer = new Array[Byte](BufferSize)
@tailrec 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 if `keepDirectory` is set to false.
*/
def withTemporaryDirectory[T](action: File => T, keepDirectory: Boolean): T = {
val dir = createTemporaryDirectory
try {
action(dir)
} finally {
if (!keepDirectory) delete(dir)
}
}
/**
* Overload of `withTemporaryDirectory` with `keepDirectory` set to false.
*/
def withTemporaryDirectory[T](action: File => T): T =
withTemporaryDirectory(action, keepDirectory = false)
/** 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 NonFatal(_) => create(tries + 1) }
}
}
create(0)
}
/**
* Creates a file in the default temporary directory, calls `action` with the
* file, deletes the file if `keepFile` is set to true, 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, keepFile: Boolean)(
action: File => T
): T = {
val file = Files.createTempFile(prefix, postfix)
try {
action(file.toFile())
} finally {
if (!keepFile) file.toFile().delete(); ()
}
}
/**
* Overload of `withTemporaryFile` with `keepFile` set to false.
*/
def withTemporaryFile[T](prefix: String, postfix: String)(action: File => T): T =
withTemporaryFile(prefix, postfix, keepFile = false)(action)
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 (case (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))
@tailrec 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. Note that this method may silently fail to
* delete the file (or directory) if any errors occur.
*/
def delete(file: File): Unit = Retry {
try {
FileTreeView.default.list(file.toPath).foreach {
case (dir, attrs) if attrs.isDirectory => delete(dir.toFile)
case (f, _) =>
try Files.deleteIfExists(f)
catch { case _: IOException => }
}
} catch {
case _: IOException => // Silently fail to preserve legacy behavior.
}
try Files.deleteIfExists(file.toPath)
catch { case _: IOException => }
()
}
/** 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
@deprecated("Please specify whether to use a static timestamp", "1.3.2")
def jar(sources: Traversable[(File, String)], outputJar: File, manifest: Manifest): Unit =
archive(sources.toSeq, outputJar, Some(manifest), None)
/**
* 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.
* @param time static timestamp to use for all entries, if any, in milliseconds since Epoch
*/
def jar(
sources: Traversable[(File, String)],
outputJar: File,
manifest: Manifest,
time: Option[Long]
): Unit =
archive(sources.toSeq, outputJar, Some(manifest), time)
@deprecated("Please specify whether to use a static timestamp", "1.3.2")
def zip(sources: Traversable[(File, String)], outputZip: File): Unit =
archive(sources.toSeq, outputZip, None, None)
/**
* 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.
* @param time static timestamp to use for all entries, if any.
*/
def zip(sources: Traversable[(File, String)], outputZip: File, time: Option[Long]): Unit =
archive(sources.toSeq, outputZip, None, time)
private def archive(
sources: Seq[(File, String)],
outputFile: File,
manifest: Option[Manifest],
time: Option[Long]
) = {
// The zip 'setTime' methods try to convert from the given time to the local time based
// on java.util.TimeZone.getDefault(). When explicitly specifying the timestamp, we assume
// this already has been done if desired, so we need to 'convert back' here:
val localTime = time.map(t => t - java.util.TimeZone.getDefault().getOffset(t))
if (outputFile.isDirectory)
sys.error("Specified output file " + outputFile + " is a directory.")
else {
val outputDir = outputFile.getParentFile match {
case null => new File(".")
case parentFile => parentFile
}
createDirectory(outputDir)
withZipOutput(outputFile, manifest, localTime) { output =>
val createEntry: (String => ZipEntry) =
if (manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
writeZip(sources, output, localTime)(createEntry)
}
}
}
private def writeZip(sources: Seq[(File, String)], output: ZipOutputStream, time: Option[Long])(
createEntry: String => ZipEntry
) = {
val files = sources
.flatMap { case (file, name) =>
if (file.isFile) (file, normalizeToSlash(name)) :: Nil else Nil
}
.sortBy { case (_, name) =>
name
}
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) = {
output putNextEntry makeDirectoryEntry(name)
output.closeEntry()
}
def makeDirectoryEntry(name: String) = {
// log.debug("\tAdding directory " + relativePath + " ...")
val e = createEntry(name)
e setTime time.getOrElse(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 time.getOrElse(getModifiedTimeOrZero(file))
e
}
def addFileEntry(file: File, name: String) = {
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 (_, name) => directoryPaths(name) })
private def normalizeToSlash(name: String) = {
val sep = File.separatorChar
if (sep == '/') name else name.replace(sep, '/')
}
private def withZipOutput(file: File, manifest: Option[Manifest], time: Option[Long])(
f: ZipOutputStream => Unit
) = {
fileOutputStream(false)(file) { fileOut =>
val (zipOut, _) =
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")
val os = new JarOutputStream(fileOut)
val e = new ZipEntry(JarFile.MANIFEST_NAME)
e setTime time.getOrElse(System.currentTimeMillis)
os.putNextEntry(e)
mf.write(new BufferedOutputStream(os))
os.closeEntry()
(os, "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] = {
// "On UNIX systems, a pathname is absolute if its prefix is "/"."
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#isAbsolute
// "This typically involves removing redundant names such as "." and ".." from the pathname, resolving symbolic links (on UNIX platforms)"
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#getCanonicalPath()
// We do not want to use getCanonicalPath because if we resolve the symbolic link, that could change
// the outcome of copyDirectory's target structure.
// Path#normailize is able to expand ".." without expanding the symlink.
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/Path.html#normalize()
// "Returns a path that is this path with redundant name elements eliminated."
def toAbsolutePath(x: File): NioPath = {
val p = x.toPath
if (!p.isAbsolute) p.toAbsolutePath
else p
}
val basePath = toAbsolutePath(base).normalize
val filePath = toAbsolutePath(file).normalize
if (filePath startsWith basePath) {
val relativePath =
catching(classOf[IllegalArgumentException]) opt (basePath relativize filePath)
relativePath map (_.toString)
} else None
}
def copy(sources: Traversable[(File, File)]): Set[File] = copy(sources, CopyOptions())
/**
* For each pair in `sources`, copies the contents of the first File (the source) to the location
* of the second File (the target).
*
* See [[sbt.io.CopyOptions]] for docs on the options available.
*
* 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)], options: CopyOptions): Set[File] =
copy(sources, options.overwrite, options.preserveLastModified, options.preserveExecutable)
def copy(
sources: Traversable[(File, File)],
overwrite: Boolean,
preserveLastModified: Boolean,
preserveExecutable: Boolean
): Set[File] =
sources.map(tupled(copyImpl(overwrite, preserveLastModified, preserveExecutable))).toSet
private def copyImpl(
overwrite: Boolean,
preserveLastModified: Boolean,
preserveExecutable: Boolean
)(from: File, to: File): File = {
if (overwrite || !to.exists || getModifiedTimeOrZero(from) > getModifiedTimeOrZero(to)) {
if (from.isDirectory)
createDirectory(to)
else {
createDirectory(to.getParentFile)
copyFile(from, to, preserveLastModified, preserveExecutable)
}
}
to
}
def copyDirectory(source: File, target: File): Unit = copyDirectory(source, target, CopyOptions())
/**
* Copies the contents of each file in the `source` directory to the corresponding file in the
* `target` directory.
*
* See [[sbt.io.CopyOptions]] for docs on the options available.
*
* Files in `target` without a corresponding file in `source` are left unmodified in any case.
* Any parent directories that do not exist are created.
*/
def copyDirectory(source: File, target: File, options: CopyOptions): Unit =
copyDirectory(
source,
target,
options.overwrite,
options.preserveLastModified,
options.preserveExecutable
)
def copyDirectory(
source: File,
target: File,
overwrite: Boolean = false,
preserveLastModified: Boolean = false,
preserveExecutable: Boolean = true
): Unit = {
val sources = PathFinder(source).allPaths pair Path.rebase(source, target)
copy(sources, overwrite, preserveLastModified, preserveExecutable)
()
}
def copyFile(sourceFile: File, targetFile: File): Unit =
copyFile(sourceFile, targetFile, CopyOptions())
/**
* Copies the contents of `sourceFile` to the location of `targetFile`, overwriting any existing content.
*
* See [[sbt.io.CopyOptions]] for docs on the options available.
*/
def copyFile(sourceFile: File, targetFile: File, options: CopyOptions): Unit =
copyFile(sourceFile, targetFile, options.preserveLastModified, options.preserveExecutable)
def copyFile(
sourceFile: File,
targetFile: File,
preserveLastModified: Boolean = false,
preserveExecutable: Boolean = true
): 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 = (64L * 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)
()
}
if (preserveExecutable) {
copyExecutable(sourceFile, targetFile)
()
}
}
/** Transfers the executable property of `sourceFile` to `targetFile`. */
def copyExecutable(sourceFile: File, targetFile: File) = {
val executable = sourceFile.canExecute
if (executable) targetFile.setExecutable(true)
}
/** 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)