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

devonfw / IDEasy / 12073943517

28 Nov 2024 06:24PM UTC coverage: 67.417% (+0.03%) from 67.39%
12073943517

push

github

web-flow
#778: add icd #779: use functions instead of alias #810: setup in current shell #782: fix IDE_ROOT on linux/mac #774: fixed HTTP proxy support (#811)

2500 of 4050 branches covered (61.73%)

Branch coverage included in aggregate %.

6511 of 9316 relevant lines covered (69.89%)

3.09 hits per line

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

63.64
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.net.URI;
10
import java.net.http.HttpClient;
11
import java.net.http.HttpClient.Redirect;
12
import java.net.http.HttpRequest;
13
import java.net.http.HttpResponse;
14
import java.nio.file.FileSystemException;
15
import java.nio.file.Files;
16
import java.nio.file.LinkOption;
17
import java.nio.file.NoSuchFileException;
18
import java.nio.file.Path;
19
import java.nio.file.attribute.BasicFileAttributes;
20
import java.nio.file.attribute.FileTime;
21
import java.nio.file.attribute.PosixFilePermission;
22
import java.nio.file.attribute.PosixFilePermissions;
23
import java.security.DigestInputStream;
24
import java.security.MessageDigest;
25
import java.security.NoSuchAlgorithmException;
26
import java.time.LocalDateTime;
27
import java.util.ArrayList;
28
import java.util.Iterator;
29
import java.util.List;
30
import java.util.Set;
31
import java.util.function.Consumer;
32
import java.util.function.Function;
33
import java.util.function.Predicate;
34
import java.util.jar.JarEntry;
35
import java.util.jar.JarInputStream;
36
import java.util.stream.Stream;
37
import java.util.zip.ZipEntry;
38
import java.util.zip.ZipInputStream;
39

40
import org.apache.commons.compress.archivers.ArchiveEntry;
41
import org.apache.commons.compress.archivers.ArchiveInputStream;
42
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
43
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
44

45
import com.devonfw.tools.ide.cli.CliException;
46
import com.devonfw.tools.ide.cli.CliOfflineException;
47
import com.devonfw.tools.ide.context.IdeContext;
48
import com.devonfw.tools.ide.os.SystemInfoImpl;
49
import com.devonfw.tools.ide.process.ProcessContext;
50
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
51
import com.devonfw.tools.ide.util.DateTimeUtil;
52
import com.devonfw.tools.ide.util.FilenameUtil;
53
import com.devonfw.tools.ide.util.HexUtil;
54

55
/**
56
 * Implementation of {@link FileAccess}.
57
 */
58
public class FileAccessImpl implements FileAccess {
1✔
59

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

62
  private static final String WINDOWS_FILE_LOCK_WARNING =
63
      "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"
64
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
65

66
  private final IdeContext context;
67

68
  /**
69
   * The constructor.
70
   *
71
   * @param context the {@link IdeContext} to use.
72
   */
73
  public FileAccessImpl(IdeContext context) {
74

75
    super();
2✔
76
    this.context = context;
3✔
77
  }
1✔
78

79
  private HttpClient createHttpClient(String url) {
80

81
    HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS);
4✔
82
    return builder.build();
3✔
83
  }
84

85
  @Override
86
  public void download(String url, Path target) {
87

88
    this.context.info("Trying to download {} from {}", target.getFileName(), url);
15✔
89
    mkdirs(target.getParent());
4✔
90
    try {
91
      if (this.context.isOffline()) {
4!
92
        throw CliOfflineException.ofDownloadViaUrl(url);
×
93
      }
94
      if (url.startsWith("http")) {
4✔
95

96
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
7✔
97
        HttpClient client = createHttpClient(url);
4✔
98
        HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
5✔
99
        if (response.statusCode() == 200) {
4!
100
          downloadFileWithProgressBar(url, target, response);
5✔
101
        }
102
      } else if (url.startsWith("ftp") || url.startsWith("sftp")) {
9!
103
        throw new IllegalArgumentException("Unsupported download URL: " + url);
×
104
      } else {
105
        Path source = Path.of(url);
5✔
106
        if (isFile(source)) {
4!
107
          // network drive
108

109
          copyFileWithProgressBar(source, target);
5✔
110
        } else {
111
          throw new IllegalArgumentException("Download path does not point to a downloadable file: " + url);
×
112
        }
113
      }
114
    } catch (Exception e) {
×
115
      throw new IllegalStateException("Failed to download file from URL " + url + " to " + target, e);
×
116
    }
1✔
117
  }
