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

#139: feature for making symlinks relative #140

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c970265
#139: relative symlink feature
MattesMrzik Nov 13, 2023
a360f9f
#139: clean up
MattesMrzik Nov 13, 2023
e295e9d
#139: added check for windows when rewriting junction
MattesMrzik Nov 16, 2023
66f09cf
#139: rewrote test, one still missing
MattesMrzik Nov 28, 2023
75eb709
#139: improved test, removed makeSymlinkRelative
MattesMrzik Nov 29, 2023
0719c4f
Merge remote-tracking branch 'upstream/main' into feature/#139-featur…
MattesMrzik Nov 29, 2023
cf287fe
#139: small bugfix in test
MattesMrzik Nov 29, 2023
24f3307
#139: fixed linux bug
MattesMrzik Nov 29, 2023
496e434
#139: added comments to test
MattesMrzik Dec 4, 2023
8d84bee
#139: impl. first change req from PR
MattesMrzik Dec 11, 2023
5d70c60
'139: added comments
MattesMrzik Dec 11, 2023
bbeb7e4
Update FileAccess.java: improved JavaDoc
hohwille Dec 11, 2023
18ec98f
#139: fixed bug
MattesMrzik Dec 11, 2023
736935c
Merge remote-tracking branch 'origin/feature/#139-feature-for-making-…
MattesMrzik Dec 11, 2023
304c48c
#139: changed fallback in case of windows junctions
MattesMrzik Dec 11, 2023
f8dfe79
#139: fixed toAbsolute, which was wrong, more tests
MattesMrzik Dec 11, 2023
b3994c8
#139: modified tests and adaptPath
MattesMrzik Dec 12, 2023
7683f5b
#139: always use toRealPath in symlink
MattesMrzik Dec 12, 2023
be24f11
#139: clean up
MattesMrzik Dec 12, 2023
985e964
Merge branch 'main' of https://github.com/devonfw/IDEasy into feature…
MattesMrzik Dec 12, 2023
f1f835b
Merge branch 'main' into feature/#139-feature-for-making-symlinks-rel…
hohwille Jan 4, 2024
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
22 changes: 20 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,28 @@ public interface FileAccess {
void move(Path source, Path targetDir);

/**
* @param source the source {@link Path} to link to.
* Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows
* junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must
* point to absolute paths. Therefore, the created link will be absolute instead of relative.
*
* @param source the source {@link Path} to link to, may be relative or absolute.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
* @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute.
*/
void symlink(Path source, Path targetLink);
void symlink(Path source, Path targetLink, boolean relative);

/**
* Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a
* Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback,
* which must point to absolute paths. Therefore, the created link will be absolute instead of relative.
*
* @param source the source {@link Path} to link to, may be relative or absolute.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
*/
default void symlink(Path source, Path targetLink) {

symlink(source, targetLink, true);
}

/**
* @param source the source {@link Path file or folder} to copy.
Expand Down
162 changes: 147 additions & 15 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
import java.net.http.HttpResponse;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -290,28 +293,158 @@ 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.
*
* @param path the {@link Path} to delete.
* @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
*/
private void deleteLinkIfExists(Path path) throws IOException {

boolean exists = false;
boolean isJunction = false;
if (this.context.getSystemInfo().isWindows()) {
try { // since broken junctions are not detected by Files.exists(brokenJunction)
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
exists = true;
isJunction = attr.isOther() && attr.isDirectory();
} catch (NoSuchFileException e) {
// ignore, since there is no previous file at the location, so nothing to delete
return;
}
}
exists = exists || Files.exists(path); // "||" since broken junctions are not detected by
// Files.exists(brokenJunction)
boolean isSymlink = exists && Files.isSymbolicLink(path);

assert !(isSymlink && isJunction);

if (exists) {
if (isJunction || isSymlink) {
this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
Files.delete(path);
} else {
throw new IllegalStateException(
"The file at " + path + " was not deleted since it is not a symlink or a Windows junction");
}
}
}

/**
* 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 relative the {@code relative} flag.
* @return the adapted {@link Path}.
* @see FileAccessImpl#symlink(Path, Path, boolean)
*/
private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {

if (source.isAbsolute()) {
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);
}
if (relative) {
source = targetLink.getParent().relativize(source);
// to make relative links like this work: dir/link -> dir
source = (source.toString().isEmpty()) ? Paths.get(".") : source;
}
} else { // source is relative
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 = (source.toString().isEmpty()) ? Paths.get(".") : 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);
}
}
}
return source;
}

/**
* Creates a Windows junction at {@code targetLink} pointing to {@code source}.
*
* @param source must be another Windows junction or a directory.
* @param targetLink the location of the Windows junction.
*/
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.");
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);
}

} 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.");
}
this.context.newProcess().executable("cmd")
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
}

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

this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source);
Path adaptedSource = null;
try {
if (Files.exists(targetLink) && Files.isSymbolicLink(targetLink)) {
this.context.debug("Deleting symbolic link to be re-created at {}", targetLink);
Files.delete(targetLink);
}
Files.createSymbolicLink(targetLink, source);
adaptedSource = adaptPath(source, targetLink, relative);
} catch (IOException 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);

try {
deleteLinkIfExists(targetLink);
} catch (IOException e) {
throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
}

try {
Files.createSymbolicLink(targetLink, adaptedSource);
} catch (FileSystemException e) {
if (this.context.getSystemInfo().isWindows()) {
this.context.info(
"Due to lack of permissions, Microsofts mklink with junction has 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());
this.context.newProcess().executable("cmd")
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run();
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());
createWindowsJunction(adaptedSource, targetLink);
} else {
throw new RuntimeException(e);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to create a 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 @@ -398,8 +531,7 @@ public void delete(Path path) {
try {
if (Files.isSymbolicLink(path)) {
Files.delete(path);
}
else {
} else {
deleteRecursive(path);
}
} catch (IOException e) {
Expand Down
Loading
Loading