• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 15728010215

18 Jun 2025 08:37AM UTC coverage: 67.733% (-0.03%) from 67.766%
15728010215

push

github

web-flow
#1340: proper fix if IDE_ROOT is not sane (#1350)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

3168 of 5080 branches covered (62.36%)

Branch coverage included in aggregate %.

8121 of 11587 relevant lines covered (70.09%)

3.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.03
cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
1
package com.devonfw.tools.ide.io;
2

3
import java.io.BufferedOutputStream;
4
import java.io.FileInputStream;
5
import java.io.FileOutputStream;
6
import java.io.IOException;
7
import java.io.InputStream;
8
import java.io.OutputStream;
9
import java.io.Reader;
10
import java.io.Writer;
11
import java.net.URI;
12
import java.net.http.HttpClient;
13
import java.net.http.HttpClient.Redirect;
14
import java.net.http.HttpRequest;
15
import java.net.http.HttpResponse;
16
import java.nio.file.FileSystem;
17
import java.nio.file.FileSystemException;
18
import java.nio.file.FileSystems;
19
import java.nio.file.Files;
20
import java.nio.file.LinkOption;
21
import java.nio.file.NoSuchFileException;
22
import java.nio.file.Path;
23
import java.nio.file.StandardCopyOption;
24
import java.nio.file.attribute.BasicFileAttributes;
25
import java.nio.file.attribute.FileTime;
26
import java.nio.file.attribute.PosixFilePermission;
27
import java.nio.file.attribute.PosixFilePermissions;
28
import java.security.DigestInputStream;
29
import java.security.MessageDigest;
30
import java.security.NoSuchAlgorithmException;
31
import java.time.Duration;
32
import java.time.LocalDateTime;
33
import java.util.ArrayList;
34
import java.util.HashSet;
35
import java.util.Iterator;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Properties;
39
import java.util.Set;
40
import java.util.function.Consumer;
41
import java.util.function.Function;
42
import java.util.function.Predicate;
43
import java.util.stream.Stream;
44

45
import org.apache.commons.compress.archivers.ArchiveEntry;
46
import org.apache.commons.compress.archivers.ArchiveInputStream;
47
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
48
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
49

50
import com.devonfw.tools.ide.cli.CliException;
51
import com.devonfw.tools.ide.cli.CliOfflineException;
52
import com.devonfw.tools.ide.context.IdeContext;
53
import com.devonfw.tools.ide.os.SystemInfoImpl;
54
import com.devonfw.tools.ide.process.ProcessContext;
55
import com.devonfw.tools.ide.util.DateTimeUtil;
56
import com.devonfw.tools.ide.util.FilenameUtil;
57
import com.devonfw.tools.ide.util.HexUtil;
58

59
/**
60
 * Implementation of {@link FileAccess}.
61
 */
62
public class FileAccessImpl implements FileAccess {
63

64
  private static final String WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/windows-file-lock.adoc";
65

66
  private static final String WINDOWS_FILE_LOCK_WARNING =
67
      "On Windows, file operations could fail due to file locks. Please ensure the files in the moved directory are not in use. For further details, see: \n"
68
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
69

70
  private static final Map<String, String> FS_ENV = Map.of("encoding", "UTF-8");
5✔
71

72
  private final IdeContext context;
73

74
  /**
75
   * The constructor.
76
   *
77
   * @param context the {@link IdeContext} to use.
78
   */
79
  public FileAccessImpl(IdeContext context) {
80

81
    super();
2✔
82
    this.context = context;
3✔
83
  }
1✔
84

85
  private HttpClient createHttpClient(String url) {
86

87
    HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS);
4✔
88
    return builder.build();
3✔
89
  }
90

91
  @Override
92
  public void download(String url, Path target) {
93

94
    this.context.info("Trying to download {} from {}", target.getFileName(), url);
15✔
95
    mkdirs(target.getParent());
4✔
96
    try {
97
      if (this.context.isOffline()) {
4!
98
        throw CliOfflineException.ofDownloadViaUrl(url);
×
99
      }
100
      if (url.startsWith("http")) {
4✔
101

102
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
7✔
103
        HttpClient client = createHttpClient(url);
4✔
104
        HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
5✔
105
        int statusCode = response.statusCode();
3✔
106
        if (statusCode == 200) {
3!
107
          downloadFileWithProgressBar(url, target, response);
6✔
108
        } else {
109
          throw new IllegalStateException("Download failed with status code " + statusCode);
×
110
        }
111
      } else if (url.startsWith("ftp") || url.startsWith("sftp")) {
9!
112
        throw new IllegalArgumentException("Unsupported download URL: " + url);
×
113
      } else {
114
        Path source = Path.of(url);
5✔
115
        if (isFile(source)) {
4!
116
          // network drive
117

118
          copyFileWithProgressBar(source, target);
5✔
119
        } else {
120
          throw new IllegalArgumentException("Download path does not point to a downloadable file: " + url);
×
121
        }
122
      }
123
    } catch (Exception e) {
×
124
      throw new IllegalStateException("Failed to download file from URL " + url + " to " + target, e);
×
125
    }
1✔
126
  }
1✔
127

128
  /**
129
   * Downloads a file while showing a {@link IdeProgressBar}.
130
   *
131
   * @param url the url to download.
132
   * @param target Path of the target directory.
133
   * @param response the {@link HttpResponse} to use.
134
   */
