|
13 | 13 | import java.net.http.HttpResponse;
|
14 | 14 | import java.nio.file.FileSystemException;
|
15 | 15 | import java.nio.file.Files;
|
| 16 | +import java.nio.file.LinkOption; |
| 17 | +import java.nio.file.NoSuchFileException; |
16 | 18 | import java.nio.file.Path;
|
17 | 19 | import java.nio.file.Paths;
|
| 20 | +import java.nio.file.attribute.BasicFileAttributes; |
18 | 21 | import java.security.DigestInputStream;
|
19 | 22 | import java.security.MessageDigest;
|
20 | 23 | import java.security.NoSuchAlgorithmException;
|
@@ -290,28 +293,158 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
|
290 | 293 | }
|
291 | 294 | }
|
292 | 295 |
|
| 296 | + /** |
| 297 | + * Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an |
| 298 | + * {@link IllegalStateException} if there is a file at the given {@link Path} that is neither a symbolic link nor a |
| 299 | + * Windows junction. |
| 300 | + * |
| 301 | + * @param path the {@link Path} to delete. |
| 302 | + * @throws IOException if the actual {@link Files#delete(Path) deletion} fails. |
| 303 | + */ |
| 304 | + private void deleteLinkIfExists(Path path) throws IOException { |
| 305 | + |
| 306 | + boolean exists = false; |
| 307 | + boolean isJunction = false; |
| 308 | + if (this.context.getSystemInfo().isWindows()) { |
| 309 | + try { // since broken junctions are not detected by Files.exists(brokenJunction) |
| 310 | + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); |
| 311 | + exists = true; |
| 312 | + isJunction = attr.isOther() && attr.isDirectory(); |
| 313 | + } catch (NoSuchFileException e) { |
| 314 | + // ignore, since there is no previous file at the location, so nothing to delete |
| 315 | + return; |
| 316 | + } |
| 317 | + } |
| 318 | + exists = exists || Files.exists(path); // "||" since broken junctions are not detected by |
| 319 | + // Files.exists(brokenJunction) |
| 320 | + boolean isSymlink = exists && Files.isSymbolicLink(path); |
| 321 | + |
| 322 | + assert !(isSymlink && isJunction); |
| 323 | + |
| 324 | + if (exists) { |
| 325 | + if (isJunction || isSymlink) { |
| 326 | + this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path); |
| 327 | + Files.delete(path); |
| 328 | + } else { |
| 329 | + throw new IllegalStateException( |
| 330 | + "The file at " + path + " was not deleted since it is not a symlink or a Windows junction"); |
| 331 | + } |
| 332 | + } |
| 333 | + } |
| 334 | + |
| 335 | + /** |
| 336 | + * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. |
| 337 | + * Additionally, {@link Path#toRealPath(LinkOption...)} is applied to {@code source}. |
| 338 | + * |
| 339 | + * @param source the {@link Path} to adapt. |
| 340 | + * @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is |
| 341 | + * set to {@code true}. |
| 342 | + * @param relative the {@code relative} flag. |
| 343 | + * @return the adapted {@link Path}. |
| 344 | + * @see FileAccessImpl#symlink(Path, Path, boolean) |
| 345 | + */ |
| 346 | + private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException { |
| 347 | + |
| 348 | + if (source.isAbsolute()) { |
| 349 | + try { |
| 350 | + source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2 |
| 351 | + } catch (IOException e) { |
| 352 | + throw new IOException( |
| 353 | + "Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e); |
| 354 | + } |
| 355 | + if (relative) { |
| 356 | + source = targetLink.getParent().relativize(source); |
| 357 | + // to make relative links like this work: dir/link -> dir |
| 358 | + source = (source.toString().isEmpty()) ? Paths.get(".") : source; |
| 359 | + } |
| 360 | + } else { // source is relative |
| 361 | + if (relative) { |
| 362 | + // even though the source is already relative, toRealPath should be called to transform paths like |
| 363 | + // this ../d1/../d2 to ../d2 |
| 364 | + source = targetLink.getParent() |
| 365 | + .relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS)); |
| 366 | + source = (source.toString().isEmpty()) ? Paths.get(".") : source; |
| 367 | + } else { // !relative |
| 368 | + try { |
| 369 | + source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS); |
| 370 | + } catch (IOException e) { |
| 371 | + throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source |
| 372 | + + ") in method FileAccessImpl.adaptPath() failed.", e); |
| 373 | + } |
| 374 | + } |
| 375 | + } |
| 376 | + return source; |
| 377 | + } |
| 378 | + |
| 379 | + /** |
| 380 | + * Creates a Windows junction at {@code targetLink} pointing to {@code source}. |
| 381 | + * |
| 382 | + * @param source must be another Windows junction or a directory. |
| 383 | + * @param targetLink the location of the Windows junction. |
| 384 | + */ |
| 385 | + private void createWindowsJunction(Path source, Path targetLink) { |
| 386 | + |
| 387 | + this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source."); |
| 388 | + Path fallbackPath; |
| 389 | + if (!source.isAbsolute()) { |
| 390 | + this.context.warning( |
| 391 | + "You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an " |
| 392 | + + "alternative, however, these can not point to relative paths. So the source (" + source |
| 393 | + + ") is interpreted as an absolute path."); |
| 394 | + try { |
| 395 | + fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS); |
| 396 | + } catch (IOException e) { |
| 397 | + throw new IllegalStateException( |
| 398 | + "Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " |
| 399 | + + "source (" + source + ") to an absolute path failed.", |
| 400 | + e); |
| 401 | + } |
| 402 | + |
| 403 | + } else { |
| 404 | + fallbackPath = source; |
| 405 | + } |
| 406 | + if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well. |
| 407 | + throw new IllegalStateException( |
| 408 | + "These junctions can only point to directories or other junctions. Please make sure that the source (" |
| 409 | + + fallbackPath + ") is one of these."); |
| 410 | + } |
| 411 | + this.context.newProcess().executable("cmd") |
| 412 | + .addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run(); |
| 413 | + } |
| 414 | + |
293 | 415 | @Override
|
294 |
| - public void symlink(Path source, Path targetLink) { |
| 416 | + public void symlink(Path source, Path targetLink, boolean relative) { |
295 | 417 |
|
296 |
| - this.context.trace("Creating symbolic link {} pointing to {}", targetLink, source); |
| 418 | + Path adaptedSource = null; |
297 | 419 | try {
|
298 |
| - if (Files.exists(targetLink) && Files.isSymbolicLink(targetLink)) { |
299 |
| - this.context.debug("Deleting symbolic link to be re-created at {}", targetLink); |
300 |
| - Files.delete(targetLink); |
301 |
| - } |
302 |
| - Files.createSymbolicLink(targetLink, source); |
| 420 | + adaptedSource = adaptPath(source, targetLink, relative); |
| 421 | + } catch (IOException e) { |
| 422 | + throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink |
| 423 | + + ") and relative (" + relative + ")", e); |
| 424 | + } |
| 425 | + this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", |
| 426 | + targetLink, adaptedSource); |
| 427 | + |
| 428 | + try { |
| 429 | + deleteLinkIfExists(targetLink); |
| 430 | + } catch (IOException e) { |
| 431 | + throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e); |
| 432 | + } |
| 433 | + |
| 434 | + try { |
| 435 | + Files.createSymbolicLink(targetLink, adaptedSource); |
303 | 436 | } catch (FileSystemException e) {
|
304 | 437 | if (this.context.getSystemInfo().isWindows()) {
|
305 |
| - this.context.info( |
306 |
| - "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: " |
307 |
| - + e.getMessage()); |
308 |
| - this.context.newProcess().executable("cmd") |
309 |
| - .addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), source.toString()).run(); |
| 438 | + this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create " |
| 439 | + + "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for " |
| 440 | + + "further details. Error was: " + e.getMessage()); |
| 441 | + createWindowsJunction(adaptedSource, targetLink); |
310 | 442 | } else {
|
311 | 443 | throw new RuntimeException(e);
|
312 | 444 | }
|
313 | 445 | } catch (IOException e) {
|
314 |
| - throw new IllegalStateException("Failed to create a symbolic link " + targetLink + " pointing to " + source, e); |
| 446 | + throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") |
| 447 | + + "symbolic link " + targetLink + " pointing to " + source, e); |
315 | 448 | }
|
316 | 449 | }
|
317 | 450 |
|
@@ -398,8 +531,7 @@ public void delete(Path path) {
|
398 | 531 | try {
|
399 | 532 | if (Files.isSymbolicLink(path)) {
|
400 | 533 | Files.delete(path);
|
401 |
| - } |
402 |
| - else { |
| 534 | + } else { |
403 | 535 | deleteRecursive(path);
|
404 | 536 | }
|
405 | 537 | } catch (IOException e) {
|
|
0 commit comments