/
File.scala
430 lines (371 loc) · 13.6 KB
/
File.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
package info.kwarc.mmt.api.utils
import java.awt.Desktop
import info.kwarc.mmt.api._
import java.io._
import java.util
import java.util.zip._
import scala.collection.mutable
/** File wraps around java.io.File to extend it with convenience methods
*
* see implicit conversions with java.io.File at the end of this file
*/
case class File(toJava: java.io.File) {
/** resolves an absolute or relative path string against this */
def resolve(s: String): File = {
val sf = new java.io.File(s)
val newfile = if (sf.isAbsolute)
sf
else
new java.io.File(toJava, s)
File(newfile.getCanonicalPath)
}
/** makes a file relative to this one */
def relativize(f: File): File = {
val relURI = FileURI(this).relativize(FileURI(f))
val FileURI(rel) = relURI
rel
}
/** opens this file using the associated (native) application */
def openInOS(): Unit = {
Desktop.getDesktop.open(toJava)
}
def canonical = File(toJava.getCanonicalFile)
/** appends one path segment */
def /(s: String): File = File(new java.io.File(toJava, s))
/** appends a list of path segments */
def /(ss: List[String]): File = ss.foldLeft(this) { case (sofar, next) => sofar / next }
/** appends a relative path */
def /(ss: FilePath): File = this / ss.segments
/** parent directory */
def up: File = {
val par = Option(toJava.getParentFile)
if (par.isEmpty) this else File(par.get)
}
/** tests if this exists, possibly in compressed form */
def existsCompressed = toJava.exists || Compress.name(this).exists
def isRoot = up == this
/** file name */
def name: String = toJava.getName
/** segments as a FilePath
*/
def toFilePath = FilePath(segments)
/** the list of file/directory/volume label names making up this file path
* an absolute Unix paths begin with an empty segment
*/
def segments: List[String] = {
val name = toJava.getName
val par = Option(toJava.getParentFile)
if (par.isEmpty)
if (name.isEmpty) if (toString.nonEmpty) List(toString.init) else Nil
else List(name) // name == "" iff this File is a root
else
File(par.get).segments ::: List(name)
}
def isAbsolute: Boolean = toJava.isAbsolute
override def toString = toJava.toString
/** @return the last file extension (if any) */
def getExtension: Option[String] = {
val name = toJava.getName
val posOfDot = name.lastIndexOf(".")
if (posOfDot == -1) None else Some(name.substring(posOfDot + 1))
}
/** sets the file extension (replaces existing extension, if any) */
def setExtension(ext: String): File = getExtension match {
case None => File(toString + "." + ext)
case Some(s) => File(toString.substring(0, toString.length - s.length) + ext)
}
/** appends a file extension (possibly resulting in multiple extensions) */
def addExtension(ext: String): File = getExtension match {
case None => setExtension(ext)
case Some(e) => setExtension(e + "." + ext)
}
/** removes the last file extension (if any) */
def stripExtension: File = getExtension match {
case None => this
case Some(s) => File(toString.substring(0, toString.length - s.length - 1))
}
/** removes the last file extension (if any) */
def stripExtensionCompressed = Compress.normalize(this).stripExtension
/** @return children of this directory */
def children: List[File] = {
if (toJava.isFile) Nil else {
val ls = toJava.list
if (ls == null) throw GeneralError("directory does not exist or is not accessible: " + toString)
ls.toList.sorted.map(this / _)
}
}
/** @return subdirectories of this directory */
def subdirs: List[File] = children.filter(_.toJava.isDirectory)
/** @return all files in this directory or any subdirectory */
def descendants: List[File] = children.flatMap {c =>
if (c.isDirectory) c.descendants else List(c)
}
/** @return true if that begins with this */
def <=(that: File) = that.segments.startsWith(segments)
/** if that<=this, return the remainder of this */
def -(that: File) = if (that <= this) Some(FilePath(this.segments.drop(that.segments.length))) else None
/** delete this, recursively if directory */
def deleteDir: Unit = {
children foreach {c =>
if (c.isDirectory) c.deleteDir
else c.toJava.delete
}
toJava.delete
}
}
/** a relative file path usually within an archive below a dimension */
case class FilePath(segments: List[String]) {
def toFile = File(toString)
def name: String = if (segments.nonEmpty) segments.last else ""
def dirPath = FilePath(if (segments.nonEmpty) segments.init else Nil)
/** append a segment */
def /(s: String): FilePath = this / FilePath(List(s))
/** append a path */
def /(p: FilePath): FilePath = FilePath(segments ::: p.segments)
override def toString: String = segments.mkString("/")
def getExtension = toFile.getExtension
def setExtension(e: String) = toFile.setExtension(e).toFilePath
def stripExtension = toFile.stripExtension.toFilePath
}
object EmptyPath extends FilePath(Nil)
object FilePath {
def apply(s:String): FilePath = FilePath(List(s))
implicit def filePathToList(fp: FilePath) = fp.segments
implicit def listToFilePath(l: List[String]) = FilePath(l)
def getall(f : File) : List[File] = rec(List(f))
private def rec(list : List[File]) : List[File] = list.flatMap(f => if (f.isDirectory) rec(f.children) else List(f))
}
/** Constructs and pattern-matches absolute file:URIs in terms of absolute File's.*/
object FileURI {
def apply(f: File): URI = {
val ss = f.segments
URI(Some("file"), None, if (ss.headOption.contains("")) ss.tail else ss, f.isAbsolute)
}
def unapply(u: URI): Option[File] = {
/* In contrast to RFC 8089 (https://tools.ietf.org/html/rfc8089), we allow for empty schemes for
"File URI References" that, like URIs, allow omitting stuff from the left. */
val valid_scheme : Boolean = u.scheme.isEmpty || u.scheme.contains("file")
// empty authority makes some Java versions throw errors
val valid_authority : Boolean = u.authority.isEmpty || u.authority.contains("") || u.authority.contains("localhost")
// We set authority to None because it's ignored later, anyway. No use to distinguish.
if (valid_scheme && valid_authority) {
val uR = u.copy(scheme = None, authority = None)
Some(File(uR.toString)) // one would expect File(new java.io.File(uR)) here but that constructor cannot handle relative paths in a URI
} else None
}
}
/** wrappers for streams that allow toggling compressions */
object Compress {
import org.tukaani.xz._
private val cache = BasicArrayCache.getInstance
def name(f: File) = f.addExtension("xz")
def normalize(f: File) = if (f.getExtension contains "xz") f.stripExtension else f
def out(s: OutputStream, c: Boolean) = if (c) new XZOutputStream(s, new LZMA2Options(), cache) else new BufferedOutputStream(s)
def in(s: InputStream, c: Boolean) = if (c) new XZInputStream(s, cache) else s
}
/** MMT's default way to write to files; uses buffering, UTF-8, and \n */
class StandardPrintWriter(f: File, compress: Boolean) extends
OutputStreamWriter(Compress.out(new FileOutputStream(f.toJava), compress), java.nio.charset.Charset.forName("UTF-8")) {
def println(s: String): Unit = {
write(s + "\n")
}
}
/** This defines some very useful methods to interact with text files at a high abstraction level. */
object File {
/** constructs a File from a string, using the java.io.File parser */
def apply(s: String): File = File(new java.io.File(s))
/** @param compress if true, the file is compressed while writing */
def Writer(f: File, compress: Boolean = false): StandardPrintWriter = {
f.up.toJava.mkdirs
val fC = if (compress) Compress.name(f) else f
new StandardPrintWriter(fC, compress)
}
/**
* convenience method for writing a string into a file
*
* overwrites existing files, creates directories if necessary
* @param f the target file
* @param strings the content to write
*/
def write(f: File, strings: String*): Unit = {
val fw = Writer(f)
strings.foreach { s => fw.write(s) }
fw.close
}
def append(f : File, strings: String*): Unit = {
scala.tools.nsc.io.File(f.toString).appendAll(strings:_*)
}
/**
* streams a list-like object to a file
* @param f the file to write to
* @param begin initial text
* @param sep text in between elements
* @param end terminal text
* @param work bind a variable "write" and call it to write into the file
* example: (l: List[Node]) => stream(f, "<root>", "\n", "</root>"){out => l map {a => out(a.toString)}}
*/
def stream(f: File, begin: String = "", sep: String = "", end: String="")(work: (String => Unit) => Unit) = {
val fw = Writer(f)
fw.write(begin)
var writeSep = false
def out(s: String): Unit = {
if (writeSep)
fw.write(sep)
else
writeSep = true
fw.write(s)
}
try {
work(out)
fw.write(end)
} finally {
fw.close
}
}
/**
* convenience method for writing a list of lines into a file
*
* overwrites existing files, creates directories if necessary
* @param f the target file
* @param lines the lines (without line terminator - will be chosen by Java and appended)
*/
def WriteLineWise(f: File, lines: List[String]): Unit = {
val fw = Writer(f)
lines.foreach {l =>
fw.println(l)
}
fw.close
}
/** convenience method to obtain a typical (buffered, UTF-8) reader for a file
*
* If f does not exist, Compress.name(f) is tried and automatically decompressed.
*/
def Reader(f: File): BufferedReader = {
val fC = Compress.name(f)
val compress = (f.isDirectory || !f.exists) && fC.exists
val fileName = if (compress) fC else f
val in = Compress.in(new FileInputStream(fileName.toJava), compress)
StreamReading.Reader(in)
}
/**
* convenience method for reading a file into a string
*
* @param f the source file
* @return s the file content (line terminators are \n)
*/
def read(f: File): String = {
val s = new StringBuilder
ReadLineWise(f) {l => s.append(l + "\n")}
s.result()
}
/** convenience method to read a file line by line
* @param f the file
* @param proc a function applied to every line (without line terminator)
*/
def ReadLineWise(f: File)(proc: String => Unit): Unit = {
val r = Reader(f)
var line: Option[String] = None
try {
while ( {
line = Option(r.readLine)
line.isDefined
})
proc(line.get)
} finally {
r.close
}
}
def readPropertiesFromString(s: String): mutable.Map[String, String] = {
val properties = new scala.collection.mutable.ListMap[String, String]
s.split("\n") foreach {line =>
// usually continuation lines start with a space but we ignore those
val tline = line.trim
if (!tline.startsWith("//")) {
val p = tline.indexOf(":")
if (p > 0) {
// make sure line contains colon and the key is non-empty
val key = tline.substring(0, p).trim
val value = tline.substring(p + 1).trim
properties(key) = properties.get(key) match {
case None => value
case Some(old) => old + " " + value
}
}
}
}
properties
}
def readProperties(manifest: File) = readPropertiesFromString(File.read(manifest))
/** current directory */
def currentDir = File(System.getProperty("user.dir"))
/** copies a file */
def copy(from: File, to: File, replace: Boolean): Boolean = {
if (!from.exists || (to.exists && !replace)) {
false
} else {
to.getParentFile.mkdirs
val opt = if (replace) List(java.nio.file.StandardCopyOption.REPLACE_EXISTING) else Nil
java.nio.file.Files.copy(from.toPath, to.toPath, opt:_*)
true
}
}
/** unzips a file */
def unzip(from: File, toDir: File, skipRootDir: Boolean = false): Unit = {
val mar = new ZipFile(from)
try {
var bytes = new Array[Byte](100000)
var len = -1
val enum = mar.entries
while (enum.hasMoreElements) {
val entry = enum.nextElement
var relPath = stringToList(entry.getName, "/")
if (skipRootDir && relPath.length > 1)
relPath = relPath.tail
val outFile = toDir / relPath
outFile.up.mkdirs
if (!entry.isDirectory) {
val istream = mar.getInputStream(entry)
val ostream = new java.io.FileOutputStream(outFile)
try {
while ({
len = istream.read(bytes, 0, bytes.length)
len != -1
}) {
ostream.write(bytes, 0, len)
}
} finally {
ostream.close
istream.close
}
}
}
} finally {
mar.close
}
}
/** dereference a URL and save as a file */
def download(uri: URI, file: File): Unit = {
val input = URI.get(uri)
file.up.mkdirs
val output = new java.io.FileOutputStream(file)
try {
val step = 8192
var byteArray = new Array[Byte](step)
var pos, n = 0
while ({
if (pos + step > byteArray.length) byteArray = util.Arrays.copyOf(byteArray, byteArray.length << 1)
n = input.read(byteArray, pos, step)
n != -1
}) pos += n
if (pos != byteArray.length) byteArray = util.Arrays.copyOf(byteArray, pos)
output.write(byteArray)
} finally {
input.close()
output.close()
}
}
/** implicit conversion Java <-> Scala */
implicit def scala2Java(file: File): java.io.File = file.toJava
/** implicit conversion Java <-> Scala */
implicit def java2Scala(file: java.io.File): File = File(file)
}