135
  private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) {
136

137
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
138
    informAboutMissingContentLength(contentLength, url);
4✔
139

140
    byte[] data = new byte[1024];
3✔
141
    boolean fileComplete = false;
2✔
142
    int count;
143

144
    try (InputStream body = response.body();
4✔
145
        FileOutputStream fileOutput = new FileOutputStream(target.toFile());
6✔
146
        BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutput, data.length);
7✔
147
        IdeProgressBar pb = this.context.newProgressBarForDownload(contentLength)) {
5✔
148
      while (!fileComplete) {
2✔
149
        count = body.read(data);
4✔
150
        if (count <= 0) {
2✔
151
          fileComplete = true;
3✔
152
        } else {
153
          bufferedOut.write(data, 0, count);
5✔
154
          pb.stepBy(count);
5✔
155
        }
156
      }
157

158
    } catch (Exception e) {
×
159
      throw new RuntimeException(e);
×
160
    }
1✔
161
  }
1✔
162

163
  /**
164
   * Copies a file while displaying a progress bar.
165
   *
166
   * @param source Path of file to copy.
167
   * @param target Path of target directory.
168
   */
169
  private void copyFileWithProgressBar(Path source, Path target) throws IOException {
170

171
    try (InputStream in = new FileInputStream(source.toFile()); OutputStream out = new FileOutputStream(target.toFile())) {
12✔
172
      long size = getFileSize(source);
4✔
173
      byte[] buf = new byte[1024];
3✔
174
      try (IdeProgressBar pb = this.context.newProgressbarForCopying(size)) {
5✔
175
        int readBytes;
176
        while ((readBytes = in.read(buf)) > 0) {
6✔
177
          out.write(buf, 0, readBytes);
5✔
178
          if (size > 0) {
4!
179
            pb.stepBy(readBytes);
5✔
180
          }
181
        }
182
      } catch (Exception e) {
×
183
        throw new RuntimeException(e);
×
184
      }
1✔
185
    }
186
  }
1✔
187

188
  private void informAboutMissingContentLength(long contentLength, String url) {
189

190
    if (contentLength < 0) {
4✔
191
      this.context.warning("Content-Length was not provided by download from {}", url);
10✔
192
    }
193
  }
1✔
194

195
  @Override
196
  public void mkdirs(Path directory) {
197

198
    if (Files.isDirectory(directory)) {
5✔
199
      return;
1✔
200
    }
201
    this.context.trace("Creating directory {}", directory);
10✔
202
    try {
203
      Files.createDirectories(directory);
5✔
204
    } catch (IOException e) {
×
205
      throw new IllegalStateException("Failed to create directory " + directory, e);
×
206
    }
1✔
207
  }
1✔
208

209
  @Override
210
  public boolean isFile(Path file) {
211

212
    if (!Files.exists(file)) {
5!
213
      this.context.trace("File {} does not exist", file);
×
214
      return false;
×
215
    }
216
    if (Files.isDirectory(file)) {
5!
217
      this.context.trace("Path {} is a directory but a regular file was expected", file);
×
218
      return false;
×
219
    }
220
    return true;
2✔
221
  }
222

223
  @Override
224
  public boolean isExpectedFolder(Path folder) {
225

226
    if (Files.isDirectory(folder)) {
5✔
227
      return true;
2✔
228
    }
229
    this.context.warning("Expected folder was not found at {}", folder);
10✔
230
    return false;
2✔
231
  }
232

233
  @Override
234
  public String checksum(Path file, String hashAlgorithm) {
235

236
    MessageDigest md;
237
    try {
238
      md = MessageDigest.getInstance(hashAlgorithm);
×
239
    } catch (NoSuchAlgorithmException e) {
×
240
      throw new IllegalStateException("No such hash algorithm " + hashAlgorithm, e);
×
241
    }
×
242
    byte[] buffer = new byte[1024];
×
243
    try (InputStream is = Files.newInputStream(file); DigestInputStream dis = new DigestInputStream(is, md)) {
×
244
      int read = 0;
×
245
      while (read >= 0) {
×
246
        read = dis.read(buffer);
×
247
      }
248
    } catch (Exception e) {
×
249
      throw new IllegalStateException("Failed to read and hash file " + file, e);
×
250
    }
×
251
    byte[] digestBytes = md.digest();
×
252
    return HexUtil.toHexString(digestBytes);
×
253
  }
254

255
  public boolean isJunction(Path path) {
256

257
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
258
      return false;
2✔
259
    }
260

261
    try {
262
      BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
×
263
      return attr.isOther() && attr.isDirectory();
×
264
    } catch (NoSuchFileException e) {
×
265
      return false; // file doesn't exist
×
266
    } catch (IOException e) {
×
267
      // errors in reading the attributes of the file
268
      throw new IllegalStateException("An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
×
269
    }
270
  }
271

272
  @Override
273
  public Path backup(Path fileOrFolder) {
274

275
    if ((fileOrFolder != null) && (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder))) {
9!
276
      delete(fileOrFolder);
4✔
277
    } else if ((fileOrFolder != null) && Files.exists(fileOrFolder)) {
7!
278
      LocalDateTime now = LocalDateTime.now();
2✔
279
      String date = DateTimeUtil.formatDate(now, true);
4✔
280
      String time = DateTimeUtil.formatTime(now);
3✔
281
      String filename = fileOrFolder.getFileName().toString();
4✔
282
      Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS).resolve(date).resolve(time + "_" + filename);
