/
Utils.kt
444 lines (396 loc) · 17.2 KB
/
Utils.kt
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
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:JvmMultifileClass
@file:JvmName("FilesKt")
package kotlin.io
import java.io.*
import java.util.*
import kotlin.*
import kotlin.text.*
import kotlin.comparisons.*
/**
* Creates an empty directory in the specified [directory], using the given [prefix] and [suffix] to generate its name.
*
* If [prefix] is not specified then some unspecified name will be used.
* If [suffix] is not specified then ".tmp" will be used.
* If [directory] is not specified then the default temporary-file directory will be used.
*
* @return a file object corresponding to a newly-created directory.
*
* @throws IOException in case of input/output error.
* @throws IllegalArgumentException if [prefix] is shorter than three symbols.
*/
public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File {
val dir = File.createTempFile(prefix, suffix, directory)
dir.delete()
if (dir.mkdir()) {
return dir
} else {
throw IOException("Unable to create temporary directory $dir.")
}
}
/**
* Creates a new empty file in the specified [directory], using the given [prefix] and [suffix] to generate its name.
*
* If [prefix] is not specified then some unspecified name will be used.
* If [suffix] is not specified then ".tmp" will be used.
* If [directory] is not specified then the default temporary-file directory will be used.
*
* @return a file object corresponding to a newly-created file.
*
* @throws IOException in case of input/output error.
* @throws IllegalArgumentException if [prefix] is shorter than three symbols.
*/
public fun createTempFile(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File {
return File.createTempFile(prefix, suffix, directory)
}
/**
* Returns the extension of this file (not including the dot), or an empty string if it doesn't have one.
*/
public val File.extension: String
get() = name.substringAfterLast('.', "")
/**
* Returns [path][File.path] of this File using the invariant separator '/' to
* separate the names in the name sequence.
*/
public val File.invariantSeparatorsPath: String
get() = if (File.separatorChar != '/') path.replace(File.separatorChar, '/') else path
/**
* Returns file's name without an extension.
*/
public val File.nameWithoutExtension: String
get() = name.substringBeforeLast(".")
/**
* Calculates the relative path for this file from [base] file.
* Note that the [base] file is treated as a directory.
* If this file matches the [base] file, then an empty string will be returned.
*
* @return relative path from [base] to this.
*
* @throws IllegalArgumentException if this and base paths have different roots.
*/
public fun File.toRelativeString(base: File): String =
toRelativeStringOrNull(base) ?: throw IllegalArgumentException("this and base files have different roots: $this and $base.")
/**
* Calculates the relative path for this file from [base] file.
* Note that the [base] file is treated as a directory.
* If this file matches the [base] file, then a [File] with empty path will be returned.
*
* @return File with relative path from [base] to this.
*
* @throws IllegalArgumentException if this and base paths have different roots.
*/
public fun File.relativeTo(base: File): File = File(this.toRelativeString(base))
/**
* Calculates the relative path for this file from [base] file.
* Note that the [base] file is treated as a directory.
* If this file matches the [base] file, then a [File] with empty path will be returned.
*
* @return File with relative path from [base] to this, or `this` if this and base paths have different roots.
*/
public fun File.relativeToOrSelf(base: File): File =
toRelativeStringOrNull(base)?.let(::File) ?: this
/**
* Calculates the relative path for this file from [base] file.
* Note that the [base] file is treated as a directory.
* If this file matches the [base] file, then a [File] with empty path will be returned.
*
* @return File with relative path from [base] to this, or `null` if this and base paths have different roots.
*/
public fun File.relativeToOrNull(base: File): File? =
toRelativeStringOrNull(base)?.let(::File)
private fun File.toRelativeStringOrNull(base: File): String? {
// Check roots
val thisComponents = this.toComponents().normalize()
val baseComponents = base.toComponents().normalize()
if (thisComponents.root != baseComponents.root) {
return null
}
val baseCount = baseComponents.size
val thisCount = thisComponents.size
val sameCount = run countSame@{
var i = 0
val maxSameCount = minOf(thisCount, baseCount)
while (i < maxSameCount && thisComponents.segments[i] == baseComponents.segments[i])
i++
return@countSame i
}
// Annihilate differing base components by adding required number of .. parts
val res = StringBuilder()
for (i in baseCount - 1 downTo sameCount) {
if (baseComponents.segments[i].name == "..") {
return null
}
res.append("..")
if (i != sameCount) {
res.append(File.separatorChar)
}
}
// Add remaining this components
if (sameCount < thisCount) {
// If some .. were appended
if (sameCount < baseCount)
res.append(File.separatorChar)
thisComponents.segments.drop(sameCount).joinTo(res, File.separator)
}
return res.toString()
}
/**
* Copies this file to the given [target] file.
*
* If some directories on a way to the [target] are missing, then they will be created.
* If the [target] file already exists, this function will fail unless [overwrite] argument is set to `true`.
*
* When [overwrite] is `true` and [target] is a directory, it is replaced only if it is empty.
*
* If this file is a directory, it is copied without its content, i.e. an empty [target] directory is created.
* If you want to copy directory including its contents, use [copyRecursively].
*
* The operation doesn't preserve copied file attributes such as creation/modification date, permissions, etc.
*
* @param overwrite `true` if destination overwrite is allowed.
* @param bufferSize the buffer size to use when copying.
* @return the [target] file.
* @throws NoSuchFileException if the source file doesn't exist.
* @throws FileAlreadyExistsException if the destination file already exists and [overwrite] argument is set to `false`.
* @throws IOException if any errors occur while copying.
*/
public fun File.copyTo(target: File, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): File {
if (!this.exists()) {
throw NoSuchFileException(file = this, reason = "The source file doesn't exist.")
}
if (target.exists()) {
val stillExists = if (!overwrite) true else !target.delete()
if (stillExists) {
throw FileAlreadyExistsException(
file = this,
other = target,
reason = "The destination file already exists."
)
}
}
if (this.isDirectory) {
if (!target.mkdirs())
throw FileSystemException(file = this, other = target, reason = "Failed to create target directory.")
} else {
target.parentFile?.mkdirs()
this.inputStream().use { input ->
target.outputStream().use { output ->
input.copyTo(output, bufferSize)
}
}
}
return target
}
/**
* Enum that can be used to specify behaviour of the `copyRecursively()` function
* in exceptional conditions.
*/
public enum class OnErrorAction {
/** Skip this file and go to the next. */
SKIP,
/** Terminate the evaluation of the function. */
TERMINATE
}
/** Private exception class, used to terminate recursive copying. */
private class TerminateException(file: File) : FileSystemException(file) {}
/**
* Copies this file with all its children to the specified destination [target] path.
* If some directories on the way to the destination are missing, then they will be created.
*
* If this file path points to a single file, then it will be copied to a file with the path [target].
* If this file path points to a directory, then its children will be copied to a directory with the path [target].
*
* If the [target] already exists, it will be deleted before copying when the [overwrite] parameter permits so.
*
* The operation doesn't preserve copied file attributes such as creation/modification date, permissions, etc.
*
* If any errors occur during the copying, then further actions will depend on the result of the call
* to `onError(File, IOException)` function, that will be called with arguments,
* specifying the file that caused the error and the exception itself.
* By default this function rethrows exceptions.
*
* Exceptions that can be passed to the `onError` function:
*
* - [NoSuchFileException] - if there was an attempt to copy a non-existent file
* - [FileAlreadyExistsException] - if there is a conflict
* - [AccessDeniedException] - if there was an attempt to open a directory that didn't succeed.
* - [IOException] - if some problems occur when copying.
*
* Note that if this function fails, then partial copying may have taken place.
*
* @param overwrite `true` if it is allowed to overwrite existing destination files and directories.
* @return `false` if the copying was terminated, `true` otherwise.
*/
public fun File.copyRecursively(
target: File,
overwrite: Boolean = false,
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean {
if (!exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
OnErrorAction.TERMINATE
}
try {
// We cannot break for loop from inside a lambda, so we have to use an exception here
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
OnErrorAction.TERMINATE)
return false
} else {
val relPath = src.toRelativeString(this)
val dstFile = File(target, relPath)
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
val stillExists = if (!overwrite) true else {
if (dstFile.isDirectory)
!dstFile.deleteRecursively()
else
!dstFile.delete()
}
if (stillExists) {
if (onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists.")) == OnErrorAction.TERMINATE)
return false
continue
}
}
if (src.isDirectory) {
dstFile.mkdirs()
} else {
if (src.copyTo(dstFile, overwrite).length() != src.length()) {
if (onError(src, IOException("Source file wasn't copied completely, length of destination file differs.")) == OnErrorAction.TERMINATE)
return false
}
}
}
}
return true
} catch (e: TerminateException) {
return false
}
}
/**
* Delete this file with all its children.
* Note that if this operation fails then partial deletion may have taken place.
*
* @return `true` if the file or directory is successfully deleted, `false` otherwise.
*/
public fun File.deleteRecursively(): Boolean = walkBottomUp().fold(true, { res, it -> (it.delete() || !it.exists()) && res })
/**
* Determines whether this file belongs to the same root as [other]
* and starts with all components of [other] in the same order.
* So if [other] has N components, first N components of `this` must be the same as in [other].
*
* @return `true` if this path starts with [other] path, `false` otherwise.
*/
public fun File.startsWith(other: File): Boolean {
val components = toComponents()
val otherComponents = other.toComponents()
if (components.root != otherComponents.root)
return false
return if (components.size < otherComponents.size) false
else components.segments.subList(0, otherComponents.size).equals(otherComponents.segments)
}
/**
* Determines whether this file belongs to the same root as [other]
* and starts with all components of [other] in the same order.
* So if [other] has N components, first N components of `this` must be the same as in [other].
*
* @return `true` if this path starts with [other] path, `false` otherwise.
*/
public fun File.startsWith(other: String): Boolean = startsWith(File(other))
/**
* Determines whether this file path ends with the path of [other] file.
*
* If [other] is rooted path it must be equal to this.
* If [other] is relative path then last N components of `this` must be the same as all components in [other],
* where N is the number of components in [other].
*
* @return `true` if this path ends with [other] path, `false` otherwise.
*/
public fun File.endsWith(other: File): Boolean {
val components = toComponents()
val otherComponents = other.toComponents()
if (otherComponents.isRooted)
return this == other
val shift = components.size - otherComponents.size
return if (shift < 0) false
else components.segments.subList(shift, components.size).equals(otherComponents.segments)
}
/**
* Determines whether this file belongs to the same root as [other]
* and ends with all components of [other] in the same order.
* So if [other] has N components, last N components of `this` must be the same as in [other].
* For relative [other], `this` can belong to any root.
*
* @return `true` if this path ends with [other] path, `false` otherwise.
*/
public fun File.endsWith(other: String): Boolean = endsWith(File(other))
/**
* Removes all . and resolves all possible .. in this file name.
* For instance, `File("/foo/./bar/gav/../baaz").normalize()` is `File("/foo/bar/baaz")`.
*
* @return normalized pathname with . and possibly .. removed.
*/
public fun File.normalize(): File =
with(toComponents()) { root.resolve(segments.normalize().joinToString(File.separator)) }
private fun FilePathComponents.normalize(): FilePathComponents =
FilePathComponents(root, segments.normalize())
private fun List<File>.normalize(): List<File> {
val list: MutableList<File> = ArrayList(this.size)
for (file in this) {
when (file.name) {
"." -> {}
".." -> if (!list.isEmpty() && list.last().name != "..") list.removeAt(list.size - 1) else list.add(file)
else -> list.add(file)
}
}
return list
}
/**
* Adds [relative] file to this, considering this as a directory.
* If [relative] has a root, [relative] is returned back.
* For instance, `File("/foo/bar").resolve(File("gav"))` is `File("/foo/bar/gav")`.
* This function is complementary with [relativeTo],
* so `f.resolve(g.relativeTo(f)) == g` should be always `true` except for different roots case.
*
* @return concatenated this and [relative] paths, or just [relative] if it's absolute.
*/
public fun File.resolve(relative: File): File {
if (relative.isRooted)
return relative
val baseName = this.toString()
return if (baseName.isEmpty() || baseName.endsWith(File.separatorChar)) File(baseName + relative) else File(baseName + File.separatorChar + relative)
}
/**
* Adds [relative] name to this, considering this as a directory.
* If [relative] has a root, [relative] is returned back.
* For instance, `File("/foo/bar").resolve("gav")` is `File("/foo/bar/gav")`.
*
* @return concatenated this and [relative] paths, or just [relative] if it's absolute.
*/
public fun File.resolve(relative: String): File = resolve(File(relative))
/**
* Adds [relative] file to this parent directory.
* If [relative] has a root or this has no parent directory, [relative] is returned back.
* For instance, `File("/foo/bar").resolveSibling(File("gav"))` is `File("/foo/gav")`.
*
* @return concatenated this.parent and [relative] paths, or just [relative] if it's absolute or this has no parent.
*/
public fun File.resolveSibling(relative: File): File {
val components = this.toComponents()
val parentSubPath = if (components.size == 0) File("..") else components.subPath(0, components.size - 1)
return components.root.resolve(parentSubPath).resolve(relative)
}
/**
* Adds [relative] name to this parent directory.
* If [relative] has a root or this has no parent directory, [relative] is returned back.
* For instance, `File("/foo/bar").resolveSibling("gav")` is `File("/foo/gav")`.
*
* @return concatenated this.parent and [relative] paths, or just [relative] if it's absolute or this has no parent.
*/
public fun File.resolveSibling(relative: String): File = resolveSibling(File(relative))