Skip to content

Commit

Permalink
#264: prevent Windows file lock errors (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvomiero authored Apr 19, 2024
1 parent eb9f9d0 commit 2ff8bc3
Showing 1 changed file with 47 additions and 69 deletions.
116 changes: 47 additions & 69 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
package com.devonfw.tools.ide.io;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.os.SystemInfoImpl;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
import com.devonfw.tools.ide.util.DateTimeUtil;
import com.devonfw.tools.ide.util.FilenameUtil;
import com.devonfw.tools.ide.util.HexUtil;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;

import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
Expand Down Expand Up @@ -32,21 +46,6 @@
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.os.SystemInfoImpl;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
import com.devonfw.tools.ide.util.DateTimeUtil;
import com.devonfw.tools.ide.util.FilenameUtil;
import com.devonfw.tools.ide.util.HexUtil;

/**
* Implementation of {@link FileAccess}.
*/
Expand Down Expand Up @@ -106,14 +105,11 @@ public void download(String url, Path target) {
* @param target Path of the target directory.
* @param response the {@link HttpResponse} to use.
*/
private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response)
throws IOException {
private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) throws IOException {

long contentLength = response.headers().firstValueAsLong("content-length").orElse(0);
if (contentLength == 0) {
this.context.warning(
"Content-Length was not provided by download source : {} using fallback for the progress bar which will be inaccurate.",
url);
this.context.warning("Content-Length was not provided by download source : {} using fallback for the progress bar which will be inaccurate.", url);
contentLength = 10000000;
}

Expand Down Expand Up @@ -147,8 +143,7 @@ private void downloadFileWithProgressBar(String url, Path target, HttpResponse<I
*/
private void copyFileWithProgressBar(Path source, Path target) throws IOException {

try (InputStream in = new FileInputStream(source.toFile());
OutputStream out = new FileOutputStream(target.toFile())) {
try (InputStream in = new FileInputStream(source.toFile()); OutputStream out = new FileOutputStream(target.toFile())) {

long size = source.toFile().length();
byte[] buf = new byte[1024];
Expand Down Expand Up @@ -238,8 +233,7 @@ private boolean isJunction(Path path) {
return false; // file doesn't exist
} catch (IOException e) {
// errors in reading the attributes of the file
throw new IllegalStateException(
"An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
throw new IllegalStateException("An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
}
}

Expand Down Expand Up @@ -331,9 +325,8 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
}

/**
* 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 {@link Path} that is neither a symbolic link nor a
* Windows junction.
* 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
* {@link Path} that is neither a symbolic link nor a Windows junction.
*
* @param path the {@link Path} to delete.
* @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
Expand All @@ -352,12 +345,11 @@ private void deleteLinkIfExists(Path path) throws IOException {
}

/**
* Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag.
* Additionally, {@link Path#toRealPath(LinkOption...)} is applied to {@code source}.
* Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
* is applied to {@code source}.
*
* @param source the {@link Path} to adapt.
* @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is
* set to {@code true}.
* @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is set to {@code true}.
* @param relative the {@code relative} flag.
* @return the adapted {@link Path}.
* @see FileAccessImpl#symlink(Path, Path, boolean)
Expand All @@ -368,8 +360,7 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO
try {
source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
} catch (IOException e) {
throw new IOException(
"Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
throw new IOException("Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
}
if (relative) {
source = targetLink.getParent().relativize(source);
Expand All @@ -380,15 +371,13 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO
if (relative) {
// even though the source is already relative, toRealPath should be called to transform paths like
// this ../d1/../d2 to ../d2
source = targetLink.getParent()
.relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
source = targetLink.getParent().relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
source = (source.toString().isEmpty()) ? Path.of(".") : source;
} else { // !relative
try {
source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source
+ ") in method FileAccessImpl.adaptPath() failed.", e);
throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
}
}
}
Expand All @@ -406,29 +395,24 @@ private void createWindowsJunction(Path source, Path targetLink) {
this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
Path fallbackPath;
if (!source.isAbsolute()) {
this.context.warning(
"You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
+ "alternative, however, these can not point to relative paths. So the source (" + source
+ ") is interpreted as an absolute path.");
this.context.warning("You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
+ "alternative, however, these can not point to relative paths. So the source (" + source + ") is interpreted as an absolute path.");
try {
fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
throw new IllegalStateException(
"Since Windows junctions are used, the source must be an absolute path. The transformation of the passed "
+ "source (" + source + ") to an absolute path failed.",
e);
"Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " + "source (" + source
+ ") to an absolute path failed.", e);
}

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

@Override
Expand All @@ -438,11 +422,9 @@ public void symlink(Path source, Path targetLink, boolean relative) {
try {
adaptedSource = adaptPath(source, targetLink, relative);
} catch (IOException e) {
throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink
+ ") and relative (" + relative + ")", e);
throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink + ") and relative (" + relative + ")", e);
}
this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative",
targetLink, adaptedSource);
this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", targetLink, adaptedSource);

try {
deleteLinkIfExists(targetLink);
Expand All @@ -455,15 +437,15 @@ public void symlink(Path source, Path targetLink, boolean relative) {
} catch (FileSystemException e) {
if (SystemInfoImpl.INSTANCE.isWindows()) {
this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
+ "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for "
+ "further details. Error was: " + e.getMessage());
+ "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for " + "further details. Error was: "
+ e.getMessage());
createWindowsJunction(adaptedSource, targetLink);
} else {
throw new RuntimeException(e);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative")
+ "symbolic link " + targetLink + " pointing to " + source, e);
throw new IllegalStateException(
"Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + "symbolic link " + targetLink + " pointing to " + source, e);
}
}

Expand Down Expand Up @@ -521,8 +503,7 @@ public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtract
return;
}
Path tmpDir = createTempDir("extract-" + archiveFile.getFileName());
this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", archiveFile, tmpDir,
targetDir);
this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", archiveFile, tmpDir, targetDir);
String filename = archiveFile.getFileName().toString();
TarCompression tarCompression = TarCompression.of(filename);
if (tarCompression != null) {
Expand Down Expand Up @@ -567,21 +548,19 @@ private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallD

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

try (Stream<Path> stream = Files.list(path)) {
Path[] subFiles = stream.toArray(Path[]::new);
if (subFiles.length == 0) {
throw new CliException("The downloaded package " + archiveFile
+ " seems to be empty as you can check in the extracted folder " + path);
throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
} else if (subFiles.length == 1) {
String filename = subFiles[0].getFileName().toString();
if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
&& !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) {
if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
subFiles[0])) {
return getProperInstallationSubDirOf(subFiles[0], archiveFile);
}
}
Expand All @@ -604,8 +583,7 @@ public void extractTar(Path file, Path targetDir, TarCompression compression) {
}

/**
* @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file
* permissions of a file on a Unix file system.
* @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
* @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
*/
public static String generatePermissionString(int permissions) {
Expand Down Expand Up @@ -707,7 +685,7 @@ public void delete(Path path) {
}
this.context.debug("Deleting {} ...", path);
try {
if (Files.isSymbolicLink(path)) {
if (Files.isSymbolicLink(path) || isJunction(path)) {
Files.delete(path);
} else {
deleteRecursive(path);
Expand Down Expand Up @@ -810,7 +788,7 @@ public Path findExistingFile(String fileName, List<Path> searchDirs) {
return filePath;
}
} catch (Exception e) {
throw new IllegalStateException("Unexpected error while checking existence of file "+filePath+" .", e);
throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
}
}
return null;
Expand Down

0 comments on commit 2ff8bc3

Please sign in to comment.