12✔
283
      backupPath = appendParentPath(backupPath, fileOrFolder.getParent(), 2);
6✔
284
      mkdirs(backupPath);
3✔
285
      Path target = backupPath.resolve(filename);
4✔
286
      this.context.info("Creating backup by moving {} to {}", fileOrFolder, target);
14✔
287
      move(fileOrFolder, target);
6✔
288
      return target;
2✔
289
    } else {
290
      this.context.trace("Backup of {} skipped as the path does not exist.", fileOrFolder);
10✔
291
    }
292
    return fileOrFolder;
2✔
293
  }
294

295
  private static Path appendParentPath(Path path, Path parent, int max) {
296

297
    if ((parent == null) || (max <= 0)) {
4!
298
      return path;
2✔
299
    }
300
    return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName());
11✔
301
  }
302

303
  @Override
304
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
305

306
    this.context.trace("Moving {} to {}", source, targetDir);
14✔
307
    try {
308
      Files.move(source, targetDir, copyOptions);
5✔
309
    } catch (IOException e) {
×
310
      String fileType = Files.isSymbolicLink(source) ? "symlink" : isJunction(source) ? "junction" : Files.isDirectory(source) ? "directory" : "file";
×
311
      String message = "Failed to move " + fileType + ": " + source + " to " + targetDir + ".";
×
312
      if (this.context.getSystemInfo().isWindows()) {
×
313
        message = message + "\n" + WINDOWS_FILE_LOCK_WARNING;
×
314
      }
315
      throw new IllegalStateException(message, e);
×
316
    }
1✔
317
  }
1✔
318

319
  @Override
320
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
321

322
    if (mode != FileCopyMode.COPY_TREE_CONTENT) {
3✔
323
      // if we want to copy the file or folder "source" to the existing folder "target" in a shell this will copy
324
      // source into that folder so that we as a result have a copy in "target/source".
325
      // With Java NIO the raw copy method will fail as we cannot copy "source" to the path of the "target" folder.
326
      // For folders we want the same behavior as the linux "cp -r" command so that the "source" folder is copied
327
      // and not only its content what also makes it consistent with the move method that also behaves this way.
328
      // Therefore we need to add the filename (foldername) of "source" to the "target" path before.
329
      // For the rare cases, where we want to copy the content of a folder (cp -r source/* target) we support
330
      // it via the COPY_TREE_CONTENT mode.
331
      Path fileName = source.getFileName();
3✔
332
      if (fileName != null) { // if filename is null, we are copying the root of a (virtual filesystem)
2✔
333
        target = target.resolve(fileName.toString());
5✔
334
      }
335
    }
336
    boolean fileOnly = mode.isFileOnly();
3✔
337
    String operation = mode.getOperation();
3✔
338
    if (mode.isExtract()) {
3✔
339
      this.context.debug("Starting to {} to {}", operation, target);
15✔
340
    } else {
341
      if (fileOnly) {
2✔
342
        this.context.debug("Starting to {} file {} to {}", operation, source, target);
19✔
343
      } else {
344
        this.context.debug("Starting to {} {} recursively to {}", operation, source, target);
18✔
345
      }
346
    }
347
    if (fileOnly && Files.isDirectory(source)) {
7!
348
      throw new IllegalStateException("Expected file but found a directory to copy at " + source);
×
349
    }
350
    if (mode.isFailIfExists()) {
3✔
351
      if (Files.exists(target)) {
5!
352
        throw new IllegalStateException("Failed to " + operation + " " + source + " to already existing target " + target);
×
353
      }
354
    } else if (mode == FileCopyMode.COPY_TREE_OVERRIDE_TREE) {
3✔
355
      delete(target);
3✔
356
    }
357
    try {
358
      copyRecursive(source, target, mode, listener);
6✔
359
    } catch (IOException e) {
×
360
      throw new IllegalStateException("Failed to " + operation + " " + source + " to " + target, e);
×
361
    }
1✔
362
  }
1✔
363

364
  private void copyRecursive(Path source, Path target, FileCopyMode mode, PathCopyListener listener) throws IOException {
365

366
    if (Files.isDirectory(source)) {
5✔
367
      mkdirs(target);
3✔
368
      try (Stream<Path> childStream = Files.list(source)) {
3✔
369
        Iterator<Path> iterator = childStream.iterator();
3✔
370
        while (iterator.hasNext()) {
3✔
371
          Path child = iterator.next();
4✔
372
          copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener);
10✔
373
        }
1✔
374
      }
375
      listener.onCopy(source, target, true);
6✔
376
    } else if (Files.exists(source)) {
5!
377
      if (mode.isOverrideFile()) {
3✔
378
        delete(target);
3✔
379
      }
380
      this.context.trace("Starting to {} {} to {}", mode.getOperation(), source, target);
19✔
381
      Files.copy(source, target);
6✔
382
      listener.onCopy(source, target, false);
6✔
383
    } else {
384
      throw new IOException("Path " + source + " does not exist.");
×
385
    }
386
  }
1✔
387

