-
Notifications
You must be signed in to change notification settings - Fork 89
#2372: Add --retention-delay option to ide cleanup #2378
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,14 +2,21 @@ | |
|
|
||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.time.Duration; | ||
| import java.time.format.DateTimeParseException; | ||
| import java.util.ArrayList; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import com.devonfw.tools.ide.cli.CliException; | ||
| import com.devonfw.tools.ide.commandlet.Commandlet; | ||
| import com.devonfw.tools.ide.context.IdeContext; | ||
| import com.devonfw.tools.ide.log.IdeLogLevel; | ||
| import com.devonfw.tools.ide.property.StringProperty; | ||
| import com.devonfw.tools.ide.step.Step; | ||
| import com.devonfw.tools.ide.tool.mvn.MvnRepository; | ||
| import com.devonfw.tools.ide.tool.repository.ToolRepository; | ||
|
|
@@ -21,6 +28,12 @@ public class CleanupCommandlet extends Commandlet { | |
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); | ||
|
|
||
| /** The default retention period of stale files. Stale files are considered stale after 1 year (365 days) of inactivity. */ | ||
| public static final Duration DEFAULT_RETENTION_DELAY = Duration.ofDays(365); | ||
|
|
||
| /** The {@link StringProperty} of the {@code --retention-delay} option. */ | ||
| private final StringProperty retentionDelay; | ||
|
|
||
| /** | ||
| * Constructor. | ||
| * | ||
|
|
@@ -30,6 +43,7 @@ public CleanupCommandlet(IdeContext context) { | |
|
|
||
| super(context); | ||
| addKeyword(getName()); | ||
| this.retentionDelay = add(new StringProperty("--retention-delay", false, null)); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -49,21 +63,59 @@ protected void doRun() { | |
|
|
||
| LOG.debug("Start cleanup commandlet"); | ||
|
|
||
| Duration retentionDelay = getRetentionDelay(); | ||
|
|
||
| InstalledSoftware installedSoftware = new InstalledSoftware(); | ||
|
|
||
| Step step = this.context.newStep("Identify unused software"); | ||
| step.run(() -> discoverUnusedSoftware(installedSoftware), true); | ||
|
|
||
| logSoftwareToBeDeleted(installedSoftware.getTools()); | ||
|
|
||
| if (hasSoftwareToDelete(installedSoftware.getTools())) { | ||
| List<Path> staleRoots = new ArrayList<>(); | ||
| List<Path> staleFiles = new ArrayList<>(); | ||
| if (this.context.getIdeHome() != null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This if-guard prevents any clean-up from happening, however _ide/tmp and the download cache cleanup would work when IDE_HOME is null. I'm not sure if this intentional, but you might want to change that if it's not. |
||
| Step staleStep = this.context.newStep("Identify stale files"); | ||
| staleStep.run(() -> { | ||
| staleRoots.addAll(getStaleFileRoots()); | ||
| discoverStaleFiles(staleRoots, staleFiles, retentionDelay); | ||
| }, true); | ||
| logStaleFilesToBeDeleted(staleFiles, retentionDelay); | ||
| } | ||
|
|
||
| boolean hasStaleFiles = !staleFiles.isEmpty(); | ||
| if (hasSoftwareToDelete(installedSoftware.getTools()) || hasStaleFiles) { | ||
| this.context.askToContinue("Do you want to continue?"); | ||
| deleteUnusedSoftware(installedSoftware.getTools()); | ||
| if (hasStaleFiles) { | ||
| deleteStaleFiles(staleFiles, staleRoots); | ||
| } | ||
| } | ||
|
|
||
| LOG.debug("Finished cleanup commandlet"); | ||
| } | ||
|
|
||
| /** | ||
| * Determines the retention delay to use. | ||
| * | ||
| * @return the retention delay, {@link #DEFAULT_RETENTION_DELAY} if the {@code --retention-delay} option was not provided. | ||
| * @throws CliException if the provided value is not a valid ISO-8601 time-based duration. | ||
| */ | ||
| private Duration getRetentionDelay() { | ||
|
|
||
| String value = this.retentionDelay.getValueAsString(); | ||
| if (value == null) { | ||
| return DEFAULT_RETENTION_DELAY; | ||
| } | ||
| try { | ||
| return Duration.parse(value); | ||
| } catch (DateTimeParseException e) { | ||
| throw new CliException( | ||
| "Invalid value '" + value + "' for --retention-delay. Please provide a time-based ISO-8601 duration such as P30D or PT2H30M.", | ||
| e); | ||
| } | ||
| } | ||
|
Comment on lines
+104
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duration.parse() legally accepts negative and zero values. This means it won't throw the CliException and later down the line in isStale() every file under all roots becomes stale, which leads to every file being deleted. You should add a positivity check here. |
||
|
|
||
| /** | ||
| * Discovers installed and unused software. | ||
| * | ||
|
|
@@ -356,4 +408,148 @@ private int deleteFolder(Path path) { | |
| } | ||
| return 0; | ||
| } | ||
|
|
||
| /** | ||
| * Discovers stale files, i.e. files that have not been modified within the given retention delay, in the given root folders. | ||
| * | ||
| * @param roots the folders to scan. | ||
| * @param staleFiles the list to populate with the stale files. | ||
| * @param retentionDelay the age after which a file is considered stale. | ||
| */ | ||
| private void discoverStaleFiles(List<Path> roots, List<Path> staleFiles, Duration retentionDelay) { | ||
|
|
||
| for (Path root : roots) { | ||
| discoverStaleFilesRecursive(root, retentionDelay, staleFiles); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Recursively collects the stale files below the given folder, i.e. all files that are older than the retention delay. | ||
| * | ||
| * @param folder the folder to scan. | ||
| * @param retentionDelay the age after which a file is considered stale. | ||
| * @param staleFiles the list to populate with the stale files. | ||
| */ | ||
| private void discoverStaleFilesRecursive(Path folder, Duration retentionDelay, List<Path> staleFiles) { | ||
|
|
||
| if (!Files.isDirectory(folder)) { | ||
| return; | ||
| } | ||
|
|
||
| for (Path child : this.context.getFileAccess().listChildren(folder, child -> true)) { | ||
| if (Files.isDirectory(child)) { | ||
| discoverStaleFilesRecursive(child, retentionDelay, staleFiles); | ||
| } else if (isStale(child, retentionDelay)) { | ||
| staleFiles.add(child); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+433
to
+446
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Files.isDirectory(child) follows links. If a symlink inside any root (e.g. ~/Downloads/ide/work -> C:\data) makes the scan descend outside the roots and delete the target's stale files, which would be a huge problem. Furthermore a self-referencing link causes unbounded recursion. You need to check for link children here and skip these. This also destroyed my IDEasy installation and I'm not sure how yours didn't get destroyed when you tested your work. |
||
|
|
||
| /** | ||
| * Determines the folders scanned for stale files: the IDEasy updates folder, the temporary folder, the download cache and the legacy download | ||
| * cache under {@code ~/Downloads/ide}. | ||
| * | ||
| * @return the list of root folders to scan, excluding folders that do not exist. | ||
| */ | ||
| private List<Path> getStaleFileRoots() { | ||
|
|
||
| List<Path> roots = new ArrayList<>(); | ||
|
|
||
| addStaleFileRoot(roots, this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES)); | ||
| addStaleFileRoot(roots, this.context.getTempPath()); | ||
| Path downloadPath = this.context.getDownloadPath(); | ||
| addStaleFileRoot(roots, downloadPath); | ||
|
|
||
| // older versions kept the download cache under ~/Downloads/ide - scan that location too if it is distinct and still exists | ||
| Path legacy = this.context.getUserHome().resolve(IdeContext.FOLDER_DOWNLOADS).resolve("ide"); | ||
| if (!legacy.equals(downloadPath)) { | ||
| addStaleFileRoot(roots, legacy); | ||
| } | ||
|
|
||
| return roots; | ||
| } | ||
|
|
||
| /** | ||
| * Adds the given folder to the list of scanned roots if it exists. | ||
| * | ||
| * @param roots the list of root folders to populate. | ||
| * @param root the candidate root folder. | ||
| */ | ||
| private void addStaleFileRoot(List<Path> roots, Path root) { | ||
|
|
||
| if (root != null && Files.exists(root)) { | ||
| roots.add(root); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the given file is older than the given retention delay. | ||
| * | ||
| * @param file the file to check. | ||
| * @param retentionDelay the age after which the file is considered stale. | ||
| * @return {@code true} if the file exists and is older than the retention delay. | ||
| */ | ||
| private boolean isStale(Path file, Duration retentionDelay) { | ||
|
|
||
| Duration age = this.context.getFileAccess().getFileAge(file); | ||
| return (age != null) && age.compareTo(retentionDelay) > 0; | ||
| } | ||
|
|
||
| /** | ||
| * Logs a summary of the stale files to be deleted. | ||
| * | ||
| * @param staleFiles the stale files to report. | ||
| * @param retentionDelay the age that the stale files exceed. | ||
| */ | ||
| private void logStaleFilesToBeDeleted(List<Path> staleFiles, Duration retentionDelay) { | ||
|
|
||
| if (staleFiles.isEmpty()) { | ||
| LOG.info("No stale files older than {} will be deleted.", retentionDelay); | ||
| } else { | ||
| for (Path staleFile : staleFiles) { | ||
| LOG.info("\t - {} will be deleted", staleFile); | ||
| } | ||
| LOG.info("Summary: {} stale file(s) older than {} will be deleted.", staleFiles.size(), retentionDelay); | ||
| } | ||
| } | ||
|
Comment on lines
+504
to
+514
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to format the duration human-readably. |
||
|
|
||
| /** | ||
| * Deletes the given stale files and removes the parent folders that became empty as a result. Folders above the scanned roots are never removed. | ||
| * | ||
| * @param staleFiles the stale files to delete. | ||
| * @param roots the folders that were scanned, which must not be removed themselves. | ||
| */ | ||
| private void deleteStaleFiles(List<Path> staleFiles, List<Path> roots) { | ||
|
|
||
| int failedDeletion = 0; | ||
|
|
||
| for (Path staleFile : staleFiles) { | ||
| if (Files.exists(staleFile)) { | ||
| LOG.debug("Deleting stale file {}", staleFile); | ||
| failedDeletion += deleteFolder(staleFile); | ||
| } | ||
| } | ||
|
|
||
| // Remove the folders that became empty after deleting the stale files, walking up from each deleted file but never removing the scanned roots | ||
| // themselves. | ||
| Set<Path> prunedFolders = new HashSet<>(); | ||
| for (Path staleFile : staleFiles) { | ||
| Path folder = staleFile.getParent(); | ||
| while (folder != null && !prunedFolders.contains(folder) && !roots.contains(folder)) { | ||
| if (!isEmptyFolder(folder)) { | ||
| break; | ||
| } | ||
| LOG.debug("Deleting empty folder {}", folder); | ||
| prunedFolders.add(folder); | ||
| failedDeletion += deleteFolder(folder); | ||
| folder = folder.getParent(); | ||
| } | ||
| } | ||
|
|
||
| if (failedDeletion > 0) { | ||
| LOG.warn("Stale files have been deleted.\nFailed to delete {} file(s) or folder(s). Please check the log for details.", failedDeletion); | ||
| } else { | ||
| IdeLogLevel.SUCCESS.log(LOG, "Stale files have been deleted successfully."); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,7 @@ cmd.claude=Tool commandlet for Claude Code CLI. | |||||
| cmd.claude.detail=Claude Code CLI is a command-line interface for interacting with the Claude AI assistant. Detailed documentation can be found at https://code.claude.com/docs/en/overview | ||||||
| cmd.cleanup=Commandlet to clean up the IDEasy installation by uninstalling all unused tools. | ||||||
| cmd.cleanup.detail=This will remove any installed tools that are currently not in use by an IDEasy project. Before anything is deleted you are asked for confirmation. Run "ide -b -f cleanup" to skip the confirmation. | ||||||
|
Comment on lines
14
to
15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be updated to match the new functionality. |
||||||
| cmd.cleanup.opt.--retention-delay=the retention period of files in the 'updates', '_ide/tmp' and 'Downloads/ide' folders, i.e. files that were not modified within this period are deleted as stale. A time-based ISO-8601 duration (e.g. 'P30D' for 30 days or 'PT2H30M' for 2 hours and 30 minutes). Defaults to 1 year (365 days) if not provided. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Furthermore the folders you mention here are correct for Windows and Linux, however on macOS there are somewhere else, maybe just remove the path descriptions and describe the folders? |
||||||
| cmd.complete=Internal commandlet for bash auto-completion. | ||||||
| cmd.complete.detail=Run 'ide complete <args>' to activate the non-interactive autocompletion, replace <args> with the arguments you want to autocomplete.\nE.g. type: 'ide complete in' to get 'install' and 'intellij' suggestions. | ||||||
| cmd.copilot=Tool commandlet for GitHub Copilot CLI. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,7 @@ cmd.claude=Werkzeug Kommando für Claude Code CLI. | |||||
| cmd.claude.detail=Claude Code CLI ist ein KI-gestützter Programmierassistent, der über die Befehlszeile ausgeführt wird. Detaillierte Dokumentation ist zu finden unter https://code.claude.com/docs/de/overview | ||||||
| cmd.cleanup=Werkzeug zum Aufräumen der IDEasy-Installation durch Deinstallieren aller ungenutzten Werkzeuge. | ||||||
| cmd.cleanup.detail=Dies wird alle installierten Werkzeuge entfernen, die derzeit von keinem IDEasy-Projekt verwendet werden. Bevor etwas gelöscht wird, wirst du um Bestätigung gebeten. Führe "ide -b -f cleanup" aus, um die Bestätigung zu überspringen. | ||||||
|
Comment on lines
14
to
15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be updated to match the new functionality. |
||||||
| cmd.cleanup.opt.--retention-delay=die Aufbewahrungsdauer von Dateien in den Ordnern 'updates', '_ide/tmp' und 'Downloads/ide', d.h. Dateien, die innerhalb dieses Zeitraums nicht modifiziert wurden, werden als veraltet gelöscht. Eine zeitbasierte ISO-8601-Dauer (z. B. 'P30D' für 30 Tage oder 'PT2H30M' für 2 Stunden und 30 Minuten). Standardmäßig 1 Jahr (365 Tage), wenn nicht angegeben. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Furthermore the folders you mention here are correct for Windows and Linux, however on macOS there are somewhere else, maybe just remove the path descriptions and describe the folders? |
||||||
| cmd.complete=Internes Werkzeug für bash Autovervollständigung. | ||||||
| cmd.complete.detail=Geben Sie 'ide complete <ausdruck>' in die Konsole ein um die einfache Autovervollständigung zu aktivieren, ersetzen Sie <ausdruck> mit dem Ausdruck, der automatisch vervollständigt werden soll.\nZ.B. geben Sie einfach 'ide complete in' in die Konsole ein um 'install' und 'intellij' als Vorschläge zu erhalten. | ||||||
| cmd.copilot=Werkzeug Kommando für GitHub Copilot CLI. | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to rename this since you already have a StringProperty called retentionDelay in the class.