Skip to content

Commit

Permalink
devonfw#139: relative symlink feature
Browse files Browse the repository at this point in the history
  • Loading branch information
MattesMrzik committed Nov 13, 2023
1 parent ad2f006 commit c970265
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 9 deletions.
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 followed, until the target is not a link.
*
* @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 no
* 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);

/**
* @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;
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
}

@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);
}

@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);
}
}
}
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);
} catch (IOException e) {
throw new IllegalStateException("Failed to delete " + path, e);
}
Expand Down
234 changes: 234 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,234 @@
package com.devonfw.tools.ide.io;

import static org.junit.jupiter.api.Assertions.*;

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 {

void arrangetestRelativeSymlinks(Path tempDir, FileAccess fileAccess) {

}

@Test
void testSymlink(@TempDir Path tempDir) {

IdeContext context = IdeTestContextMock.get();
FileAccess fileAccess = new FileAccessImpl(context);
// create a new directory
Path dir = tempDir.resolve("dir");
fileAccess.mkdirs(dir);

// create a new file using nio
Path file = tempDir.resolve("file");
try {
Files.write(file, "Hello World!".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}

// try to create a symlink to the file using Files.createSymbolicLink
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)
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(
"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)
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 later 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);
}
}
}
}
}
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!");
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
assertThat(e).hasMessageContaining(version);
Expand Down

0 comments on commit c970265

Please sign in to comment.