388
  /**
389
   * Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an {@link IllegalStateException} if there is a file at the given
390
   * {@link Path} that is neither a symbolic link nor a Windows junction.
391
   *
392
   * @param path the {@link Path} to delete.
393
   * @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
394
   */
395
  private void deleteLinkIfExists(Path path) throws IOException {
396

397
    boolean isJunction = isJunction(path); // since broken junctions are not detected by Files.exists()
4✔
398
    boolean isSymlink = Files.exists(path) && Files.isSymbolicLink(path);
12!
399

400
    assert !(isSymlink && isJunction);
5!
401

402
    if (isJunction || isSymlink) {
4!
403
      this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
9!
404
      Files.delete(path);
2✔
405
    }
406
  }
1✔
407

408
  /**
409
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
410
   * is applied to {@code source}.
411
   *
412
   * @param source the {@link Path} to adapt.
413
   * @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is set to {@code true}.
414
   * @param relative the {@code relative} flag.
415
   * @return the adapted {@link Path}.
416
   * @see FileAccessImpl#symlink(Path, Path, boolean)
417
   */
418
  private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {
419

420
    if (source.isAbsolute()) {
3✔
421
      try {
422
        source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
423
      } catch (IOException e) {
×
424
        throw new IOException("Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
425
      }
1✔
426
      if (relative) {
2✔
427
        source = targetLink.getParent().relativize(source);
5✔
428
        // to make relative links like this work: dir/link -> dir
429
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
430
      }
431
    } else { // source is relative
432
      if (relative) {
2✔
433
        // even though the source is already relative, toRealPath should be called to transform paths like
434
        // this ../d1/../d2 to ../d2
435
        source = targetLink.getParent().relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
14✔
436
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
437
      } else { // !relative
438
        try {
439
          source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
11✔
440
        } catch (IOException e) {
×
441
          throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
442
        }
1✔
443
      }
444
    }
445
    return source;
2✔
446
  }
447

448
  /**
449
   * Creates a Windows junction at {@code targetLink} pointing to {@code source}.
450
   *
451
   * @param source must be another Windows junction or a directory.
452
   * @param targetLink the location of the Windows junction.
453
   */
454
  private void createWindowsJunction(Path source, Path targetLink) {
455

456
    this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
×
457
    Path fallbackPath;
458
    if (!source.isAbsolute()) {
×
459
      this.context.warning("You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
×
460
          + "alternative, however, these can not point to relative paths. So the source (" + source + ") is interpreted as an absolute path.");
461
      try {
462
        fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
×
463
      } catch (IOException e) {
×
464
        throw new IllegalStateException(
×
465
            "Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " + "source (" + source
466
                + ") to an absolute path failed.", e);
467
      }
×
468

469
    } else {
470
      fallbackPath = source;
×
471
    }
472
    if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well.
×
473
      throw new IllegalStateException(
×
474
          "These junctions can only point to directories or other junctions. Please make sure that the source (" + fallbackPath + ") is one of these.");
475
    }
476
    this.context.newProcess().executable("cmd").addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
×
477
  }
×
478

479
  @Override
480
  public void symlink(Path source, Path targetLink, boolean relative) {
481

482
    Path adaptedSource = null;
2✔
483
    try {
484
      adaptedSource = adaptPath(source, targetLink, relative);
6✔
485
    } catch (IOException e) {
×
486
      throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink + ") and relative (" + relative + ")", e);
×
487
    }
1✔
488
    this.context.debug("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", targetLink, adaptedSource);
23✔
489

490
    try {
491
      deleteLinkIfExists(targetLink);
3✔
492
    } catch (IOException e) {
×
493
      throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
×
494
    }
1✔
495

496
    try {
497
      Files.createSymbolicLink(targetLink, adaptedSource);
6✔
498
    } catch (FileSystemException e) {
×
499
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
500
        this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
×
501
            + "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlink.adoc for " + "further details. Error was: "
502
            + e.getMessage());
×
503
        createWindowsJunction(adaptedSource, targetLink);
×
504
      } else {
505
        throw new RuntimeException(e);
×
506
      }
507
    } catch (IOException e) {
×
508
      throw new IllegalStateException(
×
509
          "Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + "symbolic link " + targetLink + " pointing to " + source, e);
×
510
    }
1✔
511
  }
1✔
512

513
  @Override
514
  public Path toRealPath(Path path) {
515

516
    return toRealPath(path, true);
5✔
517
  }
518

519
  @Override
520
  public Path toCanonicalPath(Path path) {
521

522
    return toRealPath(path, false);
5✔
523
  }
524

525
  private Path toRealPath(Path path, boolean resolveLinks) {
526

527
    try {
528
      Path realPath;
529
      if (resolveLinks) {
2✔
530
        realPath = path.toRealPath();
6✔
531
      } else {
532
        realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
9✔
533
      }
534
      if (!realPath.equals(path)) {
4✔
535
        this.context.trace("Resolved path {} to {}", path, realPath);
14✔
536
      }
537
      return realPath;
2✔
538
    } catch (IOException e) {
×
539
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
540
    }
541
  }
542

543
  @Override
