blob: 66882b4b28715991d95113b475a9b90b46e68ac6 [file] [log] [blame]
/*
* 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);
}
}