| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.janitor; |
| |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.temporal.ChronoUnit; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.WeakHashMap; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.prefs.Preferences; |
| import javax.swing.Icon; |
| import org.netbeans.api.annotations.common.StaticResource; |
| import org.netbeans.api.progress.ProgressHandle; |
| import org.openide.*; |
| import org.openide.awt.Notification; |
| import org.openide.awt.NotificationDisplayer; |
| import org.openide.modules.Places; |
| import org.openide.util.*; |
| import org.openide.util.NbBundle.Messages; |
| import org.openide.windows.OnShowing; |
| |
| /** |
| * |
| * @author Laszlo Kishalmi |
| */ |
| @Messages({ |
| "# {0} - is the user directory name", |
| "# {1} - the days of abandonement", |
| "# {2} - the disk space can be reclaimed (in megabytes)", |
| "TIT_ABANDONED_USERDIR=NetBeans {0} was last used {1} days ago.", |
| |
| "# {0} - the name of the abandoned cache dir.", |
| "# {1} - the disk space can be reclaimed (in megabytes)", |
| "TIT_ABANDONED_CACHEDIR=NetBeans cache directory {0} seems to be abandoned.", |
| |
| "# {0} - is the user directory name", |
| "# {1} - the days of abandonement", |
| "# {2} - the disk space can be reclaimed (in megabytes)", |
| "DESC_ABANDONED_USERDIR=Remove unused data and cache directories of NetBeans {0}. " |
| + "Free up {2} MB of disk space.", |
| |
| "# {0} - is the cache directory name", |
| "# {1} - the disk space can be reclaimed (in megabytes)", |
| "DESC_ABANDONED_CACHEDIR=NetBeans could not find a user dir for cache dir {0}, so it is probably abandoned. " |
| + "Remove abandoned cache dir, " |
| + "free up {1} MB of disk space.", |
| |
| "TIT_CONFIRM_CLEANUP=Confirm Cleanup", |
| |
| "# {0} - the dirname to be cleaned up", |
| "TXT_CONFIRM_CLEANUP=Remove user and cache data for NetBeans {0}?", |
| |
| "# {0} - the dirname to be cleaned up", |
| "TXT_CONFIRM_CACHE_CLEANUP=Remove abandoned cache dir?", |
| |
| "# {0} - the dirname to be cleaned up", |
| "LBL_CLEANUP=Removing unused/abandoned user and/or cache dirs." |
| }) |
| public class Janitor { |
| |
| private static final int UNUSED_DAYS = 30; |
| |
| public static final String PROP_JANITOR_ENABLED = "janitorEnabled"; //NOI18N |
| public static final String PROP_UNUSED_DAYS = "UnusedDays"; //NOI18N |
| |
| private static final String LOGFILE_NAME = "var/log/messages.log"; //NOI18N |
| private static final String ALL_CHECKSUM_NAME = "lastModified/all-checksum.txt"; //NOI18N |
| @StaticResource |
| private static final String CLEAN_ICON = "org/netbeans/modules/janitor/resources/clean.gif"; //NOI18N |
| |
| static final RequestProcessor JANITOR_RP = new RequestProcessor("janitor", 1); //NOI18N |
| static final Map<ActionListener, Notification> CLEANUP_TASKS = new WeakHashMap<>(); |
| static final Runnable SCAN_FOR_JUNK = () -> { |
| // Remove previously opened notifications |
| CLEANUP_TASKS.values().forEach((nf) -> nf.clear()); |
| CLEANUP_TASKS.clear(); |
| |
| Icon clean = ImageUtilities.loadImageIcon(CLEAN_ICON, false); |
| List<Pair<String, Integer>> otherVersions = getOtherVersions(); |
| |
| for (Pair<String, Integer> ver : otherVersions) { |
| String name = ver.first(); |
| Integer age = ver.second(); |
| long toFree = size(getUserDir(name)) + size(getCacheDir(name)); |
| toFree = toFree / (1_000_000) + 1; |
| ActionListener cleanupListener; |
| Notification nf; |
| if (getUserDir(name) != null) { |
| cleanupListener = cleanupAction(name, Bundle.TXT_CONFIRM_CLEANUP(name)); |
| nf = NotificationDisplayer.getDefault().notify( |
| Bundle.TIT_ABANDONED_USERDIR(name, age, toFree), |
| clean, |
| Bundle.DESC_ABANDONED_USERDIR(name, age, toFree), |
| cleanupListener); |
| |
| } else { |
| cleanupListener = cleanupAction(name, Bundle.TXT_CONFIRM_CACHE_CLEANUP(name)); |
| nf = NotificationDisplayer.getDefault().notify( |
| Bundle.TIT_ABANDONED_CACHEDIR(name, toFree), |
| clean, |
| Bundle.DESC_ABANDONED_CACHEDIR(name, toFree), |
| cleanupListener); |
| } |
| CLEANUP_TASKS.put(cleanupListener, nf); |
| } |
| }; |
| |
| static ActionListener cleanupAction(String name, String label) { |
| return new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent evt) { |
| JanitorPanel panel = new JanitorPanel(label); |
| DialogDescriptor descriptor = new DialogDescriptor( |
| panel, |
| Bundle.TIT_CONFIRM_CLEANUP(), |
| true, |
| DialogDescriptor.YES_NO_OPTION, |
| DialogDescriptor.YES_OPTION, |
| null |
| ); |
| if (DialogDescriptor.YES_OPTION == DialogDisplayer.getDefault().notify(descriptor)) { |
| JANITOR_RP.post(() -> { |
| try (ProgressHandle handle = ProgressHandle.createHandle(Bundle.LBL_CLEANUP(name))){ |
| handle.start(); |
| deleteDir(getUserDir(name)); |
| deleteDir(getCacheDir(name)); |
| } |
| }); |
| } |
| Janitor.setEnabled(panel.isEnabledOnStartup()); |
| Notification nf = CLEANUP_TASKS.get(this); |
| if (nf != null) { |
| nf.clear(); |
| } |
| } |
| }; |
| } |
| |
| public static final Preferences getPreferences() { |
| return NbPreferences.forModule(Janitor.class); |
| } |
| |
| @OnShowing |
| public static final class PlatformOpenHook implements Runnable { |
| |
| @Override |
| public void run() { |
| if (isEnabled()) { |
| // Starting delayed, not to interfere with other startup IO operations |
| JANITOR_RP.post(SCAN_FOR_JUNK, 60_000); |
| } |
| } |
| |
| } |
| |
| static void runNow() { |
| JANITOR_RP.post(SCAN_FOR_JUNK); |
| } |
| |
| static File getUserDir(String version) { |
| File ret = null; |
| File userDir = Places.getUserDirectory(); |
| if (userDir != null) { |
| ret = new File(userDir.getParentFile(), version); |
| ret = ret.isDirectory() ? ret : null; |
| } |
| |
| return ret; |
| } |
| |
| static File getCacheDir(String version) { |
| File ret = null; |
| File cacheDir = Places.getCacheDirectory(); |
| if (cacheDir != null) { |
| ret = new File(cacheDir.getParentFile(), version); |
| ret = ret.isDirectory() ? ret : null; |
| } |
| return ret; |
| } |
| |
| static void deleteDir(File dir) { |
| if ((dir == null) || !dir.exists()) return; |
| Path path = dir.toPath(); |
| try { |
| Files.walkFileTree(path, new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
| Files.delete(file); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { |
| Files.delete(dir); |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } catch(IOException ex) { |
| // Well we've tried |
| } |
| } |
| |
| public static long size(File f) { |
| |
| if (f == null) { |
| return 0; |
| } |
| final Path path = f.toPath(); |
| final AtomicLong size = new AtomicLong(0); |
| |
| try { |
| Files.walkFileTree(path, new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { |
| size.addAndGet(attrs.size()); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult visitFileFailed(Path file, IOException exc) { |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult postVisitDirectory(Path dir, IOException exc) { |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } catch (IOException e) { |
| } |
| |
| return size.get(); |
| } |
| |
| static List<Pair<String, Integer>> getOtherVersions() { |
| File userDir = Places.getUserDirectory(); |
| List<Pair<String, Integer>> ret = new LinkedList<>(); |
| Set<String> availableUserDirs = new HashSet<>(); |
| Instant now = Instant.now(); |
| if (userDir != null) { |
| File userParent = userDir.getParentFile(); |
| for (File f : userParent.listFiles()) { |
| availableUserDirs.add(f.getName()); |
| Path logFile = new File(f, LOGFILE_NAME).toPath(); |
| if (!f.equals(userDir) && Files.isRegularFile(logFile)) { |
| try { |
| Instant lastModified = Files.getLastModifiedTime(logFile).toInstant(); |
| Integer age = (int) Duration.between(lastModified, now).toDays(); |
| if (lastModified.plus(getUnusedDays(), ChronoUnit.DAYS).isBefore(now)) { |
| ret.add(Pair.of(f.getName(), age)); |
| } |
| } catch (IOException ex) { |
| //Just ignore what we can't process |
| } |
| } |
| } |
| } |
| |
| //Search for abandoned cache dirs (cache dirs with no user dir) |
| File cacheDir = Places.getCacheDirectory(); |
| if (cacheDir != null) { |
| File cacheParent = cacheDir.getParentFile(); |
| for (File f : cacheParent.listFiles()) { |
| if (f.isDirectory() && !availableUserDirs.contains(f.getName())) { |
| if (new File(f, ALL_CHECKSUM_NAME).exists() && !cacheDir.equals(f)) { |
| try { |
| Instant lastModified = Files.getLastModifiedTime(f.toPath()).toInstant(); |
| Integer age = (int) Duration.between(lastModified, now).toDays(); |
| ret.add(Pair.of(f.getName(), age)); |
| } catch (IOException ex) { |
| //Just ignore what we can't process |
| } |
| } |
| } |
| } |
| } |
| return ret; |
| } |
| |
| static void setEnabled(boolean b) { |
| getPreferences().putBoolean(PROP_JANITOR_ENABLED, b); |
| } |
| |
| static boolean isEnabled() { |
| return getPreferences().getBoolean(PROP_JANITOR_ENABLED, true); |
| } |
| |
| static void setUnusedDays(int days) { |
| getPreferences().putInt(PROP_UNUSED_DAYS, days); |
| } |
| |
| static int getUnusedDays() { |
| return getPreferences().getInt(PROP_UNUSED_DAYS, UNUSED_DAYS); |
| } |
| |
| } |