diff --git a/cli/src/main/groovy/com/muwire/cli/Cli.groovy b/cli/src/main/groovy/com/muwire/cli/Cli.groovy index c1ee95cb..2dbcd9c0 100644 --- a/cli/src/main/groovy/com/muwire/cli/Cli.groovy +++ b/cli/src/main/groovy/com/muwire/cli/Cli.groovy @@ -4,6 +4,7 @@ import java.util.concurrent.CountDownLatch import com.muwire.core.Core import com.muwire.core.MuWireSettings +import com.muwire.core.UILoadedEvent import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.DisconnectionEvent @@ -83,7 +84,8 @@ class Cli { } core.eventBus.register(AllFilesLoadedEvent.class, fileLoader) core.startServices() - + + core.eventBus.publish(new UILoadedEvent()) println "waiting for files to load" latch.await() // now we begin diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index e77548c5..5dee8d85 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -23,6 +23,7 @@ import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.HasherService import com.muwire.core.files.PersisterService +import com.muwire.core.files.DirectoryWatcher import com.muwire.core.hostcache.CacheClient import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostDiscoveredEvent @@ -70,6 +71,7 @@ public class Core { private final ConnectionEstablisher connectionEstablisher private final HasherService hasherService private final DownloadManager downloadManager + private final DirectoryWatcher directoryWatcher public Core(MuWireSettings props, File home, String myVersion) { this.home = home @@ -162,6 +164,7 @@ public class Core { log.info "initializing persistence service" persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager) + eventBus.register(UILoadedEvent.class, persisterService) log.info("initializing host cache") File hostStorage = new File(home, "hosts.json") @@ -213,6 +216,9 @@ public class Core { connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher) + log.info("initializing directory watcher") + directoryWatcher = new DirectoryWatcher(eventBus, fileManager) + eventBus.register(FileSharedEvent.class, directoryWatcher) log.info("initializing hasher service") hasherService = new HasherService(new FileHasher(), eventBus, fileManager) @@ -221,9 +227,9 @@ public class Core { public void startServices() { hasherService.start() + directoryWatcher.start() trustService.start() trustService.waitForLoad() - persisterService.start() hostCache.start() connectionManager.start() cacheClient.start() @@ -240,6 +246,8 @@ public class Core { connectionAcceptor.stop() log.info("shutting down connection establisher") connectionEstablisher.stop() + log.info("shutting down directory watcher") + directoryWatcher.stop() log.info("shutting down connection manager") connectionManager.shutdown() } diff --git a/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy b/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy index d1078ceb..76ced3b3 100644 --- a/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy +++ b/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy @@ -1,6 +1,11 @@ package com.muwire.core +import java.util.stream.Collectors + import com.muwire.core.hostcache.CrawlerResponse +import com.muwire.core.util.DataUtil + +import net.i2p.data.Base64 class MuWireSettings { @@ -10,10 +15,9 @@ class MuWireSettings { int updateCheckInterval String nickname File downloadLocation - String sharedFiles CrawlerResponse crawlerResponse boolean shareDownloadedFiles - boolean watchSharedDirectories + Set watchedDirectories MuWireSettings() { this(new Properties()) @@ -26,11 +30,16 @@ class MuWireSettings { nickname = props.getProperty("nickname","MuWireUser") downloadLocation = new File((String)props.getProperty("downloadLocation", System.getProperty("user.home"))) - sharedFiles = props.getProperty("sharedFiles") downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) - watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true")) + + watchedDirectories = new HashSet<>() + if (props.containsKey("watchedDirectories")) { + String[] encoded = props.getProperty("watchedDirectories").split(",") + encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) } + } + } void write(OutputStream out) throws IOException { @@ -43,9 +52,14 @@ class MuWireSettings { props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval)) props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) - props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories)) - if (sharedFiles != null) - props.setProperty("sharedFiles", sharedFiles) + + if (!watchedDirectories.isEmpty()) { + String encoded = watchedDirectories.stream(). + map({Base64.encode(DataUtil.encodei18nString(it))}). + collect(Collectors.joining(",")) + props.setProperty("watchedDirectories", encoded) + } + props.store(out, "") } diff --git a/core/src/main/groovy/com/muwire/core/files/DirectoryWatcher.groovy b/core/src/main/groovy/com/muwire/core/files/DirectoryWatcher.groovy index 1410786a..e0b5121e 100644 --- a/core/src/main/groovy/com/muwire/core/files/DirectoryWatcher.groovy +++ b/core/src/main/groovy/com/muwire/core/files/DirectoryWatcher.groovy @@ -1,4 +1,137 @@ package com.muwire.core.files +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.Paths +import static java.nio.file.StandardWatchEventKinds.* +import java.nio.file.WatchEvent +import java.nio.file.WatchKey +import java.nio.file.WatchService +import java.util.concurrent.ConcurrentHashMap + +import com.muwire.core.EventBus +import com.muwire.core.SharedFile + +import groovy.util.logging.Log +import net.i2p.util.SystemVersion + +@Log class DirectoryWatcher { + + private static final long WAIT_TIME = 1000 + + private static final WatchEvent.Kind[] kinds + static { + if (SystemVersion.isMac()) + kinds = [ENTRY_MODIFY, ENTRY_DELETE] + else + kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE] + } + + private final EventBus eventBus + private final FileManager fileManager + private final Thread watcherThread, publisherThread + private final Map waitingFiles = new ConcurrentHashMap<>() + private WatchService watchService + private volatile boolean shutdown + + DirectoryWatcher(EventBus eventBus, FileManager fileManager) { + this.eventBus = eventBus + this.fileManager = fileManager + this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher") + watcherThread.setDaemon(true) + this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher") + publisherThread.setDaemon(true) + } + + void start() { + watchService = FileSystems.getDefault().newWatchService() + watcherThread.start() + publisherThread.start() + } + + void stop() { + shutdown = true + watcherThread.interrupt() + publisherThread.interrupt() + watchService.close() + } + + void onFileSharedEvent(FileSharedEvent e) { + if (!e.file.isDirectory()) + return + Path path = e.file.getCanonicalFile().toPath() + path.register(watchService, kinds) + + } + + private void watch() { + try { + while(!shutdown) { + WatchKey key = watchService.take() + key.pollEvents().each { + switch(it.kind()) { + case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break + case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break + case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break + } + } + key.reset() + } + } catch (InterruptedException e) { + if (!shutdown) + throw e + } + } + + + private void processCreated(Path parent, Path path) { + File f= join(parent, path) + log.fine("created entry $f") + if (f.isDirectory()) + f.toPath().register(watchService, kinds) + } + + private void processModified(Path parent, Path path) { + File f = join(parent, path) + log.fine("modified entry $f") + waitingFiles.put(f, System.currentTimeMillis()) + } + + private void processDeleted(Path parent, Path path) { + File f = join(parent, path) + log.fine("deleted entry $f") + SharedFile sf = fileManager.fileToSharedFile.get(f) + if (sf != null) + eventBus.publish(new FileUnsharedEvent(unsharedFile : sf)) + } + + private static File join(Path parent, Path path) { + File parentFile = parent.toFile().getCanonicalFile() + new File(parentFile, path.toFile().getName()) + } + + private void publish() { + try { + while(!shutdown) { + Thread.sleep(WAIT_TIME) + long now = System.currentTimeMillis() + def published = [] + waitingFiles.each { file, timestamp -> + if (now - timestamp > WAIT_TIME) { + log.fine("publishing file $file") + eventBus.publish new FileSharedEvent(file : file) + published << file + } + } + published.each { + waitingFiles.remove(it) + } + } + } catch (InterruptedException e) { + if (!shutdown) + throw e + } + } } diff --git a/core/src/main/groovy/com/muwire/core/files/HasherService.groovy b/core/src/main/groovy/com/muwire/core/files/HasherService.groovy index 9607b1ea..b3701a30 100644 --- a/core/src/main/groovy/com/muwire/core/files/HasherService.groovy +++ b/core/src/main/groovy/com/muwire/core/files/HasherService.groovy @@ -32,7 +32,7 @@ class HasherService { private void process(File f) { f = f.getCanonicalFile() if (f.isDirectory()) { - f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) } + f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) } } else { if (f.length() == 0) { eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f") diff --git a/core/src/main/groovy/com/muwire/core/files/PersisterService.groovy b/core/src/main/groovy/com/muwire/core/files/PersisterService.groovy index 63911113..051cde0a 100644 --- a/core/src/main/groovy/com/muwire/core/files/PersisterService.groovy +++ b/core/src/main/groovy/com/muwire/core/files/PersisterService.groovy @@ -11,6 +11,7 @@ import com.muwire.core.EventBus import com.muwire.core.InfoHash import com.muwire.core.Service import com.muwire.core.SharedFile +import com.muwire.core.UILoadedEvent import com.muwire.core.util.DataUtil import groovy.json.JsonOutput @@ -36,14 +37,14 @@ class PersisterService extends Service { timer = new Timer("file persister", true) } - void start() { - timer.schedule({load()} as TimerTask, 1) - } - void stop() { timer.cancel() } - + + void onUILoadedEvent(UILoadedEvent e) { + timer.schedule({load()} as TimerTask, 1) + } + void load() { if (location.exists() && location.isFile()) { def slurper = new JsonSlurper() diff --git a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy index 7102eff1..609e7480 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy @@ -194,6 +194,13 @@ class MainFrameController { println "unsharing selected files" } + void saveMuWireSettings() { + File f = new File(core.home, "MuWire.properties") + f.withOutputStream { + core.muOptions.write(it) + } + } + void mvcGroupInit(Map args) { application.addPropertyChangeListener("core", {e-> core = e.getNewValue() diff --git a/gui/griffon-app/lifecycle/Ready.groovy b/gui/griffon-app/lifecycle/Ready.groovy index 7f8419f5..61bdd49b 100644 --- a/gui/griffon-app/lifecycle/Ready.groovy +++ b/gui/griffon-app/lifecycle/Ready.groovy @@ -1,3 +1,4 @@ + import griffon.core.GriffonApplication import griffon.core.env.Metadata import groovy.util.logging.Log @@ -104,12 +105,6 @@ class Ready extends AbstractLifecycleHandler { it.propertyChange(new PropertyChangeEvent(this, "core", null, core)) } - if (props.sharedFiles != null) { - props.sharedFiles.split(",").each { - core.eventBus.publish(new FileSharedEvent(file : new File(it))) - } - } - core.eventBus.publish(new UILoadedEvent()) } } diff --git a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy index e50c3d38..90b7911a 100644 --- a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy @@ -9,6 +9,7 @@ import javax.swing.JTable import com.muwire.core.Core import com.muwire.core.InfoHash +import com.muwire.core.MuWireSettings import com.muwire.core.Persona import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionEvent @@ -19,6 +20,7 @@ import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileSharedEvent +import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.search.QueryEvent import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultEvent @@ -53,6 +55,7 @@ class MainFrameModel { def downloads = [] def uploads = [] def shared = [] + def watched = [] def connectionList = [] def searches = new LinkedList() def trusted = [] @@ -131,9 +134,10 @@ class MainFrameModel { core.eventBus.register(QueryEvent.class, this) core.eventBus.register(UpdateAvailableEvent.class, this) core.eventBus.register(FileDownloadedEvent.class, this) + core.eventBus.register(FileUnsharedEvent.class, this) timer.schedule({ - int retryInterval = application.context.get("muwire-settings").downloadRetryInterval + int retryInterval = core.muOptions.downloadRetryInterval if (retryInterval > 0) { retryInterval *= 60000 long now = System.currentTimeMillis() @@ -156,6 +160,10 @@ class MainFrameModel { runInsideUIAsync { trusted.addAll(core.trustService.good.values()) distrusted.addAll(core.trustService.bad.values()) + + watched.addAll(core.muOptions.watchedDirectories) + builder.getVariable("watched-directories-table").model.fireTableDataChanged() + watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) } } }) @@ -236,6 +244,17 @@ class MainFrameModel { } } + void onFileUnsharedEvent(FileUnsharedEvent e) { + InfoHash infohash = e.unsharedFile.infoHash + if (!infoHashes.remove(infohash)) + return + runInsideUIAsync { + shared.remove(e.unsharedFile) + JTable table = builder.getVariable("shared-files-table") + table.model.fireTableDataChanged() + } + } + void onUploadEvent(UploadEvent e) { runInsideUIAsync { uploads << e.uploader diff --git a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy index 3a5b941d..0e850ffb 100644 --- a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy @@ -139,16 +139,32 @@ class MainFrameView { panel (constraints: "uploads window"){ gridLayout(cols : 1, rows : 2) panel { - borderLayout() - panel (constraints : BorderLayout.NORTH) { - button(text : "Click here to share files", actionPerformed : shareFiles) + gridLayout(cols : 2, rows : 1) + panel { + borderLayout() + panel (constraints : BorderLayout.NORTH) { + button(text : "Add directories to watch", actionPerformed : watchDirectories) + } + scrollPane (constraints : BorderLayout.CENTER) { + table(id : "watched-directories-table", autoCreateRowSorter: true) { + tableModel(list : model.watched) { + closureColumn(header: "Watched Directories", type : String, read : { it }) + } + } + } } - scrollPane ( constraints : BorderLayout.CENTER) { - table(id : "shared-files-table", autoCreateRowSorter: true) { - tableModel(list : model.shared) { - closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()}) - closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() }) - } + panel { + borderLayout() + panel (constraints : BorderLayout.NORTH) { + button(text : "Share files", actionPerformed : shareFiles) + } + scrollPane(constraints : BorderLayout.CENTER) { + table(id : "shared-files-table", autoCreateRowSorter: true) { + tableModel(list : model.shared) { + closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.file.getAbsolutePath()}) + closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.file.length() }) + } + } } } } @@ -466,11 +482,26 @@ class MainFrameView { def shareFiles = { def chooser = new JFileChooser() - chooser.setDialogTitle("Select file or directory to share") - chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES) + chooser.setDialogTitle("Select file to share") + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY) int rv = chooser.showOpenDialog(null) if (rv == JFileChooser.APPROVE_OPTION) { model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile())) } } + + def watchDirectories = { + def chooser = new JFileChooser() + chooser.setDialogTitle("Select directory to watch") + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) + int rv = chooser.showOpenDialog(null) + if (rv == JFileChooser.APPROVE_OPTION) { + File f = chooser.getSelectedFile() + model.watched << f.getAbsolutePath() + application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath() + mvcGroup.controller.saveMuWireSettings() + builder.getVariable("watched-directories-table").model.fireTableDataChanged() + model.core.eventBus.publish(new FileSharedEvent(file : f)) + } + } } \ No newline at end of file