544
  public Path createTempDir(String name) {
545

546
    try {
547
      Path tmp = this.context.getTempPath();
4✔
548
      Path tempDir = tmp.resolve(name);
4✔
549
      int tries = 1;
2✔
550
      while (Files.exists(tempDir)) {
5!
551
        long id = System.nanoTime() & 0xFFFF;
×
552
        tempDir = tmp.resolve(name + "-" + id);
×
553
        tries++;
×
554
        if (tries > 200) {
×
555
          throw new IOException("Unable to create unique name!");
×
556
        }
557
      }
×
558
      return Files.createDirectory(tempDir);
5✔
559
    } catch (IOException e) {
×
560
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
561
    }
562
  }
563

564
  @Override
565
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
566

567
    if (Files.isDirectory(archiveFile)) {
5✔
568
      // TODO: check this case
569
      Path properInstallDir = archiveFile; // getProperInstallationSubDirOf(archiveFile, archiveFile);
2✔
570
      this.context.warning("Found directory for download at {} hence copying without extraction!", archiveFile);
10✔
571
      copy(properInstallDir, targetDir, FileCopyMode.COPY_TREE_CONTENT);
5✔
572
      postExtractHook(postExtractHook, targetDir);
4✔
573
      return;
1✔
574
    } else if (!extract) {
2✔
575
      mkdirs(targetDir);
3✔
576
      move(archiveFile, targetDir.resolve(archiveFile.getFileName()));
9✔
577
      return;
1✔
578
    }
579
    Path tmpDir = createTempDir("extract-" + archiveFile.getFileName());
7✔
580
    this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", archiveFile, tmpDir, targetDir);
18✔
581
    String filename = archiveFile.getFileName().toString();
4✔
582
    TarCompression tarCompression = TarCompression.of(filename);
3✔
583
    if (tarCompression != null) {
2✔
584
      extractTar(archiveFile, tmpDir, tarCompression);
6✔
585
    } else {
586
      String extension = FilenameUtil.getExtension(filename);
3✔
587
      if (extension == null) {
2!
588
        throw new IllegalStateException("Unknown archive format without extension - can not extract " + archiveFile);
×
589
      } else {
590
        this.context.trace("Determined file extension {}", extension);
10✔
591
      }
592
      switch (extension) {
8!
593
        case "zip" -> extractZip(archiveFile, tmpDir);
×
594
        case "jar" -> extractJar(archiveFile, tmpDir);
5✔
595
        case "dmg" -> extractDmg(archiveFile, tmpDir);
×
596
        case "msi" -> extractMsi(archiveFile, tmpDir);
×
597
        case "pkg" -> extractPkg(archiveFile, tmpDir);
×
598
        default -> throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
×
599
      }
600
    }
601
    Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
5✔
602
    postExtractHook(postExtractHook, properInstallDir);
4✔
603
    move(properInstallDir, targetDir);
6✔
604
    delete(tmpDir);
3✔
605
  }
1✔
606

607
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
608

609
    if (postExtractHook != null) {
2✔
610
      postExtractHook.accept(properInstallDir);
3✔
611
    }
612
  }
1✔
613

614
  /**
615
   * @param path the {@link Path} to start the recursive search from.
616
   * @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed path (including {@code s}) are the sole
617
   *     item in their respective directory and {@code s} is not named "bin".
618
   */
619
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
620

621
    try (Stream<Path> stream = Files.list(path)) {
3✔
622
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
623
      if (subFiles.length == 0) {
3!
624
        throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
×
625
      } else if (subFiles.length == 1) {
4✔
626
        String filename = subFiles[0].getFileName().toString();
6✔
627
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
19!
628
            subFiles[0])) {
629
          return getProperInstallationSubDirOf(subFiles[0], archiveFile);
9✔
630
        }
631
      }
632
      return path;
4✔
633
    } catch (IOException e) {
4!
634
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
635
    }
636
  }
637

638
  @Override
639
  public void extractZip(Path file, Path targetDir) {
640

641
    this.context.info("Extracting ZIP file {} to {}", file, targetDir);
14✔
642
    URI uri = URI.create("jar:" + file.toUri());
6✔
643
    try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
4✔
644
      long size = 0;
2✔
645
      for (Path root : fs.getRootDirectories()) {
11✔
646
        size += getFileSizeRecursive(root);
6✔
647
      }
1✔
648
      try (final IdeProgressBar progressBar = this.context.newProgressbarForExtracting(size)) {
5✔
649
        for (Path root : fs.getRootDirectories()) {
11✔
650
          copy(root, targetDir, FileCopyMode.EXTRACT, (s, t, d) -> onFileCopiedFromZip(s, t, d, progressBar));
15✔
651
        }
1✔
652
      }
653
    } catch (IOException e) {
×
654
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
655
    }
1✔
656
  }
1✔
657

658
  @SuppressWarnings("unchecked")
659
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
660

661
    if (directory) {
2✔
662
      return;
1✔
663
    }
664
    if (!context.getSystemInfo().isWindows()) {
5✔
665
      try {
666
        Object attribute = Files.getAttribute(source, "zip:permissions");
6✔
667
        if (attribute instanceof Set<?> permissionSet) {
6✔
668
          Files.setPosixFilePermissions(target, (Set<PosixFilePermission>) permissionSet);
4✔
669
        }
670
      } catch (Exception e) {
×
671
        context.error(e, "Failed to transfer zip permissions for {}", target);
×
672
      }
1✔
673
    }
674
    progressBar.stepBy(getFileSize(target));
5✔
675
  }
