1 /*******************************************************************************
2 * Copyright (c) 2017 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * Semantum Oy - #7330 - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.utils.datastructures.file;
14 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
15 import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
16 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
17 import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
19 import java.io.Closeable;
20 import java.io.IOException;
21 import java.nio.file.FileSystems;
22 import java.nio.file.FileVisitResult;
23 import java.nio.file.FileVisitor;
24 import java.nio.file.Files;
25 import java.nio.file.NoSuchFileException;
26 import java.nio.file.Path;
27 import java.nio.file.SimpleFileVisitor;
28 import java.nio.file.WatchEvent;
29 import java.nio.file.WatchEvent.Kind;
30 import java.nio.file.WatchKey;
31 import java.nio.file.WatchService;
32 import java.nio.file.attribute.BasicFileAttributes;
34 import java.util.concurrent.atomic.AtomicInteger;
35 import java.util.function.LongConsumer;
37 import org.simantics.databoard.util.BijectionMap;
38 import org.simantics.utils.FileUtils;
39 import org.simantics.utils.datastructures.MapSet;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import gnu.trove.map.TObjectLongMap;
44 import gnu.trove.map.hash.TObjectLongHashMap;
47 * @author Tuukka Lehtonen
50 public class DirectorySizeTracker implements Runnable, Closeable {
52 private static final Logger LOGGER = LoggerFactory.getLogger(DirectorySizeTracker.class);
54 private static final int TRACE_NONE = 0;
55 @SuppressWarnings("unused")
56 private static final int TRACE_DIRS = 1;
57 @SuppressWarnings("unused")
58 private static final int TRACE_EVENTS = 2;
59 @SuppressWarnings("unused")
60 private static final int TRACE_FILES = 3;
62 private static final Kind<?>[] ALL_EVENTS = { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
64 private static AtomicInteger threadCounter = new AtomicInteger();
66 private LongConsumer consumer;
67 private Kind<?>[] events;
69 private WatchService watcher;
71 private Object lock = new Object();
73 // For watched directory tracking
74 private BijectionMap<WatchKey, Path> trackedDirs = new BijectionMap<>();
75 private MapSet<Path, Path> subdirs = new MapSet.Hash<Path, Path>();
76 private MapSet<Path, Path> files = new MapSet.Hash<Path, Path>();
78 // For directory and file size tracking
79 private TObjectLongMap<Path> entrySizes = new TObjectLongHashMap<>(1024, 0.5f, -1L);
80 private TObjectLongMap<Path> dirSizes = new TObjectLongHashMap<>(512, 0.5f, -1L);
81 private long totalSize = 0L;
83 private int traceLevel = TRACE_NONE;
84 private boolean running = true;
85 private Thread thread;
87 public static DirectorySizeTracker startTracker(LongConsumer sizeChangeListener) throws IOException {
88 DirectorySizeTracker watcher = new DirectorySizeTracker(sizeChangeListener, ALL_EVENTS);
89 watcher.thread = new Thread(watcher, DirectorySizeTracker.class.getSimpleName() + threadCounter.get());
90 watcher.thread.start();
94 private DirectorySizeTracker(LongConsumer sizeChangeListener, Kind<?>[] events) throws IOException {
95 this.consumer = sizeChangeListener;
97 this.watcher = FileSystems.getDefault().newWatchService();
100 public void close(boolean joinThread) throws InterruptedException {
108 public void close() throws IOException {
111 } catch (InterruptedException e) {
112 throw new IOException(e);
117 * Register the given directory with the WatchService to listen for the
118 * default set of events provided to the constructor.
120 public void track(Path dir) throws IOException {
121 synchronized (lock) {
122 if (trackedDirs.containsRight(dir))
125 LOGGER.info("Starting to track entire directory " + dir + " with a total of " + entrySizes.size() + " files and " + trackedDirs.size() + " dirs with a total size of " + totalSize);
126 Files.walkFileTree(dir, REGISTER);
128 LOGGER.info("Now tracking entire directory " + dir + " with a total of " + entrySizes.size() + " files and " + trackedDirs.size() + " dirs with a total size of " + totalSize);
132 private FileVisitor<Path> REGISTER = new SimpleFileVisitor<Path>() {
133 private Path currentDir;
135 public java.nio.file.FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
138 LOGGER.info("Set current dir to " + currentDir);
139 return FileVisitResult.CONTINUE;
142 public java.nio.file.FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
143 registerFile(currentDir, file, attrs);
144 return FileVisitResult.CONTINUE;
147 public java.nio.file.FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
148 LOGGER.warn("Failed to visit file " + file, exc);
149 return FileVisitResult.CONTINUE;
152 public java.nio.file.FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
153 // Start tracking directories only after registering all entries
155 Path parent = dir.getParent();
157 subdirs.add(parent, dir);
159 return FileVisitResult.CONTINUE;
164 * Register the given directory with the WatchService to listen for the
165 * specified set of events.
167 private boolean trackDir(Path dir) throws IOException {
168 if (trackedDirs.containsRight(dir))
171 WatchKey key = dir.register(watcher, events);
173 if (traceLevel > 1) {
174 Path prev = trackedDirs.getRight(key);
176 LOGGER.info("Tracking new directory {}\n", dir);
178 if (!dir.equals(prev)) {
179 LOGGER.info("Tracked directory update: {} -> {}\n", prev, dir);
184 trackedDirs.map(key, dir);
188 private boolean registerFile(Path dir, Path file, BasicFileAttributes attrs) {
189 if (files.add(dir, file)) {
190 long entrySize = attrs.size();
191 entrySizes.put(file, entrySize);
192 dirSizes.adjustOrPutValue(dir, entrySize, entrySize);
193 totalSize += entrySize;
195 LOGGER.info("Registered file " + file + " size " + entrySize + " for total size " + totalSize);
198 long size = attrs.size();
199 long oldSize = entrySizes.put(file, size);
200 long sizeDelta = oldSize >= 0 ? size - oldSize : size;
201 totalSize += sizeDelta;
202 dirSizes.adjustOrPutValue(dir, sizeDelta, sizeDelta);
204 LOGGER.info("Modified " + file + " size from " + oldSize + " to " + size
205 + " with delta " + sizeDelta + ", dir size = " + dirSizes.get(dir)
206 + ", total size = " + totalSize);
211 private boolean unregisterFile(Path dir, Path file) {
212 long fileSize = entrySizes.remove(file);
214 totalSize -= fileSize;
215 if (files.remove(dir, file))
216 dirSizes.adjustValue(dir, -fileSize);
218 LOGGER.info("Unregistered file " + file + " of size " + fileSize + ", dirSize = " + dirSizes.get(dir) + ", totalSize = " + totalSize);
224 public void untrack(Path dir) {
225 synchronized (lock) {
226 if (!trackedDirs.containsRight(dir))
229 LOGGER.info("Starting to untrack entire directory " + dir + " with total tracked size " + totalSize);
231 subdirs.remove(dir.getParent(), dir);
233 LOGGER.info("Done untracking entire directory " + dir + " with total tracked size " + totalSize);
237 private void untrackTree(Path dir) {
238 Set<Path> subdirs = this.subdirs.removeValues(dir);
239 for (Path subdir : subdirs)
244 private void untrackDir(Path dir) {
246 LOGGER.info("Untrack directory " + dir + " with total tracked size " + totalSize);
247 WatchKey key = trackedDirs.removeWithRight(dir);
251 Set<Path> registeredFiles = files.removeValues(dir);
252 for (Path file : registeredFiles)
253 unregisterFile(dir, file);
254 dirSizes.remove(dir);
257 @SuppressWarnings("unchecked")
258 static <T> WatchEvent<T> cast(WatchEvent<?> event) {
259 return (WatchEvent<T>) event;
263 * Process all events for keys queued to the watcher
265 void processEvents() {
267 // wait for key to be signaled
270 key = watcher.take();
271 } catch (InterruptedException x) {
275 Path dir = trackedDirs.getRight(key);
277 LOGGER.error("WatchKey not registered: " + key);
281 synchronized (lock) {
282 for (WatchEvent<?> event : key.pollEvents()) {
283 WatchEvent.Kind<?> kind = event.kind();
285 // TBD - provide example of how OVERFLOW event is handled
286 if (kind == OVERFLOW)
289 // Context for directory entry event is the file name of entry
290 WatchEvent<Path> evt = cast(event);
291 Path name = evt.context();
292 Path child = dir.resolve(name);
295 LOGGER.info(String.format("%s: %s", event.kind().name(), child));
297 if (kind == ENTRY_CREATE) {
299 BasicFileAttributes attrs = Files.readAttributes(child, BasicFileAttributes.class);
300 if (attrs.isDirectory())
302 else if (attrs.isRegularFile()) {
303 registerFile(dir, child, attrs);
305 } catch (IOException ioe) {
306 LOGGER.error("Failed to read attribute for path " + child, ioe);
308 } else if (kind == ENTRY_MODIFY) {
310 BasicFileAttributes attrs = Files.readAttributes(child, BasicFileAttributes.class);
311 if (attrs.isRegularFile()) {
312 registerFile(dir, child, attrs);
314 } catch (NoSuchFileException ioe) {
315 // It is possible that child is a directory that has been removed.
316 // In this case, just untrack the whole tree of child.
317 if (!entrySizes.containsKey(child)) {
320 } catch (IOException ioe) {
321 LOGGER.error("Failed to read attribute for path " + child, ioe);
323 } else if (kind == ENTRY_DELETE) {
324 if (!unregisterFile(dir, child)) {
325 // This must have been a directory since it isn't registered as a file.
331 // reset key and remove from set if directory no longer accessible
332 boolean valid = key.reset();
335 LOGGER.info("WatchKey for dir " + dir + " is no longer valid. Untracking it.");
337 if (trackedDirs.isEmpty()) {
338 // all directories are inaccessible
345 LOGGER.info("STATUS: Tracking a total of " + entrySizes.size() + " files and " + trackedDirs.size() + " dirs with a total size of " + totalSize);
346 if (consumer != null)
347 consumer.accept(totalSize);
357 FileUtils.uncheckedClose(watcher);
362 * @return total size of the tracked directories in bytes
364 public long getTotalSize() {
368 // public static void main(String[] args) {
370 // DirectorySizeTracker tracker = DirectorySizeTracker.startTracker(null);
371 // tracker.track(Paths.get("d:/track-test"));
372 // LOGGER.info("AFTER TRACK: Total size from " + tracker.entrySizes.size() + " files and " + tracker.trackedDirs.size() + " directories is " + tracker.getTotalSize());
373 // Thread.sleep(2000);
374 // //tracker.untrack(Paths.get("d:/track-test"));
375 // //LOGGER.info("AFTER UNTRACK: Total size from " + tracker.entrySizes.size() + " files and " + tracker.trackedDirs.size() + " directories is " + tracker.getTotalSize());
376 // } catch (IOException | InterruptedException e) {
377 // LOGGER.error("test failed", e);