1✔
118

119
  /**
120
   * Downloads a file while showing a {@link IdeProgressBar}.
121
   *
122
   * @param url the url to download.
123
   * @param target Path of the target directory.
124
   * @param response the {@link HttpResponse} to use.
125
   */
126
  private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) {
127

128
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
129
    informAboutMissingContentLength(contentLength, url, null);
5✔
130

131
    byte[] data = new byte[1024];
3✔
132
    boolean fileComplete = false;
2✔
133
    int count;
134

135
    try (InputStream body = response.body();
4✔
136
        FileOutputStream fileOutput = new FileOutputStream(target.toFile());
6✔
137
        BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutput, data.length);
7✔
138
        IdeProgressBar pb = this.context.prepareProgressBar("Downloading", contentLength)) {
6✔
139
      while (!fileComplete) {
2✔
140
        count = body.read(data);
4✔
141
        if (count <= 0) {
2✔
142
          fileComplete = true;
3✔
143
        } else {
144
          bufferedOut.write(data, 0, count);
5✔
145
          pb.stepBy(count);
5✔
146
        }
147
      }
148

149
    } catch (Exception e) {
×
150
      throw new RuntimeException(e);
×
151
    }
1✔
152
  }
1✔
153

154
  /**
155
   * Copies a file while displaying a progress bar.
156
   *
157
   * @param source Path of file to copy.
158
   * @param target Path of target directory.
159
   */
160
  private void copyFileWithProgressBar(Path source, Path target) throws IOException {
161

162
    try (InputStream in = new FileInputStream(source.toFile()); OutputStream out = new FileOutputStream(target.toFile())) {
12✔
163
      long size;
164
      size = getFileSize(source);
4✔
165

166
      informAboutMissingContentLength(size, null, source);
5✔
167
      byte[] buf = new byte[1024];
3✔
168
      int readBytes;
169

170
      try (IdeProgressBar pb = this.context.prepareProgressBar("Copying", size)) {
6✔
171
        while ((readBytes = in.read(buf)) > 0) {
6✔
172
          out.write(buf, 0, readBytes);
5✔
173
          if (size > 0) {
4!
174
            pb.stepBy(readBytes);
5✔
175
          }
176
        }
177
      } catch (Exception e) {
×
178
        throw new RuntimeException(e);
×
179
      }
1✔
180
    }
181
  }
1✔
182

183
  private void informAboutMissingContentLength(long contentLength, String url, Path path) {
184

185
    String source;
186
    if (contentLength < 0) {
4✔
187
      if (path != null) {
2!
188
        source = path.toString();
×
189
      } else {
190
        source = url;
2✔
191
      }
192
      this.context.warning("Content-Length was not provided by download/copy source: {}.",
10✔
193
          source);
194
    }
195
  }
1✔
196

197
  @Override