1✔
676

677
  @Override
678
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
679

680
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
681
  }
1✔
682

683
  @Override
684
  public void extractJar(Path file, Path targetDir) {
685

686
    extractZip(file, targetDir);
4✔
687
  }
1✔
688

689
  /**
690
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
691
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
692
   */
693
  public static String generatePermissionString(int permissions) {
694

695
    // Ensure that only the last 9 bits are considered
696
    permissions &= 0b111111111;
4✔
697

698
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
699
    for (int i = 0; i < 9; i++) {
7✔
700
      int mask = 1 << i;
4✔
701
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
702
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
703
    }
704

705
    return permissionStringBuilder.toString();
3✔
706
  }
707

708
  private void extractArchive(Path file, Path targetDir, Function<InputStream, ArchiveInputStream<?>> unpacker) {
709

710
    this.context.info("Extracting TAR file {} to {}", file, targetDir);
14✔
711
    try (InputStream is = Files.newInputStream(file);
5✔
712
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
713
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
714

715
      ArchiveEntry entry = ais.getNextEntry();
3✔
716
      boolean isTar = ais instanceof TarArchiveInputStream;
3✔
717
      while (entry != null) {
2✔
718
        String permissionStr = null;
2✔
719
        if (isTar) {
2!
720
          int tarMode = ((TarArchiveEntry) entry).getMode();
4✔
721
          permissionStr = generatePermissionString(tarMode);
3✔
722
        }
723
        Path entryName = Path.of(entry.getName());
6✔
724
        Path entryPath = targetDir.resolve(entryName).toAbsolutePath();
5✔
725
        if (!entryPath.startsWith(targetDir)) {
4!
726
          throw new IOException("Preventing path traversal attack from " + entryName + " to " + entryPath);
×
727
        }
728
        if (entry.isDirectory()) {
3✔
729
          mkdirs(entryPath);
4✔
730
        } else {
731
          // ensure the file can also be created if directory entry was missing or out of order...
732
          mkdirs(entryPath.getParent());
4✔
733
          Files.copy(ais, entryPath);
6✔
734
        }
735
        if (isTar && !this.context.getSystemInfo().isWindows()) {
7!
736
          Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(permissionStr);
3✔
737
          Files.setPosixFilePermissions(entryPath, permissions);
4✔
738
        }
739
        pb.stepBy(entry.getSize());
4✔
740
        entry = ais.getNextEntry();
3✔
741
      }
1✔
742
    } catch (IOException e) {
×
743
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
744
    }
1✔
745
  }
1✔
746

747
  @Override
748
  public void extractDmg(Path file, Path targetDir) {
749

750
    this.context.info("Extracting DMG file {} to {}", file, targetDir);
×
751
    assert this.context.getSystemInfo().isMac();
×
752

753
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
754
    mkdirs(mountPath);
×
755
    ProcessContext pc = this.context.newProcess();
×
756
    pc.executable("hdiutil");
×
757
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
758
    pc.run();
×
759
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
760
    if (appPath == null) {
×
761
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
762
    }
763

764
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
765
    pc.addArgs("detach", "-force", mountPath);
×
766
    pc.run();
×
767
  }
×
768

769
  @Override
770
  public void extractMsi(Path file, Path targetDir) {
771

772
    this.context.info("Extracting MSI file {} to {}", file, targetDir);
×
773
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
774
    // msiexec also creates a copy of the MSI
775
    Path msiCopy = targetDir.resolve(file.getFileName());
×
776
    delete(msiCopy);
×
777
  }
×
778

779
  @Override
780
  public void extractPkg(Path file, Path targetDir) {
781

782
    this.context.info("Extracting PKG file {} to {}", file, targetDir);
×
783
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
784
    ProcessContext pc = this.context.newProcess();
×
785
    // we might also be able to use cpio from commons-compression instead of external xar...
786
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
787
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
788
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
789
    delete(tmpDirPkg);
×
790
  }
×
791

792
  @Override
793
  public void delete(Path path) {
794

795
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
796
      this.context.trace("Deleting {} skipped as the path does not exist.", path);
10✔
797
      return;
1✔
798
    }
799
    this.context.debug("Deleting {} ...", path);
10✔
800
    try {
801
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
802
        Files.delete(path);
3✔
803
      } else {
804
        deleteRecursive(path);
3✔
805
      }
806
    } catch (IOException e) {
×
807
      throw new IllegalStateException("Failed to delete " + path, e);
×
808
    }
1✔
809
  }
1✔
810

811
  private void deleteRecursive(Path path) throws IOException {
812

813
    if (Files.isDirectory(path)) {
5✔
814
      try (Stream<Path> childStream = Files.list(path)) {
3✔
815
        Iterator<Path> iterator = childStream.iterator();
3✔
816
        while (iterator.hasNext()) {
3✔
817
          Path child = iterator.next();
4✔
818
          deleteRecursive(child);
3✔
819
        }
1✔
820
      }
821
    }
822
    this.context.trace("Deleting {} ...", path);
10✔
823
    Files.delete(path);
2✔
824
  }
1✔
825

826
  @Override
827
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
828

829
    try {
830
      if (!Files.isDirectory(dir)) {
5✔
831
        return null;
2✔
832
      }
833
      return findFirstRecursive(dir, filter, recursive);
6✔
834
    } catch (IOException e) {
×
835
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
836
    }
