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 2 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
26 changes: 26 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ public interface FileAccess {
*/
void move(Path source, Path targetDir);

/**
* Symbolic links can point to relative or absolute paths. Here the link is converted to be relative. If the target of
* the link is again a link, then that lead is not followed.
*
* @param link the {@link Path} of the symbolic link.
*/
void makeSymlinkRelative(Path link);

/**
* Symbolic links can point to relative or absolute paths. Here the link is converted to be relative.
*
* @param link the {@link Path} of the symbolic link.
* @param followTarget - {@code true} if the target of the link is again a link, then that lead is followed, until the
* target is not a link. - {@code false} if the target of the link is again a link, then that lead is not
* followed.
*/
void makeSymlinkRelative(Path link, boolean followTarget);

/**
* Creates a symbolic relative link.
*
* @param source the source {@link Path} to link to.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
*/
void relativeSymlink(Path targetLink, Path source);
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param source the source {@link Path} to link to.
* @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
Expand Down
71 changes: 63 additions & 8 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import java.net.http.HttpResponse;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
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 @@ -282,22 +284,74 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
}
}

@Override
public void makeSymlinkRelative(Path link) {

makeSymlinkRelative(link, false);
}

@Override
public void makeSymlinkRelative(Path link, boolean followTarget) {

if (!Files.isSymbolicLink(link)) {
throw new IllegalStateException(
"Can't call makeSymlinkRelative on " + link + " since it is not a symbolic link.");
}
Path linkTarget = null;
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
try {
linkTarget = followTarget ? link.toRealPath() : Files.readSymbolicLink(link);
} catch (IOException e) {
throw new RuntimeException("For link " + link + " the call to "
+ (followTarget ? "toRealPath" : "readSymbolicLink") + " in method makeSymlinkRelative failed.", e);
}
this.context.getFileAccess().delete(link); // delete old absolute link
this.context.getFileAccess().relativeSymlink(link, linkTarget); // and replace it by the new relative link
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public void relativeSymlink(Path link, Path source) {

Path relativeSource = link.getParent().relativize(source);
// to make relative links like this work: dir/link -> dir
relativeSource = (relativeSource.toString().isEmpty()) ? Paths.get(".") : relativeSource;
symlink(relativeSource, link);
}
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved

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

this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source);
try {
if (Files.exists(targetLink) && Files.isSymbolicLink(targetLink)) {
this.context.debug("Deleting symbolic link to be re-created at {}", targetLink);
Files.delete(targetLink);
if (Files.exists(targetLink)) {
if (Files.isSymbolicLink(targetLink)) {
this.context.debug("Deleting symbolic link to be re-created at {}", targetLink);
Files.delete(targetLink);
} else {
BasicFileAttributes attr = Files.readAttributes(targetLink, BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
if (attr.isOther() && attr.isDirectory()) {
this.context.debug("Deleting symbolic link (junction) to be re-created at {}", targetLink);
Files.delete(targetLink);
}
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
}
}
Files.createSymbolicLink(targetLink, source);
} catch (FileSystemException e) {
if (this.context.getSystemInfo().isWindows()) {
info(
"Due to lack of permissions, Microsofts 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());

String infoMsg = "Due to lack of permissions, Microsofts 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();
info(infoMsg);
if (!source.isAbsolute()) {
throw new IllegalStateException(
infoMsg + "\\n These junctions can only point to absolute paths. Please make sure that the targetLink ("
+ targetLink + ") is absolute.");
}
if (!Files.isDirectory(source)) { // if source is a junction. This returns true as well.
throw new IllegalStateException(infoMsg
+ "\\n These junctions can only point to directories or other junctions. Please make sure that the source ("
+ source + ") is one of these.");
}
context.newProcess().executable("cmd")
.addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run();
} else {
Expand Down Expand Up @@ -391,8 +445,9 @@ public void delete(Path path) {
try {
if (Files.isSymbolicLink(path)) {
Files.delete(path);
} else {
deleteRecursive(path);
}
deleteRecursive(path);
hohwille marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
throw new IllegalStateException("Failed to delete " + path, e);
}
Expand Down
227 changes: 227 additions & 0 deletions cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package com.devonfw.tools.ide.io;

import static org.junit.jupiter.api.Assertions.*;
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Function;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeTestContextMock;

public class FileAccessImplTest extends Assertions {

@Test
void testSymlink(@TempDir Path tempDir) {
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved

IdeContext context = IdeTestContextMock.get();
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
FileAccess fileAccess = new FileAccessImpl(context);
Path dir = tempDir.resolve("dir");
fileAccess.mkdirs(dir);

Path file = tempDir.resolve("file");
try {
Files.write(file, "Hello World!".getBytes());
} catch (IOException e) {
throw new RuntimeException("When preparing testSymlink writing of " + file + " failed. ", e);
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
}
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved

Path link = tempDir.resolve("link");
Path linkToLink = tempDir.resolve("linkToLink");

boolean junctionsUsed = false;
try {
Files.createSymbolicLink(link, file);
} catch (IOException e) { // if permission is not hold, then junctions are used instead of symlinks (on Windows)
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
if (context.getSystemInfo().isWindows()) {
junctionsUsed = true;
// this should work
fileAccess.symlink(dir, link);
fileAccess.symlink(dir, link); // should work again
fileAccess.symlink(link, linkToLink);

IllegalStateException e1 = assertThrows(IllegalStateException.class, () -> {
fileAccess.symlink(file, link);
});
assertThat(e1).hasMessageContaining("These junctions can only point to directories or other junctions");

IllegalStateException e2 = assertThrows(IllegalStateException.class, () -> {
fileAccess.symlink(Paths.get("dir"), link);
});
assertThat(e2).hasMessageContaining("These junctions can only point to absolute paths");
} else {
throw new RuntimeException(
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
"Creating symbolic link with Files.createSymbolicLink failed and junctions can not be used since the OS is not windows: "
+ e.getMessage());
}
}

// test for normal symlinks (not junctions)
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
if (!junctionsUsed) {
try {
fileAccess.symlink(file, link); // should work again
fileAccess.symlink(link, linkToLink);
} catch (Exception e) {
fail("Creating symbolic links failed: " + e.getMessage());
}
try {
assertEquals(linkToLink.toRealPath(), file);
assertEquals(Files.readSymbolicLink(linkToLink), link);
} catch (IOException e) {
fail("Reading symbolic links failed: " + e.getMessage());
}
}
}

@Test
void testMakeSymlinkRelative(@TempDir Path tempDir) {

// arrange
IdeContext context = IdeTestContextMock.get();
FileAccess fileAccess = new FileAccessImpl(context);
Path parent = tempDir.resolve("parent");
Path d1 = parent.resolve("d1");
Path d11 = d1.resolve("d11");
Path d111 = d11.resolve("d111");
Path d1111 = d111.resolve("d1111");
Path d2 = parent.resolve("d2");
Path d22 = d2.resolve("d22");
Path d222 = d22.resolve("d222");
Path[] dirPaths = new Path[] { parent, d1, d11, d111, d1111, d2, d22, d222 };
for (Path dirPath : dirPaths) {
fileAccess.mkdirs(dirPath);
}
Path link_d11_d1 = d11.resolve("link_d11_d1");
fileAccess.symlink(d1, link_d11_d1);

Path link_d11_d11 = d11.resolve("link_d11_d11");
fileAccess.symlink(d11, link_d11_d11);

Path link_d11_d111 = d11.resolve("link_d11_d111");
fileAccess.symlink(d111, link_d11_d111);

Path link_d11_d1111 = d11.resolve("link_d11_d1111");
fileAccess.symlink(d1111, link_d11_d1111);

Path link_d11_d2 = d11.resolve("link_d11_d2");
fileAccess.symlink(d2, link_d11_d2);

Path link_d11_d22 = d11.resolve("link_d11_d22");
fileAccess.symlink(d22, link_d11_d22);

Path link_d11_d222 = d11.resolve("link_d11_d222");
fileAccess.symlink(d222, link_d11_d222);

Path link_d22_link_d11_d1 = d22.resolve("link_d22_link_d11_d1");
fileAccess.symlink(link_d11_d1, link_d22_link_d11_d1);

Path link_d2_link_d11_d1 = d2.resolve("link_d2_link_d11_d1");
fileAccess.symlink(link_d11_d1, link_d2_link_d11_d1);

Path link_parent_link_d2_link_d11_d1 = parent.resolve("link_parent_link_d2_link_d11_d1");
fileAccess.symlink(link_d2_link_d11_d1, link_parent_link_d2_link_d11_d1);

Path[] links = new Path[] { link_d11_d1, link_d11_d11, link_d11_d111, link_d11_d1111, link_d11_d2, link_d11_d22,
link_d11_d222, link_d22_link_d11_d1, link_d2_link_d11_d1, link_parent_link_d2_link_d11_d1 };

// act: check if moving breaks absolute symlinks
Path parent2 = tempDir.resolve("parent2");
fileAccess.move(parent, parent2);

// assert: check if moving breaks absolute symlinks
Function<Path, Path> transformPath = path -> {
String newPath = path.toString().replace("_parent_", "_par_").replace("parent", "parent2").replace("_par_",
"_parent_");
return Paths.get(newPath);
};
for (Path link : links) {
try {
Path linkInParent2 = transformPath.apply(link);
assertThat(linkInParent2).existsNoFollowLinks();
Path realPath = linkInParent2.toRealPath();
if (Files.exists(realPath)) {
fail("The link target " + realPath + " (from toRealPath) should not exist");
}
Path readPath = Files.readSymbolicLink(linkInParent2);
if (!Files.exists(readPath)) {
fail("The link target " + readPath + " (from readSymbolicLink) should not exist");
}
} catch (IOException e) {
assertThat(e).isInstanceOf(IOException.class);
}
}

// assert: Can't call makeSymlinkRelative since it is not a symbolic link
IllegalStateException e1 = assertThrows(IllegalStateException.class, () -> {
fileAccess.makeSymlinkRelative(d1);
});
assertThat(e1).hasMessageContaining("is not a symbolic link");

boolean junctionsUsed = false;
try {
Files.createSymbolicLink(tempDir.resolve("my_test_link"), parent2);
} catch (IOException e) {
if (!context.getSystemInfo().isWindows()) {
fail("Creating symbolic link with Files.createSymbolicLink failed and junctions can not be used"
+ " since the OS is not windows: " + e.getMessage());
}
junctionsUsed = true;
IllegalStateException e2 = assertThrows(IllegalStateException.class, () -> {
fileAccess.makeSymlinkRelative(link_d2_link_d11_d1);
});
assertThat(e2).hasMessageContaining("is not a symbolic link");
}

// act: make symlinks relative and move
fileAccess.move(parent2, parent); // revert previous move
if (!junctionsUsed) {
for (Path link : links) {
if (link.equals(link_d2_link_d11_d1)) {
fileAccess.makeSymlinkRelative(link, false);
} else {
fileAccess.makeSymlinkRelative(link, true);
}
}

// redo move, and check in assert if symlinks still work
fileAccess.move(parent, parent2);

// assert
for (Path link : links) {
Path linkInParent2 = transformPath.apply(link);
if (link.equals(link_d2_link_d11_d1)) {
try { // checking if the transformation of absolute to relative path with flag followTarget=false works
Path correct = transformPath.apply(link_d11_d1);
assertEquals(correct, linkInParent2.getParent().resolve(Files.readSymbolicLink(linkInParent2))
.toRealPath(LinkOption.NOFOLLOW_LINKS));
} catch (IOException e) {
throw new RuntimeException("Couldn't get path of link where followTarget was set to false: ", e);
}
}
assertThat(linkInParent2).existsNoFollowLinks();
try {
Path realPath = linkInParent2.toRealPath();
assertThat(realPath).existsNoFollowLinks();
assertThat(realPath).exists();
} catch (IOException e) {
throw new RuntimeException("Could not call toRealPath on moved relative link: " + linkInParent2, e);
}
try {
Path readPath = Files.readSymbolicLink(linkInParent2);
assertThat(linkInParent2.getParent().resolve(readPath)).existsNoFollowLinks();
assertThat(linkInParent2.getParent().resolve(readPath)).exists();
} catch (IOException e) {
throw new RuntimeException("Could not call Files.readSymbolicLink on moved relative link: " + linkInParent2,
e);
}
}
}
}
}
MattesMrzik marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void testIllegal() {
for (String version : illegalVersions) {
try {
VersionIdentifier.of(version);
fail("Illegal verion '" + version + "' did not cause an exception!");
fail("Illegal version '" + version + "' did not cause an exception!");
hohwille marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
assertThat(e).hasMessageContaining(version);
Expand Down
Loading