198
  public void mkdirs(Path directory) {
199

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

211
  @Override
212
  public boolean isFile(Path file) {
213

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

225
  @Override
226
  public boolean isExpectedFolder(Path folder) {
227

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

235
  @Override
236
  public String checksum(Path file) {
237

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

257
  private boolean isJunction(Path path) {
258

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

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

274
  @Override
275
  public void backup(Path fileOrFolder) {
276

277
    if (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder)) {
7!
278
      delete(fileOrFolder);
×
279
    } else {
280
      // fileOrFolder is a directory
281
      Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_BACKUPS);
8✔
282
      LocalDateTime now = LocalDateTime.now();
2✔
283
      String date = DateTimeUtil.formatDate(now);
3✔
284
      String time = DateTimeUtil.formatTime(now);
3✔
285
      Path backupDatePath = backupPath.resolve(date);
4✔
286
      mkdirs(backupDatePath);
3✔
287
      Path target = backupDatePath.resolve(fileOrFolder.getFileName().toString() + "_" + time);
8✔
288
      this.context.info("Creating backup by moving {} to {}", fileOrFolder, target);
14✔
289
      move(fileOrFolder, target);
4✔
290
    }
291
  }
1✔
292

293
  @Override
294
  public void move(Path source, Path targetDir) {
295

296
    this.context.trace("Moving {} to {}", source, targetDir);
14✔
297
    try {
298
      Files.move(source, targetDir);
6✔
299
    } catch (IOException e) {
×
300
      String fileType = Files.isSymbolicLink(source) ? "symlink" : isJunction(source) ? "junction" : Files.isDirectory(source) ? "directory" : "file";
×
301
      String message = "Failed to move " + fileType + ": " + source + " to " + targetDir + ".";
×
302
      if (this.context.getSystemInfo().isWindows()) {
×
303
        message = message + "\n" + WINDOWS_FILE_LOCK_WARNING;
×
304
      }
305
      throw new IllegalStateException(message, e);
×
306
    }
1✔
307
  }
1✔
308

309
  @Override
310
  public void copy(Path source, Path target, FileCopyMode mode) {
311

312
    if (mode != FileCopyMode.COPY_TREE_CONTENT) {
3✔
313
      // if we want to copy the file or folder "source" to the existing folder "target" in a shell this will copy
314
      // source into that folder so that we as a result have a copy in "target/source".
315
      // With Java NIO the raw copy method will fail as we cannot copy "source" to the path of the "target" folder.
316
      // For folders we want the same behavior as the linux "cp -r" command so that the "source" folder is copied
317
      // and not only its content what also makes it consistent with the move method that also behaves this way.
318
      // Therefore we need to add the filename (foldername) of "source" to the "target" path before.
319
      // For the rare cases, where we want to copy the content of a folder (cp -r source/* target) we support
320
      // it via the COPY_TREE_CONTENT mode.
321
      target = target.resolve(source.getFileName());
5✔
322
    }
323
    boolean fileOnly = mode.isFileOnly();
3✔
324
    if (fileOnly) {
2✔
325
      this.context.debug("Copying file {} to {}", source, target);
15✔
326
    } else {
327
      this.context.debug("Copying {} recursively to {}", source, target);
14✔
328
    }
329
    if (fileOnly && Files.isDirectory(source)) {
7!
330
      throw new IllegalStateException("Expected file but found a directory to copy at " + source);
×
331
    }
332
    if (mode.isFailIfExists()) {
3✔
333
      if (Files.exists(target)) {
5!
334
        throw new IllegalStateException("Failed to copy " + source + " to already existing target " + target);
×
335
      }
336
    } else if (mode == FileCopyMode.COPY_TREE_OVERRIDE_TREE) {
3✔
337
      delete(target);
3✔
338
    }
339
    try {
340
      copyRecursive(source, target, mode);
5✔
341
    } catch (IOException e) {
×
342
      throw new IllegalStateException("Failed to copy " + source + " to " + target, e);
×
343
    }
1✔
344
  }
1✔
345

346
  private void copyRecursive(Path source, Path target, FileCopyMode mode) throws IOException {
347

348
    if (Files.isDirectory(source)) {
5✔
349
      mkdirs(target);
3✔
350
      try (Stream<Path> childStream = Files.list(source)) {
4✔
351
        Iterator<Path> iterator = childStream.iterator();
3✔
352
        while (iterator.hasNext()) {
3✔
353
          Path child = iterator.next();
4✔
354
          copyRecursive(child, target.resolve(child.getFileName()), mode);
8✔
355
        }
1✔
356
      }
357
    } else if (Files.exists(source)) {
5!
358
      if (mode.isOverrideFile()) {
3✔
359
        delete(target);
3✔
360
      }
361
      this.context.trace("Copying {} to {}", source, target);
14✔
362
      Files.copy(source, target);
7✔
363
    } else {
364
      throw new IOException("Path " + source + " does not exist.");
×
365
    }
366
  }
1✔
367

368
  /**
369
   * 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
370
   * {@link Path} that is neither a symbolic link nor a Windows junction.
371
   *
372
   * @param path the {@link Path} to delete.
373
   * @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
374
   */
375
  private void deleteLinkIfExists(Path path) throws IOException {
376

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

380
    assert !(isSymlink && isJunction);
5!
381

382
    if (isJunction || isSymlink) {
4!
383
      this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
8!
384
      Files.delete(path);
2✔
385
    }
386
  }
1✔
387

388
  /**
389
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
390
   * is applied to {@code source}.
391
   *
392
   * @param source the {@link Path} to adapt.
393
   * @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is set to {@code true}.
394
   * @param relative the {@code relative} flag.
395
   * @return the adapted {@link Path}.
396
   * @see FileAccessImpl#symlink(Path, Path, boolean)
397
   */
398
  private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {
399

400
    if (source.isAbsolute()) {
3✔
401
      try {
402
        source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
403
      } catch (IOException e) {
×
404
        throw new IOException("Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
405
      }
1✔
406
      if (relative) {
2✔
407
        source = targetLink.getParent().relativize(source);
5✔
408
        // to make relative links like this work: dir/link -> dir
409
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
410
      }
411
    } else { // source is relative
412
      if (relative) {
2✔
413
        // even though the source is already relative, toRealPath should be called to transform paths like
414
        // this ../d1/../d2 to ../d2
415
        source = targetLink.getParent().relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
14✔
416
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
417
      } else { // !relative
418
        try {
419
          source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
11✔
420
        } catch (IOException e) {
×
421
          throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
422
        }
1✔
423
      }
424
    }
425
    return source;
2✔
426
  }
427

428
  /**
429
   * Creates a Windows junction at {@code targetLink} pointing to {@code source}.
430
   *
431
   * @param source must be another Windows junction or a directory.
432
   * @param targetLink the location of the Windows junction.
433
   */
434
  private void createWindowsJunction(Path source, Path targetLink) {
435

436
    this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
×
437
    Path fallbackPath;
438
    if (!source.isAbsolute()) {
×
439
      this.context.warning("You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
×
440
          + "alternative, however, these can not point to relative paths. So the source (" + source + ") is interpreted as an absolute path.");
441
      try {
442
        fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
×
443
      } catch (IOException e) {
×
444
        throw new IllegalStateException(
×
445
            "Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " + "source (" + source
446
                + ") to an absolute path failed.", e);
447
      }
×
448

449
    } else {
450
      fallbackPath = source;
×
451
    }
452
    if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well.
×
453
      throw new IllegalStateException(
×
454
          "These junctions can only point to directories or other junctions. Please make sure that the source (" + fallbackPath + ") is one of these.");
455
    }
456
    this.context.newProcess().executable("cmd").addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
×
457
  }
×
458

459
  @Override
460
  public void symlink(Path source, Path targetLink, boolean relative) {
461

462
    Path adaptedSource = null;
2✔
463
    try {
464
      adaptedSource = adaptPath(source, targetLink, relative);
6✔
465
    } catch (IOException e) {
×
466
      throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink + ") and relative (" + relative + ")", e);
×
467
    }
1✔
468
    this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", targetLink, adaptedSource);
23✔
469

470
    try {
471
      deleteLinkIfExists(targetLink);
3✔
472
    } catch (IOException e) {
×
473
      throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
×
474
    }
1✔
475

476
    try {
477
      Files.createSymbolicLink(targetLink, adaptedSource);
6✔
478
    } catch (FileSystemException e) {
×
479
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
480
        this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
×
481
            + "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for " + "further details. Error was: "
482
            + e.getMessage());
×
483
        createWindowsJunction(adaptedSource, targetLink);
×
484
      } else {
485
        throw new RuntimeException(e);
×
486
      }
487
    } catch (IOException e) {
×
488
      throw new IllegalStateException(
×
489
          "Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + "symbolic link " + targetLink + " pointing to " + source, e);
×
490
    }
1✔
491
  }
1✔
492

493
  @Override
494
  public Path toRealPath(Path path) {
495

496
    try {
497
      Path realPath = path.toRealPath();
×
498
      if (!realPath.equals(path)) {
×
499
        this.context.trace("Resolved path {} to {}", path, realPath);
×
500
      }
501
      return realPath;
×
502
    } catch (IOException e) {
×
503
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
504
    }
505
  }
506

507
  @Override
508
  public Path createTempDir(String name) {
509

510
    try {
511
      Path tmp = this.context.getTempPath();
4✔
512
      Path tempDir = tmp.resolve(name);
4✔
513
      int tries = 1;
2✔
514
      while (Files.exists(tempDir)) {
5!
515
        long id = System.nanoTime() & 0xFFFF;
×
516
        tempDir = tmp.resolve(name + "-" + id);
×
517
        tries++;
×
518
        if (tries > 200) {
×
519
          throw new IOException("Unable to create unique name!");
×
520
        }
521
      }
×
522
      return Files.createDirectory(tempDir);
5✔
523
    } catch (IOException e) {
×
524
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
525
    }
526
  }
527

528
  @Override
529
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
530

531
    if (Files.isDirectory(archiveFile)) {
5✔
532
      // TODO: check this case
533
      Path properInstallDir = archiveFile; // getProperInstallationSubDirOf(archiveFile, archiveFile);
2✔
534
      this.context.warning("Found directory for download at {} hence copying without extraction!", archiveFile);
10✔
535
      copy(properInstallDir, targetDir, FileCopyMode.COPY_TREE_CONTENT);
5✔
536
      postExtractHook(postExtractHook, targetDir);
4✔
537
      return;
1✔
538
    } else if (!extract) {
2!
539
      mkdirs(targetDir);
×
540
      move(archiveFile, targetDir.resolve(archiveFile.getFileName()));
×
541
      return;
×
542
    }
543
    Path tmpDir = createTempDir("extract-" + archiveFile.getFileName());
6✔
544
    this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", archiveFile, tmpDir, targetDir);
18✔
545
    String filename = archiveFile.getFileName().toString();
4✔
546
    TarCompression tarCompression = TarCompression.of(filename);
3✔
547
    if (tarCompression != null) {
2✔
548
      extractTar(archiveFile, tmpDir, tarCompression);
6✔
549
    } else {
550
      String extension = FilenameUtil.getExtension(filename);
3✔
551
      if (extension == null) {
2!
552
        throw new IllegalStateException("Unknown archive format without extension - can not extract " + archiveFile);
×
553
      } else {
554
        this.context.trace("Determined file extension {}", extension);
10✔
555
      }
556
      switch (extension) {
8!
557
        case "zip" -> {
558
          extractZip(archiveFile, tmpDir);
×
559
        }
×
560
        case "jar" -> {
561
          extractJar(archiveFile, tmpDir);
4✔
562
        }
1✔
563
        case "dmg" -> {
564
          extractDmg(archiveFile, tmpDir);
×
565
        }
×
566
        case "msi" -> {
567
          extractMsi(archiveFile, tmpDir);
×
568
        }
×
569
        case "pkg" -> {
570
          extractPkg(archiveFile, tmpDir);
×
571
        }
×
572
        default -> {
573
          throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
×
574
        }
575
      }
576
    }
577
    Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
5✔
578
    postExtractHook(postExtractHook, properInstallDir);
4✔
579
    move(properInstallDir, targetDir);
4✔
580
    delete(tmpDir);
3✔
581
  }
1✔
582

583
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
584

585
    if (postExtractHook != null) {
2✔
586
      postExtractHook.accept(properInstallDir);
3✔
587
    }
588
  }
1✔
589

590
  /**
591
   * @param path the {@link Path} to start the recursive search from.
592
   * @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
593
   *     item in their respective directory and {@code s} is not named "bin".
594
   */
595
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
596

597
    try (Stream<Path> stream = Files.list(path)) {
3✔
598
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
599
      if (subFiles.length == 0) {
3!
600
        throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
×
601
      } else if (subFiles.length == 1) {
4✔
602
        String filename = subFiles[0].getFileName().toString();
6✔
603
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
19!
604
            subFiles[0])) {
605
          return getProperInstallationSubDirOf(subFiles[0], archiveFile);
9✔
606
        }
607
      }
608
      return path;
4✔
609
    } catch (IOException e) {
4!
610
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
611
    }
612
  }
613

614
  @Override
615
  public void extractZip(Path file, Path targetDir) {
616

617
    extractZipArchive(file, targetDir);
4✔
618
  }
1✔
619

620
  @Override
621
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
622

623
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
624
  }
1✔
625

626
  @Override
627
  public void extractJar(Path file, Path targetDir) {
628

629
    this.context.trace("Unpacking JAR {} to {}", file, targetDir);
14✔
630
    try (JarInputStream jis = new JarInputStream(Files.newInputStream(file)); IdeProgressBar pb = getProgressbarForUnpacking(
13✔
631
        getFileSize(file))) {
1✔
632
      JarEntry entry;
633
      while ((entry = jis.getNextJarEntry()) != null) {
5✔
634
        Path entryPath = targetDir.resolve(entry.getName()).toAbsolutePath();
6✔
635

636
        if (!entryPath.startsWith(targetDir)) {
4!
637
          throw new IOException("Preventing path traversal attack from " + entry.getName() + " to " + entryPath);
×
638
        }
639

640
        if (entry.isDirectory()) {
3✔
641
          Files.createDirectories(entryPath);
6✔
642
        } else {
643
          Files.createDirectories(entryPath.getParent());
6✔
644
          Files.copy(jis, entryPath);
6✔
645
        }
646
        pb.stepBy(entry.getCompressedSize());
4✔
647
        jis.closeEntry();
2✔
648
      }
1✔
649
    } catch (IOException e) {
×
650
      throw new IllegalStateException("Failed to extract JAR " + file + " to " + targetDir, e);
×
651
    }
1✔
652
  }