837
  }
838

839
  private Path findFirstRecursive(Path dir, Predicate<Path> filter, boolean recursive) throws IOException {
840

841
    List<Path> folders = null;
2✔
842
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
843
      Iterator<Path> iterator = childStream.iterator();
3✔
844
      while (iterator.hasNext()) {
3✔
845
        Path child = iterator.next();
4✔
846
        if (filter.test(child)) {
4✔
847
          return child;
4✔
848
        } else if (recursive && Files.isDirectory(child)) {
2!
849
          if (folders == null) {
×
850
            folders = new ArrayList<>();
×
851
          }
852
          folders.add(child);
×
853
        }
854
      }
1✔
855
    }
4!
856
    if (folders != null) {
2!
857
      for (Path child : folders) {
×
858
        Path match = findFirstRecursive(child, filter, recursive);
×
859
        if (match != null) {
×
860
          return match;
×
861
        }
862
      }
×
863
    }
864
    return null;
2✔
865
  }
866

867
  @Override
868
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
869

870
    if (!Files.isDirectory(dir)) {
5✔
871
      return List.of();
2✔
872
    }
873
    List<Path> children = new ArrayList<>();
4✔
874
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
875
      Iterator<Path> iterator = childStream.iterator();
3✔
876
      while (iterator.hasNext()) {
3✔
877
        Path child = iterator.next();
4✔
878
        Path filteredChild = filter.apply(child);
5✔
879
        if (filteredChild != null) {
2✔
880
          if (filteredChild == child) {
3!
881
            this.context.trace("Accepted file {}", child);
11✔
882
          } else {
883
            this.context.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
884
          }
885
          children.add(filteredChild);
5✔
886
        } else {
887
          this.context.trace("Ignoring file {} according to filter", child);
10✔
888
        }
889
      }
1✔
890
    } catch (IOException e) {
×
891
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
892
    }
1✔
893
    return children;
2✔
894
  }
895

896
  @Override
897
  public boolean isEmptyDir(Path dir) {
898

899
    return listChildren(dir, f -> true).isEmpty();
8✔
900
  }
901

902
  private long getFileSize(Path file) {
903

904
    try {
905
      return Files.size(file);
3✔
906
    } catch (IOException e) {
×
907
      this.context.warning(e.getMessage(), e);
×
908
      return 0;
×
909
    }
910
  }
911

912
  private long getFileSizeRecursive(Path path) {
913

914
    long size = 0;
2✔
915
    if (Files.isDirectory(path)) {
5✔
916
      try (Stream<Path> childStream = Files.list(path)) {
3✔
917
        Iterator<Path> iterator = childStream.iterator();
3✔
918
        while (iterator.hasNext()) {
3✔
919
          Path child = iterator.next();
4✔
920
          size += getFileSizeRecursive(child);
6✔
921
        }
1✔
922
      } catch (IOException e) {
×
923
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
924
      }
1✔
925
    } else {
926
      size += getFileSize(path);
6✔
927
    }
928
    return size;
2✔
929
  }
930

931
  @Override
932
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
933

934
    for (Path dir : searchDirs) {
10!
935
      Path filePath = dir.resolve(fileName);
4✔
936
      try {
937
        if (Files.exists(filePath)) {
5✔
938
          return filePath;
2✔
939
        }
940
      } catch (Exception e) {
×
941
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
942
      }
1✔
943
    }
1✔
944
    return null;
×
945
  }
946

947
  @Override
948
  public void makeExecutable(Path file, boolean confirm) {
949

950
    if (Files.exists(file)) {
5✔
951
      if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
952
        this.context.trace("Windows does not have executable flags hence omitting for file {}", file);
×
953
        return;
×
954
      }
955
      try {
956
        // Read the current file permissions
957
        Set<PosixFilePermission> existingPermissions = Files.getPosixFilePermissions(file);
5✔
958

959
        // Add execute permission for all users
960
        Set<PosixFilePermission> executablePermissions = new HashSet<>(existingPermissions);
5✔
961
        boolean update = false;
2✔
962
        update |= executablePermissions.add(PosixFilePermission.OWNER_EXECUTE);
6✔
963
        update |= executablePermissions.add(PosixFilePermission.GROUP_EXECUTE);
6✔
964
        update |= executablePermissions.add(PosixFilePermission.OTHERS_EXECUTE);
6✔
965

966
        if (update) {
2✔
967
          if (confirm) {
2!
968
            boolean yesContinue = this.context.question(
×
969
                "We want to execute " + file.getFileName() + " but this command seems to lack executable permissions!\n"
×
970
                    + "Most probably the tool vendor did forgot to add x-flags in the binary release package.\n"
971
                    + "Before running the command, we suggest to set executable permissions to the file:\n"
972
                    + file + "\n"
973
                    + "For security reasons we ask for your confirmation so please check this request.\n"
974
                    + "Changing permissions from " + PosixFilePermissions.toString(existingPermissions) + " to " + PosixFilePermissions.toString(
×
975
                    executablePermissions) + ".\n"
976
                    + "Do you confirm to make the command executable before running it?");
977
            if (!yesContinue) {
×
978
              return;
×
979
            }
980
          }
981
          this.context.debug("Setting executable flags for file {}", file);
10✔
982
          // Set the new permissions
983
          Files.setPosixFilePermissions(file, executablePermissions);
5✔
984
        } else {
985
          this.context.trace("Executable flags already present so no need to set them for file {}", file);
10✔
986
        }
987
      } catch (IOException e) {
×
988
        throw new RuntimeException(e);
×
989
      }
1✔
990
    } else {
991
      this.context.warning("Cannot set executable flag on file that does not exist: {}", file);
10✔
992
    }
