From afcaffcdd9b2d23413c26bf472c575bfcfe21351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Mon, 9 Dec 2024 14:37:01 +0100 Subject: [PATCH] #799: fix zip extraction to preserve file attributes (#835) --- CHANGELOG.adoc | 1 + .../devonfw/tools/ide/context/IdeContext.java | 49 +++- .../tools/ide/context/IdeContextConsole.java | 5 +- .../tools/ide/io/AbstractIdeProgressBar.java | 69 +++-- .../com/devonfw/tools/ide/io/FileAccess.java | 22 +- .../devonfw/tools/ide/io/FileAccessImpl.java | 270 ++++++++---------- .../devonfw/tools/ide/io/FileCopyMode.java | 34 ++- .../devonfw/tools/ide/io/IdeProgressBar.java | 37 ++- .../tools/ide/io/IdeProgressBarConsole.java | 34 +-- .../tools/ide/io/PathCopyListener.java | 27 ++ .../ide/context/AbstractIdeContextTest.java | 4 +- .../ide/context/AbstractIdeTestContext.java | 8 +- .../tools/ide/io/FileAccessImplTest.java | 2 +- .../ide/io/IdeProgressBarConsoleTest.java | 9 +- .../tools/ide/io/IdeProgressBarTest.java | 13 +- .../tools/ide/io/IdeProgressBarTestImpl.java | 18 +- .../tool/androidstudio/AndroidStudioTest.java | 2 +- .../tools/ide/tool/intellij/IntellijTest.java | 2 +- 18 files changed, 368 insertions(+), 238 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 4c67c3599..0b2b28fa7 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -20,6 +20,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/782[#782]: Fix IDE_ROOT variable on Linux * https://github.com/devonfw/IDEasy/issues/637[#637]: Added option to disable updates * https://github.com/devonfw/IDEasy/issues/764[#764]: IDEasy not working properly in CMD +* https://github.com/devonfw/IDEasy/issues/799[#799]: binaries from zip download lack executable flags * https://github.com/devonfw/IDEasy/issues/81[#81]: Implement Toolcommandlet for Kubernetes * https://github.com/devonfw/IDEasy/issues/737[#737]: Adds cd command to ide shell * https://github.com/devonfw/IDEasy/issues/758[#758]: Create status commandlet diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index af8076cf8..0273629a5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -399,13 +399,50 @@ default Path getSettingsTemplatePath() { ProcessContext newProcess(); /** - * Prepares the {@link IdeProgressBar} initializes task name and maximum size as well as the behaviour and style. - * - * @param taskName name of the task. - * @param size of the content. - * @return {@link IdeProgressBar} to use. + * @param title the {@link IdeProgressBar#getTitle() title}. + * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size}. + * @param unitName the {@link IdeProgressBar#getUnitName() unit name}. + * @param unitSize the {@link IdeProgressBar#getUnitSize() unit size}. + * @return the new {@link IdeProgressBar} to use. + */ + IdeProgressBar newProgressBar(String title, long size, String unitName, long unitSize); + + /** + * @param title the {@link IdeProgressBar#getTitle() title}. + * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes. + * @return the new {@link IdeProgressBar} to use. */ - IdeProgressBar prepareProgressBar(String taskName, long size); + default IdeProgressBar newProgressBarInMib(String title, long size) { + + return newProgressBar(title, size, "MiB", 1048576); + } + + /** + * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes. + * @return the new {@link IdeProgressBar} for copy. + */ + default IdeProgressBar newProgressBarForDownload(long size) { + + return newProgressBarInMib(IdeProgressBar.TITLE_DOWNLOADING, size); + } + + /** + * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes. + * @return the new {@link IdeProgressBar} for extracting. + */ + default IdeProgressBar newProgressbarForExtracting(long size) { + + return newProgressBarInMib(IdeProgressBar.TITLE_EXTRACTING, size); + } + + /** + * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes. + * @return the new {@link IdeProgressBar} for copy. + */ + default IdeProgressBar newProgressbarForCopying(long size) { + + return newProgressBarInMib(IdeProgressBar.TITLE_COPYING, size); + } /** * @return the {@link DirectoryMerger} used to configure and merge the workspace for an {@link com.devonfw.tools.ide.tool.ide.IdeToolCommandlet IDE}. diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java index 375706b9e..d643df1c4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java @@ -59,7 +59,8 @@ protected String readLine() { } @Override - public IdeProgressBar prepareProgressBar(String taskName, long size) { - return new IdeProgressBarConsole(getSystemInfo(), taskName, size); + public IdeProgressBar newProgressBar(String title, long size, String unitName, long unitSize) { + + return new IdeProgressBarConsole(getSystemInfo(), title, size, unitName, unitSize); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java b/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java index ad425cce8..14682e9b2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java @@ -5,22 +5,59 @@ */ public abstract class AbstractIdeProgressBar implements IdeProgressBar { - private long currentProgress; + /** @see #getTitle() */ + protected final String title; + + /** @see #getMaxSize() */ + protected final long maxSize; + + /** @see #getUnitName() */ + protected final String unitName; - private final long maxLength; + /** @see #getUnitSize() */ + protected final long unitSize; + + private long currentProgress; /** - * @param maxLength the maximum length of the progress bar. + * The constructor. + * + * @param title the {@link #getTitle() title}. + * @param maxSize the {@link #getMaxSize() maximum size}. + * @param unitName the {@link #getUnitName() unit name}. + * @param unitSize the {@link #getUnitSize() unit size}. */ - public AbstractIdeProgressBar(long maxLength) { + public AbstractIdeProgressBar(String title, long maxSize, String unitName, long unitSize) { + + super(); + this.title = title; + this.maxSize = maxSize; + this.unitName = unitName; + this.unitSize = unitSize; + } + + @Override + public String getTitle() { + + return this.title; + } + + @Override + public long getMaxSize() { + + return this.maxSize; + } + + @Override + public String getUnitName() { - this.maxLength = maxLength; + return this.unitName; } @Override - public long getMaxLength() { + public long getUnitSize() { - return maxLength; + return this.unitSize; } /** @@ -38,8 +75,8 @@ public long getMaxLength() { */ protected void stepTo(long stepPosition) { - if ((this.maxLength > 0) && (stepPosition > this.maxLength)) { - stepPosition = this.maxLength; // clip to max avoiding overflow + if ((this.maxSize > 0) && (stepPosition > this.maxSize)) { + stepPosition = this.maxSize; // clip to max avoiding overflow } this.currentProgress = stepPosition; doStepTo(stepPosition); @@ -56,11 +93,11 @@ protected void stepTo(long stepPosition) { public void stepBy(long stepSize) { this.currentProgress += stepSize; - if (this.maxLength > 0) { + if (this.maxSize > 0) { // check if maximum overflow - if (this.currentProgress > this.maxLength) { - this.currentProgress = this.maxLength; - stepTo(this.maxLength); + if (this.currentProgress > this.maxSize) { + this.currentProgress = this.maxSize; + stepTo(this.maxSize); return; } } @@ -76,12 +113,12 @@ public long getCurrentProgress() { @Override public void close() { - if (this.maxLength < 0) { + if (this.maxSize < 0) { return; } - if (this.currentProgress < this.maxLength) { - stepTo(this.maxLength); + if (this.currentProgress < this.maxSize) { + stepTo(this.maxSize); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index a4ae5d3d0..d3f46e900 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -90,7 +90,7 @@ default void symlink(Path source, Path targetLink) { /** * @param source the source {@link Path file or folder} to copy. - * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. will always ensure that in the end + * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. Will always ensure that in the end * you will find the same content of {@code source} in {@code target}. */ default void copy(Path source, Path target) { @@ -105,10 +105,24 @@ default void copy(Path source, Path target) { * {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While * {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find * the same content of {@code source} in {@code target}. - * @param fileOnly - {@code true} if {@code fileOrFolder} is expected to be a file and an exception shall be thrown if it is a directory, {@code false} - * otherwise (copy recursively). + * @param mode the {@link FileCopyMode}. */ - void copy(Path source, Path target, FileCopyMode fileOnly); + default void copy(Path source, Path target, FileCopyMode mode) { + + copy(source, target, mode, PathCopyListener.NONE); + } + + /** + * @param source the source {@link Path file or folder} to copy. + * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source} + * and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to + * {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While + * {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find + * the same content of {@code source} in {@code target}. + * @param mode the {@link FileCopyMode}. + * @param listener the {@link PathCopyListener} that will be called for each copied {@link Path}. + */ + void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener); /** * @param archiveFile the {@link Path} to the file to extract. diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 2ed74428e..763616e2a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -11,7 +11,9 @@ import java.net.http.HttpClient.Redirect; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.FileSystem; import java.nio.file.FileSystemException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; @@ -27,15 +29,12 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; @@ -63,6 +62,8 @@ public class FileAccessImpl implements FileAccess { "On Windows, file operations could fail due to file locks. Please ensure the files in the moved directory are not in use. For further details, see: \n" + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE; + private static final Map FS_ENV = Map.of("encoding", "UTF-8"); + private final IdeContext context; /** @@ -126,7 +127,7 @@ public void download(String url, Path target) { private void downloadFileWithProgressBar(String url, Path target, HttpResponse response) { long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1); - informAboutMissingContentLength(contentLength, url, null); + informAboutMissingContentLength(contentLength, url); byte[] data = new byte[1024]; boolean fileComplete = false; @@ -135,7 +136,7 @@ private void downloadFileWithProgressBar(String url, Path target, HttpResponse 0) { out.write(buf, 0, readBytes); if (size > 0) { @@ -180,17 +177,10 @@ private void copyFileWithProgressBar(Path source, Path target) throws IOExceptio } } - private void informAboutMissingContentLength(long contentLength, String url, Path path) { + private void informAboutMissingContentLength(long contentLength, String url) { - String source; if (contentLength < 0) { - if (path != null) { - source = path.toString(); - } else { - source = url; - } - this.context.warning("Content-Length was not provided by download/copy source: {}.", - source); + this.context.warning("Content-Length was not provided by download from {}", url); } } @@ -247,8 +237,7 @@ public String checksum(Path file) { throw new IllegalStateException("Failed to read and hash file " + file, e); } byte[] digestBytes = md.digest(); - String checksum = HexUtil.toHexString(digestBytes); - return checksum; + return HexUtil.toHexString(digestBytes); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("No such hash algorithm " + UrlChecksum.HASH_ALGORITHM, e); } @@ -307,7 +296,7 @@ public void move(Path source, Path targetDir) { } @Override - public void copy(Path source, Path target, FileCopyMode mode) { + public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) { if (mode != FileCopyMode.COPY_TREE_CONTENT) { // if we want to copy the file or folder "source" to the existing folder "target" in a shell this will copy @@ -318,32 +307,40 @@ public void copy(Path source, Path target, FileCopyMode mode) { // Therefore we need to add the filename (foldername) of "source" to the "target" path before. // For the rare cases, where we want to copy the content of a folder (cp -r source/* target) we support // it via the COPY_TREE_CONTENT mode. - target = target.resolve(source.getFileName()); + Path fileName = source.getFileName(); + if (fileName != null) { // if filename is null, we are copying the root of a (virtual filesystem) + target = target.resolve(fileName.toString()); + } } boolean fileOnly = mode.isFileOnly(); - if (fileOnly) { - this.context.debug("Copying file {} to {}", source, target); + String operation = mode.getOperation(); + if (mode.isExtract()) { + this.context.debug("Starting to {} to {}", operation, target); } else { - this.context.debug("Copying {} recursively to {}", source, target); + if (fileOnly) { + this.context.debug("Starting to {} file {} to {}", operation, source, target); + } else { + this.context.debug("Starting to {} {} recursively to {}", operation, source, target); + } } if (fileOnly && Files.isDirectory(source)) { throw new IllegalStateException("Expected file but found a directory to copy at " + source); } if (mode.isFailIfExists()) { if (Files.exists(target)) { - throw new IllegalStateException("Failed to copy " + source + " to already existing target " + target); + throw new IllegalStateException("Failed to " + operation + " " + source + " to already existing target " + target); } } else if (mode == FileCopyMode.COPY_TREE_OVERRIDE_TREE) { delete(target); } try { - copyRecursive(source, target, mode); + copyRecursive(source, target, mode, listener); } catch (IOException e) { - throw new IllegalStateException("Failed to copy " + source + " to " + target, e); + throw new IllegalStateException("Failed to " + operation + " " + source + " to " + target, e); } } - private void copyRecursive(Path source, Path target, FileCopyMode mode) throws IOException { + private void copyRecursive(Path source, Path target, FileCopyMode mode, PathCopyListener listener) throws IOException { if (Files.isDirectory(source)) { mkdirs(target); @@ -351,15 +348,17 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I Iterator iterator = childStream.iterator(); while (iterator.hasNext()) { Path child = iterator.next(); - copyRecursive(child, target.resolve(child.getFileName()), mode); + copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener); } } + listener.onCopy(source, target, true); } else if (Files.exists(source)) { if (mode.isOverrideFile()) { delete(target); } - this.context.trace("Copying {} to {}", source, target); + this.context.trace("Starting to {} {} to {}", mode.getOperation(), source, target); Files.copy(source, target); + listener.onCopy(source, target, false); } else { throw new IOException("Path " + source + " does not exist."); } @@ -554,24 +553,12 @@ public void extract(Path archiveFile, Path targetDir, Consumer postExtract this.context.trace("Determined file extension {}", extension); } switch (extension) { - case "zip" -> { - extractZip(archiveFile, tmpDir); - } - case "jar" -> { - extractJar(archiveFile, tmpDir); - } - case "dmg" -> { - extractDmg(archiveFile, tmpDir); - } - case "msi" -> { - extractMsi(archiveFile, tmpDir); - } - case "pkg" -> { - extractPkg(archiveFile, tmpDir); - } - default -> { - throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile); - } + case "zip" -> extractZip(archiveFile, tmpDir); + case "jar" -> extractJar(archiveFile, tmpDir); + case "dmg" -> extractDmg(archiveFile, tmpDir); + case "msi" -> extractMsi(archiveFile, tmpDir); + case "pkg" -> extractPkg(archiveFile, tmpDir); + default -> throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile); } } Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile); @@ -614,7 +601,37 @@ private Path getProperInstallationSubDirOf(Path path, Path archiveFile) { @Override public void extractZip(Path file, Path targetDir) { - extractZipArchive(file, targetDir); + this.context.info("Extracting ZIP file {} to {}", file, targetDir); + URI uri = URI.create("jar:" + file.toUri()); + try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) { + long size = 0; + for (Path root : fs.getRootDirectories()) { + size += getFileSizeRecursive(root); + } + try (IdeProgressBar bp = this.context.newProgressbarForExtracting(size)) { + PathCopyListener listener = (source, target, directory) -> { + if (directory) { + return; + } + if (!context.getSystemInfo().isWindows()) { + try { + Object attribute = Files.getAttribute(source, "zip:permissions"); + if (attribute instanceof Set permissionSet) { + Files.setPosixFilePermissions(target, (Set) permissionSet); + } + } catch (Exception e) { + context.error(e, "Failed to transfer zip permissions for {}", target); + } + } + bp.stepBy(getFileSize(target)); + }; + for (Path root : fs.getRootDirectories()) { + copy(root, targetDir, FileCopyMode.EXTRACT, listener); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e); + } } @Override @@ -626,29 +643,7 @@ public void extractTar(Path file, Path targetDir, TarCompression compression) { @Override public void extractJar(Path file, Path targetDir) { - this.context.trace("Unpacking JAR {} to {}", file, targetDir); - try (JarInputStream jis = new JarInputStream(Files.newInputStream(file)); IdeProgressBar pb = getProgressbarForUnpacking( - getFileSize(file))) { - JarEntry entry; - while ((entry = jis.getNextJarEntry()) != null) { - Path entryPath = targetDir.resolve(entry.getName()).toAbsolutePath(); - - if (!entryPath.startsWith(targetDir)) { - throw new IOException("Preventing path traversal attack from " + entry.getName() + " to " + entryPath); - } - - if (entry.isDirectory()) { - Files.createDirectories(entryPath); - } else { - Files.createDirectories(entryPath.getParent()); - Files.copy(jis, entryPath); - } - pb.stepBy(entry.getCompressedSize()); - jis.closeEntry(); - } - } catch (IOException e) { - throw new IllegalStateException("Failed to extract JAR " + file + " to " + targetDir, e); - } + extractZip(file, targetDir); } /** @@ -661,7 +656,6 @@ public static String generatePermissionString(int permissions) { permissions &= 0b111111111; StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx"); - for (int i = 0; i < 9; i++) { int mask = 1 << i; char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-'; @@ -671,11 +665,13 @@ public static String generatePermissionString(int permissions) { return permissionStringBuilder.toString(); } - private void extractArchive(Path file, Path targetDir, Function unpacker) { + private void extractArchive(Path file, Path targetDir, Function> unpacker) { + + this.context.info("Extracting TAR file {} to {}", file, targetDir); + try (InputStream is = Files.newInputStream(file); + ArchiveInputStream ais = unpacker.apply(is); + IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) { - this.context.trace("Unpacking archive {} to {}", file, targetDir); - try (InputStream is = Files.newInputStream(file); ArchiveInputStream ais = unpacker.apply(is); IdeProgressBar pb = getProgressbarForUnpacking( - getFileSize(file))) { ArchiveEntry entry = ais.getNextEntry(); boolean isTar = ais instanceof TarArchiveInputStream; while (entry != null) { @@ -708,37 +704,10 @@ private void extractArchive(Path file, Path targetDir, Function filter, boolean recurs return null; } - /** - * @param sizeFile the size of archive - * @return prepared progressbar for unpacking - */ - private IdeProgressBar getProgressbarForUnpacking(long sizeFile) { - - return this.context.prepareProgressBar("Unpacking", sizeFile); - } - @Override public List listChildren(Path dir, Predicate filter) { @@ -892,15 +854,33 @@ public boolean isEmptyDir(Path dir) { return listChildren(dir, f -> true).isEmpty(); } - /** - * Gets the file size of a provided file path. - * - * @param path of the file. - * @return the file size. - */ - protected long getFileSize(Path path) { + private long getFileSize(Path file) { - return path.toFile().length(); + try { + return Files.size(file); + } catch (IOException e) { + this.context.warning(e.getMessage(), e); + return 0; + } + } + + private long getFileSizeRecursive(Path path) { + + long size = 0; + if (Files.isDirectory(path)) { + try (Stream childStream = Files.list(path)) { + Iterator iterator = childStream.iterator(); + while (iterator.hasNext()) { + Path child = iterator.next(); + size += getFileSizeRecursive(child); + } + } catch (IOException e) { + throw new RuntimeException("Failed to iterate children of folder " + path, e); + } + } else { + size += getFileSize(path); + } + return size; } @Override @@ -921,31 +901,31 @@ public Path findExistingFile(String fileName, List searchDirs) { @Override public void makeExecutable(Path filePath) { - if (SystemInfoImpl.INSTANCE.isWindows()) { - return; - } if (Files.exists(filePath)) { - // Read the current file permissions - Set perms; - try { - perms = Files.getPosixFilePermissions(filePath); - } catch (IOException e) { - throw new RuntimeException(e); + if (SystemInfoImpl.INSTANCE.isWindows()) { + this.context.trace("Windows does not have executable flags hence omitting for file {}", filePath); + return; } + try { + // Read the current file permissions + Set perms = Files.getPosixFilePermissions(filePath); - if (perms != null) { // Add execute permission for all users - perms.add(PosixFilePermission.OWNER_EXECUTE); - perms.add(PosixFilePermission.GROUP_EXECUTE); - perms.add(PosixFilePermission.OTHERS_EXECUTE); - - // Set the new permissions - try { + boolean update = false; + update |= perms.add(PosixFilePermission.OWNER_EXECUTE); + update |= perms.add(PosixFilePermission.GROUP_EXECUTE); + update |= perms.add(PosixFilePermission.OTHERS_EXECUTE); + + if (update) { + this.context.debug("Setting executable flags for file {}", filePath); + // Set the new permissions Files.setPosixFilePermissions(filePath, perms); - } catch (IOException e) { - throw new RuntimeException(e); + } else { + this.context.trace("Executable flags already present so no need to set them for file {}", filePath); } + } catch (IOException e) { + throw new RuntimeException(e); } } else { this.context.warning("Cannot set executable flag on file that does not exist: {}", filePath); diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java index cbf8d32bf..08686cbc2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java @@ -19,16 +19,16 @@ public enum FileCopyMode { /** Copy {@link #isRecursive() recursively} and override existing files but merge existing folders. */ COPY_TREE_OVERRIDE_FILES, - /** - * Copy {@link #isRecursive() recursively} and {@link FileAccess#delete(java.nio.file.Path) delete} the target-file if it exists before copying. - */ + /** Copy {@link #isRecursive() recursively} from virtual filesystem of a compressed archive and override existing files but merge existing folders. */ + EXTRACT, + + /** Copy {@link #isRecursive() recursively} and {@link FileAccess#delete(java.nio.file.Path) delete} the target-file if it exists before copying. */ COPY_TREE_OVERRIDE_TREE, - /** - * Copy {@link #isRecursive() recursively} and appends the file name to the target. - */ + /** Copy {@link #isRecursive() recursively} and append the file name to the target. */ COPY_TREE_CONTENT; + /** * @return {@code true} if only a single file shall be copied. Will fail if a directory is given to copy, {@code false} otherwise (to copy folders * recursively). @@ -54,8 +54,30 @@ public boolean isFailIfExists() { return (this == COPY_FILE_FAIL_IF_EXISTS) || (this == COPY_TREE_FAIL_IF_EXISTS); } + /** + * @return {@code true} to override existing files, {@code false} otherwise. + */ public boolean isOverrideFile() { return (this == COPY_FILE_OVERRIDE) || (this == COPY_TREE_OVERRIDE_FILES); } + + /** + * @return {@code true} if we copy from a virtual filesystem of a compressed archive. + */ + public boolean isExtract() { + + return (this == EXTRACT); + } + + /** + * @return the name of the operation (typically "copy" but may also be e.g. "extract"). + */ + public String getOperation() { + + if (isExtract()) { + return "extract"; + } + return "copy"; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java index 2370ca497..6d6612265 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java @@ -5,6 +5,36 @@ */ public interface IdeProgressBar extends AutoCloseable { + /** The {@link #getTitle() title} for extracting. */ + String TITLE_EXTRACTING = "Extracting"; + + /** The {@link #getTitle() title} for downloading. */ + String TITLE_DOWNLOADING = "Downloading"; + + /** The {@link #getTitle() title} for copying. */ + String TITLE_COPYING = "Copying"; + + /** + * @return the title (task name or activity) to display in the progress bar. + */ + String getTitle(); + + /** + * @return the maximum value when the progress bar has reached its end (100%) or {@code -1} if the maximum is undefined. + */ + long getMaxSize(); + + /** + * @return the name of the unit displayed to the end user (e.g. "files" or "MiB"). + */ + String getUnitName(); + + /** + * @return the size of a single unit (e.g. 1 if the {@link #stepBy(long) reported progress} and {@link #getMaxSize() max size} numbers remain unchanged or 1000 for + * "kilo" or 1000000 for "mega" in order to avoid displaying too long numbers). + */ + long getUnitSize(); + /** * Increases the progress bar by given step size. * @@ -13,12 +43,7 @@ public interface IdeProgressBar extends AutoCloseable { void stepBy(long stepSize); /** - * @return the maximum value when the progress bar has reached its end or {@code -1} if the maximum is undefined. - */ - long getMaxLength(); - - /** - * @return the total count accumulated with {@link #stepBy(long)} or {@link #getMaxLength()} in case of overflow. + * @return the total count accumulated with {@link #stepBy(long)} or {@link #getMaxSize()} in case of overflow. */ long getCurrentProgress(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java index d89a74c55..9223f6239 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java @@ -18,15 +18,16 @@ public class IdeProgressBarConsole extends AbstractIdeProgressBar { * The constructor. * * @param systemInfo the {@link SystemInfo}. - * @param taskName the {@link ProgressBar} to initialize. + * @param title the title (task name or activity) to display in the progress bar. * @param maxSize the maximum size of the progress bar. + * @param unitName the name of the unit to display in the progress bar. + * @param unitSize the size of the unit (e.g. 1000 for kilo, 1000000 for mega). */ - public IdeProgressBarConsole(SystemInfo systemInfo, String taskName, long maxSize) { - - super(maxSize); + public IdeProgressBarConsole(SystemInfo systemInfo, String title, long maxSize, String unitName, long unitSize) { + super(title, maxSize, unitName, unitSize); this.systemInfo = systemInfo; - this.progressBar = createProgressBar(taskName, maxSize); + this.progressBar = createProgressBar(); } /** @@ -34,17 +35,10 @@ public IdeProgressBarConsole(SystemInfo systemInfo, String taskName, long maxSiz */ protected ProgressBar getProgressBar() { - return progressBar; + return this.progressBar; } - /** - * Creates the {@link ProgressBar} initializes task name and maximum size as well as the behaviour and style. - * - * @param taskName name of the task. - * @param size of the content. - * @return {@link ProgressBar} to use. - */ - protected ProgressBar createProgressBar(String taskName, long size) { + private ProgressBar createProgressBar() { ProgressBarBuilder pbb = new ProgressBarBuilder(); String leftBracket, rightBracket, fractionSymbols; @@ -66,18 +60,18 @@ protected ProgressBar createProgressBar(String taskName, long size) { .rightBracket(rightBracket).block(block).space(' ').fractionSymbols(fractionSymbols).rightSideFractionSymbol(' ') .build()); - pbb.setUnit("MiB", 1048576); - if (size <= 0) { - pbb.setTaskName(taskName + " (unknown size)"); + pbb.setUnit(this.unitName, this.unitSize); + if (this.maxSize <= 0) { + pbb.setTaskName(this.title + " (unknown size)"); pbb.setInitialMax(-1); pbb.hideEta(); } else { - pbb.setTaskName(taskName); + pbb.setTaskName(this.title); pbb.showSpeed(); - pbb.setInitialMax(size); + pbb.setInitialMax(this.maxSize); } pbb.continuousUpdate(); - pbb.setUpdateIntervalMillis(1); + pbb.setUpdateIntervalMillis(100); return pbb.build(); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java b/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java new file mode 100644 index 000000000..442e3abe9 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java @@ -0,0 +1,27 @@ +package com.devonfw.tools.ide.io; + +import java.nio.file.Path; + +/** + * Interface for a listener of {@link Path} while {@link FileAccess#copy(Path, Path, FileCopyMode, PathCopyListener) copying}. + * + * @see FileAccess#copy(Path, Path, FileCopyMode, PathCopyListener) + */ +@FunctionalInterface +public interface PathCopyListener { + + /** + * An empty {@link PathCopyListener} instance doing nothing. + */ + static PathCopyListener NONE = (s, t, d) -> { + }; + + /** + * @param source the {@link Path} of the source to copy. + * @param target the {@link Path} of the copied target. + * @param directory - {@code true} in case of {@link java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) directory}, {@code false} + * otherwise (regular file). + */ + void onCopy(Path source, Path target, boolean directory); + +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java index 6070241b0..39a0f0016 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java @@ -122,8 +122,8 @@ private static List assertProgressEventsAn List eventList = progressBar.getEventList(); assertThat(eventList).hasSize(chunkCount + 1); // extra case for unknown file size (indefinite progress bar) - if (progressBar.getMaxLength() != -1L) { - assertThat(progressBar.getMaxLength()).isEqualTo(maxSize); + if (progressBar.getMaxSize() != -1L) { + assertThat(progressBar.getMaxSize()).isEqualTo(maxSize); } return eventList; } diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java index de3c1d66f..35059a819 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java @@ -108,12 +108,12 @@ public Map getProgressBarMap() { } @Override - public IdeProgressBar prepareProgressBar(String taskName, long size) { + public IdeProgressBar newProgressBar(String title, long maxSize, String unitName, long unitSize) { - IdeProgressBarTestImpl progressBar = new IdeProgressBarTestImpl(taskName, size); - IdeProgressBarTestImpl duplicate = this.progressBarMap.put(taskName, progressBar); + IdeProgressBarTestImpl progressBar = new IdeProgressBarTestImpl(title, maxSize, unitName, unitSize); + IdeProgressBarTestImpl duplicate = this.progressBarMap.put(title, progressBar); // If we have multiple downloads or unpacking, we may have an existing "Downloading" or "Unpacking" key - assert (taskName.equals("Downloading")) || (taskName.equals("Unpacking")) || duplicate == null; + assert (title.equals(IdeProgressBar.TITLE_DOWNLOADING)) || (title.equals(IdeProgressBar.TITLE_EXTRACTING)) || duplicate == null; return progressBar; } diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java index e8724d6fc..cf5fd149e 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java @@ -478,7 +478,7 @@ public void testUnzip(@TempDir Path tempDir) { // act context.getFileAccess() - .extractZip(Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.zip"), + .extractZip(Path.of("src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip"), tempDir); // assert diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java index 206d6935f..598137a96 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java @@ -17,8 +17,7 @@ public class IdeProgressBarConsoleTest extends AbstractIdeContextTest { private IdeProgressBarConsole newProgressBar(long maxSize) { SystemInfo systemInfo = SystemInfoImpl.INSTANCE; - IdeProgressBarConsole progressBarConsole = new IdeProgressBarConsole(systemInfo, "downloading", maxSize); - return progressBarConsole; + return new IdeProgressBarConsole(systemInfo, IdeProgressBar.TITLE_DOWNLOADING, maxSize, "MB", 1_000_000); } @Test @@ -67,7 +66,7 @@ public void testProgressBarMaxSizeKnownStepBy() throws Exception { // assert assertThat(progressBarConsole.getProgressBar().isIndefinite()).isEqualTo(false); - assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize); + assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize); assertThat(progressBarConsole.getCurrentProgress()).isEqualTo(maxSize); } @@ -83,7 +82,7 @@ public void testProgressBarMaxSizeKnownDoStepTo() throws Exception { // assert assertThat(progressBarConsole.getProgressBar().isIndefinite()).isEqualTo(false); - assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize); + assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize); assertThat(progressBarConsole.getCurrentProgress()).isEqualTo(maxSize); } @@ -100,7 +99,7 @@ public void testProgressBarMaxSizeKnownIncompleteSteps() throws Exception { // act progressBarConsole.close(); // assert - assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize); + assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize); } @Test diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java index ed424386f..d7c440fa5 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java @@ -68,17 +68,12 @@ public void testProgressBarDownloadWithMissingContentLength(@TempDir Path tempDi //assert assertUnknownProgressBar(context, "Downloading", MAX_LENGTH); - checkLogMessageForMissingContentLength(context, testUrl); + assertThat(context).logAtWarning().hasMessage( + "Content-Length was not provided by download from " + testUrl); assertThat(tempDir.resolve("windows_x64_url.tgz")).exists(); IdeProgressBarTestImpl progressBar = context.getProgressBarMap().get(taskName); - assertThat(progressBar.getMaxLength()).isEqualTo(-1); - } - - private void checkLogMessageForMissingContentLength(IdeTestContext context, String source) { - - assertThat(context).logAtWarning().hasMessage( - "Content-Length was not provided by download/copy source: " + source + "."); + assertThat(progressBar.getMaxSize()).isEqualTo(-1); } /** @@ -108,6 +103,6 @@ public void testProgressBarCopyWithKnownFileSize(@TempDir Path tempDir) { assertProgressBar(context, "Copying", maxSize); assertThat(tempDir.resolve("windows_x64_url.tgz")).exists(); IdeProgressBarTestImpl progressBar = context.getProgressBarMap().get(taskName); - assertThat(progressBar.getMaxLength()).isEqualTo(maxSize); + assertThat(progressBar.getMaxSize()).isEqualTo(maxSize); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java index 64ca4c286..89b24892d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java @@ -15,9 +15,6 @@ public class IdeProgressBarTestImpl extends AbstractIdeProgressBar { /** Ending time of an {@link IdeProgressBar}. */ private Instant end; - /** The task name of an {@link IdeProgressBar}. */ - private final String name; - /** The total span of an {@link IdeProgressBar}. */ private long total; @@ -27,13 +24,14 @@ public class IdeProgressBarTestImpl extends AbstractIdeProgressBar { /** * The constructor. * - * @param name the task name. - * @param max maximum length of the bar. + * @param title the {@link #getTitle() title}. + * @param maxSize the {@link #getMaxSize() maximum size}. + * @param unitName the {@link #getUnitName() unit name}. + * @param unitSize the {@link #getUnitSize() unit size}. */ - public IdeProgressBarTestImpl(String name, long max) { - super(max); + public IdeProgressBarTestImpl(String title, long maxSize, String unitName, long unitSize) { + super(title, maxSize, unitName, unitSize); this.start = Instant.now(); - this.name = name; this.eventList = new ArrayList<>(); } @@ -56,8 +54,8 @@ public void close() { this.end = Instant.now(); } - if (getMaxLength() != -1) { - assert this.total == getMaxLength(); + if (getMaxSize() != -1) { + assert this.total == getMaxSize(); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java index e2ac4eabd..3e306bcee 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java @@ -82,7 +82,7 @@ private void checkInstallation(IdeTestContext context) { assertThat(context.getSoftwarePath().resolve("android-studio/.ide.software.version")).exists().hasContent("2024.1.1.1"); assertThat(context).logAtSuccess().hasEntries("Successfully ended step 'Install plugin MockedPlugin'.", // "Successfully installed android-studio in version 2024.1.1.1"); - assertThat(context.getPluginsPath().resolve("android-studio").resolve("mockedPlugin").resolve("MockedClass.class")).exists(); + assertThat(context.getPluginsPath().resolve("android-studio").resolve("mockedPlugin").resolve("dev").resolve("MockedClass.class")).exists(); } private void setupMockedPlugin(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java index c57b4cd30..4e397634d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java @@ -143,7 +143,7 @@ private void checkInstallation(IdeTestContext context) { assertThat(context).logAtSuccess().hasEntries("Successfully installed java in version 17.0.10_7", "Successfully installed intellij in version 2023.3.3"); assertThat(context).logAtSuccess().hasMessage("Successfully ended step 'Install plugin MockedPlugin'."); - assertThat(context.getPluginsPath().resolve("intellij").resolve("mockedPlugin").resolve("MockedClass.class")).exists(); + assertThat(context.getPluginsPath().resolve("intellij").resolve("mockedPlugin").resolve("dev").resolve("MockedClass.class")).exists(); } private void setupMockedPlugin(WireMockRuntimeInfo wmRuntimeInfo, boolean mockedPluginActive) throws IOException {