1✔
653

654
  /**
655
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
656
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
657
   */
658
  public static String generatePermissionString(int permissions) {
659

660
    // Ensure that only the last 9 bits are considered
661
    permissions &= 0b111111111;
4✔
662

663
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
664

665
    for (int i = 0; i < 9; i++) {
7✔
666
      int mask = 1 << i;
4✔
667
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
668
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
669
    }
670

671
    return permissionStringBuilder.toString();
3✔
672
  }
673

674
  private void extractArchive(Path file, Path targetDir, Function<InputStream, ArchiveInputStream> unpacker) {
675

676
    this.context.trace("Unpacking archive {} to {}", file, targetDir);
14✔
677
    try (InputStream is = Files.newInputStream(file); ArchiveInputStream ais = unpacker.apply(is); IdeProgressBar pb = getProgressbarForUnpacking(
15✔
678
        getFileSize(file))) {
1✔
679
      ArchiveEntry entry = ais.getNextEntry();
3✔
680
      boolean isTar = ais instanceof TarArchiveInputStream;
3✔
681
      while (entry != null) {
2✔
682
        String permissionStr = null;
2✔
683
        if (isTar) {
2!
684
          int tarMode = ((TarArchiveEntry) entry).getMode();
4✔
685
          permissionStr = generatePermissionString(tarMode);
3✔
686
        }
687
        Path entryName = Path.of(entry.getName());
6✔
688
        Path entryPath = targetDir.resolve(entryName).toAbsolutePath();
5✔
689
        if (!entryPath.startsWith(targetDir)) {
4!
690
          throw new IOException("Preventing path traversal attack from " + entryName + " to " + entryPath);
×
691
        }
692
        if (entry.isDirectory()) {
3✔
693
          mkdirs(entryPath);
4✔
694
        } else {
695
          // ensure the file can also be created if directory entry was missing or out of order...
696
          mkdirs(entryPath.getParent());
4✔
697
          Files.copy(ais, entryPath);
6✔
698
        }
699
        if (isTar && !this.context.getSystemInfo().isWindows()) {
7!
700
          Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(permissionStr);
3✔
701
          Files.setPosixFilePermissions(entryPath, permissions);
4✔
702
        }
703
        pb.stepBy(entry.getSize());
4✔
704
        entry = ais.getNextEntry();
3✔
705
      }
1✔
706
    } catch (IOException e) {
×
707
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
708
    }
1✔
709
  }
1✔
710

711
  private void extractZipArchive(Path file, Path targetDir) {
712

713
    this.context.trace("Unpacking archive {} to {}", file, targetDir);
14✔
714
    try (FileInputStream fis = new FileInputStream(file.toFile()); ZipInputStream zis = new ZipInputStream(fis); IdeProgressBar pb = getProgressbarForUnpacking(
16✔
715
        getFileSize(file))) {
1✔
716
      ZipEntry entry = zis.getNextEntry();
3✔
717
      while (entry != null) {
2✔
718
        Path entryName = Path.of(entry.getName());
6✔
719
        Path entryPath = targetDir.resolve(entryName).toAbsolutePath();
5✔
720
        if (!entryPath.startsWith(targetDir)) {
4!
721
          throw new IOException("Preventing path traversal attack from " + entryName + " to " + entryPath);
×
722
        }
723
        if (entry.isDirectory()) {
3!
724
          mkdirs(entryPath);
×
725
        } else {
726
          // ensure the file can also be created if directory entry was missing or out of order...
727
          mkdirs(entryPath.getParent());
4✔
728
          Files.copy(zis, entryPath);
6✔
729
        }
730
        pb.stepBy(entry.getCompressedSize());
4✔
731
        zis.closeEntry();
2✔
732
        entry = zis.getNextEntry();
3✔
733
      }
1✔
734
    } catch (IOException e) {
×
735
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
736
    }
1✔
737
  }
1✔
738

739
  @Override
740
  public void extractDmg(Path file, Path targetDir) {
741

742
    assert this.context.getSystemInfo().isMac();
×
743

744
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
745
    mkdirs(mountPath);
×
746
    ProcessContext pc = this.context.newProcess();
×
747
    pc.executable("hdiutil");
×
748
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
749
    pc.run();
×
750
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
751
    if (appPath == null) {
×
752
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
753
    }
754

755
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
756
    pc.addArgs("detach", "-force", mountPath);
×
757
    pc.run();
×
758
  }
×
759

760
  @Override
761
  public void extractMsi(Path file, Path targetDir) {
762

763
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
764
    // msiexec also creates a copy of the MSI
765
    Path msiCopy = targetDir.resolve(file.getFileName());
×
766
    delete(msiCopy);
×
767
  }
×
768

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

772
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
773
    ProcessContext pc = this.context.newProcess();
×
774
    // we might also be able to use cpio from commons-compression instead of external xar...
775
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
776
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
777
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
778
    delete(tmpDirPkg);
×
779
  }
×
780

781
  @Override
782
  public void delete(Path path) {
783

784
    if (!Files.exists(path)) {
5✔
785
      this.context.trace("Deleting {} skipped as the path does not exist.", path);
10✔
786
      return;
1✔
787
    }
788
    this.context.debug("Deleting {} ...", path);
10✔
789
    try {
790
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
791
        Files.delete(path);
×
792
      } else {
793
        deleteRecursive(path);
3✔
794
      }
795
    } catch (IOException e) {
×
796
      throw new IllegalStateException("Failed to delete " + path, e);
×
797
    }
1✔
798
  }
1✔
799

800
  private void deleteRecursive(Path path) throws IOException {
801

802
    if (Files.isDirectory(path)) {
5✔
803
      try (Stream<Path> childStream = Files.list(path)) {
3✔
804
        Iterator<Path> iterator = childStream.iterator();
3✔
805
        while (iterator.hasNext()) {
3✔
806
          Path child = iterator.next();
4✔
807
          deleteRecursive(child);
3✔
808
        }
1✔
809
      }
810
    }
811
    this.context.trace("Deleting {} ...", path);
10✔
812
    Files.delete(path);
2✔
813
  }
1✔
814

815
  @Override
816
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
817

818
    try {
819
      if (!Files.isDirectory(dir)) {
5!
820
        return null;
×
821
      }
822
      return findFirstRecursive(dir, filter, recursive);
6✔
823
    } catch (IOException e) {
×
824
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
825
    }
826
  }
827

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

830
    List<Path> folders = null;
2✔
831
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
832
      Iterator<Path> iterator = childStream.iterator();
3✔
833
      while (iterator.hasNext()) {
3✔
834
        Path child = iterator.next();
4✔
835
        if (filter.test(child)) {
4✔
836
          return child;
4✔
837
        } else if (recursive && Files.isDirectory(child)) {
2!
838
          if (folders == null) {
×
839
            folders = new ArrayList<>();
×
840
          }
841
          folders.add(child);
×
842
        }
843
      }
1✔
844
    }
4!
845
    if (folders != null) {
2!
846
      for (Path child : folders) {
×
847
        Path match = findFirstRecursive(child, filter, recursive);
×
848
        if (match != null) {
×
849
          return match;
×
850
        }
851
      }
×
852
    }
853
    return null;
2✔
854
  }
855

856
  /**
857
   * @param sizeFile the size of archive
858
   * @return prepared progressbar for unpacking
859
   */
860
  private IdeProgressBar getProgressbarForUnpacking(long sizeFile) {
861

862
    return this.context.prepareProgressBar("Unpacking", sizeFile);
6✔
863
  }
864

865
  @Override
866
  public List<Path> listChildren(Path dir, Predicate<Path> filter) {
867

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

889
  @Override
890
  public boolean isEmptyDir(Path dir) {
891

892
    return listChildren(dir, f -> true).isEmpty();
8✔
893
  }
894

895
  /**
896
   * Gets the file size of a provided file path.
897
   *
898
   * @param path of the file.
899
   * @return the file size.
900
   */
901
  protected long getFileSize(Path path) {
902

903
    return path.toFile().length();
4✔
904
  }
905

906
  @Override
907
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
908

909
    for (Path dir : searchDirs) {
10!
910
      Path filePath = dir.resolve(fileName);
4✔
911
      try {
912
        if (Files.exists(filePath)) {
5!
913
          return filePath;
2✔
914
        }
915
      } catch (Exception e) {
×
916
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
917
      }
×
918
    }
×
919
    return null;
×
920
  }
921

922
  @Override
923
  public void makeExecutable(Path filePath) {
924
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
925
      return;
×
926
    }
927

928
    if (Files.exists(filePath)) {
5!
929
      // Read the current file permissions
930
      Set<PosixFilePermission> perms;
931
      try {
932
        perms = Files.getPosixFilePermissions(filePath);
5✔
933
      } catch (IOException e) {
×
934
        throw new RuntimeException(e);
×
935
      }
1✔
936

937
      if (perms != null) {
2!
938
        // Add execute permission for all users
939
        perms.add(PosixFilePermission.OWNER_EXECUTE);
4✔
940
        perms.add(PosixFilePermission.GROUP_EXECUTE);
4✔
941
        perms.add(PosixFilePermission.OTHERS_EXECUTE);
4✔
942

943
        // Set the new permissions
944
        try {
945
          Files.setPosixFilePermissions(filePath, perms);
4✔
946
        } catch (IOException e) {
×
947
          throw new RuntimeException(e);
×
948
        }
1✔
949
      }
950
    } else {
1✔
951
      this.context.warning("Cannot set executable flag on file that does not exist: {}", filePath);
×
952
    }
953
  }
1✔
954

955
  @Override
956
  public void touch(Path filePath) {
957

958
    if (Files.exists(filePath)) {
5✔
959
      try {
960
        Files.setLastModifiedTime(filePath, FileTime.fromMillis(System.currentTimeMillis()));
5✔
961
      } catch (IOException e) {
×
962
        throw new IllegalStateException("Could not update modification-time of " + filePath, e);
×
963
      }
1✔
964
    } else {
965
      try {
966
        Files.createFile(filePath);
5✔
967
      } catch (IOException e) {
1✔
968
        throw new IllegalStateException("Could not create empty file " + filePath, e);
7✔
969
      }
1✔
970
    }
971
  }
1✔
972
}
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