993
  }
1✔
994

995
  @Override
996
  public void touch(Path file) {
997

998
    if (Files.exists(file)) {
5✔
999
      try {
1000
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1001
      } catch (IOException e) {
×
1002
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1003
      }
1✔
1004
    } else {
1005
      try {
1006
        Files.createFile(file);
5✔
1007
      } catch (IOException e) {
1✔
1008
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1009
      }
1✔
1010
    }
1011
  }
1✔
1012

1013
  @Override
1014
  public String readFileContent(Path file) {
1015

1016
    this.context.trace("Reading content of file from {}", file);
10✔
1017
    if (!Files.exists((file))) {
5✔
1018
      this.context.debug("File {} does not exist", file);
10✔
1019
      return null;
2✔
1020
    }
1021
    try {
1022
      String content = Files.readString(file);
3✔
1023
      this.context.trace("Completed reading {} character(s) from file {}", content.length(), file);
16✔
1024
      return content;
2✔
1025
    } catch (IOException e) {
×
1026
      throw new IllegalStateException("Failed to read file " + file, e);
×
1027
    }
1028
  }
1029

1030
  @Override
1031
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1032

1033
    if (createParentDir) {
2✔
1034
      mkdirs(file.getParent());
4✔
1035
    }
1036
    if (content == null) {
2!
1037
      content = "";
×
1038
    }
1039
    this.context.trace("Writing content with {} character(s) to file {}", content.length(), file);
16✔
1040
    if (Files.exists(file)) {
5✔
1041
      this.context.info("Overriding content of file {}", file);
10✔
1042
    }
1043
    try {
1044
      Files.writeString(file, content);
6✔
1045
      this.context.trace("Wrote content to file {}", file);
10✔
1046
    } catch (IOException e) {
×
1047
      throw new RuntimeException("Failed to write file " + file, e);
×
1048
    }
1✔
1049
  }
1✔
1050

1051
  @Override
1052
  public List<String> readFileLines(Path file) {
1053

1054
    this.context.trace("Reading content of file from {}", file);
10✔
1055
    if (!Files.exists(file)) {
5✔
1056
      this.context.warning("File {} does not exist", file);
10✔
1057
      return null;
2✔
1058
    }
1059
    try {
1060
      List<String> content = Files.readAllLines(file);
3✔
1061
      this.context.trace("Completed reading {} lines from file {}", content.size(), file);
16✔
1062
      return content;
2✔
1063
    } catch (IOException e) {
×
1064
      throw new IllegalStateException("Failed to read file " + file, e);
×
1065
    }
1066
  }
1067

1068
  @Override
1069
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1070

1071
    if (createParentDir) {
2!
1072
      mkdirs(file.getParent());
×
1073
    }
1074
    if (content == null) {
2!
1075
      content = List.of();
×
1076
    }
1077
    this.context.trace("Writing content with {} lines to file {}", content.size(), file);
16✔
1078
    if (Files.exists(file)) {
5✔
1079
      this.context.debug("Overriding content of file {}", file);
10✔
1080
    }
1081
    try {
1082
      Files.write(file, content);
6✔
1083
      this.context.trace("Wrote content to file {}", file);
10✔
1084
    } catch (IOException e) {
×
1085
      throw new RuntimeException("Failed to write file " + file, e);
×
1086
    }
1✔
1087
  }
1✔
1088

1089
  @Override
1090
  public void readProperties(Path file, Properties properties) {
1091

1092
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1093
      properties.load(reader);
3✔
1094
      this.context.debug("Successfully loaded {} properties from {}", properties.size(), file);
16✔
1095
    } catch (IOException e) {
×
1096
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1097
    }
1✔
1098
  }
1✔
1099

1100
  @Override
1101
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1102

1103
    if (createParentDir) {
2✔
1104
      mkdirs(file.getParent());
4✔
1105
    }
1106
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1107
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1108
      this.context.debug("Successfully saved {} properties to {}", properties.size(), file);
16✔
1109
    } catch (IOException e) {
×
1110
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1111
    }
1✔
1112
  }
1✔
1113

1114
  @Override
1115
  public Duration getFileAge(Path path) {
1116
    if (Files.exists(path)) {
5✔
1117
      try {
1118
        long currentTime = System.currentTimeMillis();
2✔
1119
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1120
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1121
      } catch (IOException e) {
×
1122
        this.context.warning().log(e, "Could not get modification-time of {}.", path);
×
1123
      }
×
1124
    } else {
1125
      this.context.debug("Path {} is missing - skipping modification-time and file age check.", path);
10✔
1126
    }
1127
    return null;
2✔
1128
  }
1129

1130
  @Override
1131
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1132

1133
    Duration age = getFileAge(path);
4✔
1134
    if (age == null) {
2✔
1135
      return false;
2✔
1136
    }
1137
    context.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
18✔
1138
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1139
  }
1140
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc