Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#264: prevent Windows file lock #288

Merged
merged 2 commits into from
Apr 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading