Compare commits

...

61 Commits

Author SHA1 Message Date
1227cf9263 Release 0.5.0 2019-10-15 12:38:25 +01:00
a05575485f move things around 2019-10-15 10:40:50 +01:00
f5bccd8126 All shared directories are watched directories. Fix manipulation of tree structure 2019-10-15 08:38:23 +01:00
70fb789abf remove the watched directories table 2019-10-15 04:51:21 +01:00
feb712c253 Move persisting of files on dedicated thread. Introduce an event to forcefully persist files. Do that immediately after unsharing anything 2019-10-15 04:21:40 +01:00
d22b403e2a stop watching multiple directories at once 2019-10-14 23:16:05 +01:00
a24982e0df fix comments for local results 2019-10-14 22:47:52 +01:00
6c26019164 allow switching without restart 2019-10-14 21:40:03 +01:00
965fa79bbf fix count of shared files in tree view mode 2019-10-14 20:57:50 +01:00
60ddb85461 Tree view of the shared files. The count is wrong for some reason 2019-10-14 20:13:25 +01:00
c7284623bc Release 0.4.16 2019-10-13 22:14:33 +01:00
3e7f2aa70a Add a note about DND, automatically watch shared directories 2019-10-13 20:21:28 +01:00
4f436a636c implement drop on MW -> share files/directories 2019-10-13 20:00:08 +01:00
b49dbc30c3 comment already decoded by the time it gets to the gui 2019-10-11 19:01:40 +01:00
c25d314e1c typo 2019-10-11 18:56:46 +01:00
b28587a275 wip on file comments 2019-10-11 18:42:02 +01:00
8b8e5d59be Silence an IllegalArgumentException while sorting downloads table 2019-10-11 11:21:56 +01:00
70bbe1f636 update version 2019-10-10 17:33:07 +01:00
337605dc0f Release 0.4.15 2019-10-10 16:48:10 +01:00
14bdfa6b2e throttle even further - 500/s 2019-10-09 17:34:54 +01:00
ed3f9da773 throttle loading even further, to 1000/sec 2019-10-09 16:46:17 +01:00
251080d08f throttle loading of files to 500/s 2019-10-09 16:34:09 +01:00
f530ab999d operations on multiple selection in shared files table 2019-10-09 03:38:08 +01:00
4133384e48 ability to share multiple files and directories 2019-10-08 21:30:34 +01:00
600fc98868 update TODO 2019-10-07 12:38:26 +01:00
129eeb3b88 JDK needed, not JRE 2019-10-07 12:38:09 +01:00
20b51b78a0 reduce priority of file persister thread 2019-10-07 11:59:51 +01:00
33fe755b60 implement multiple-selection on downloads table 2019-10-07 04:26:35 +01:00
8b0668a134 Rewrite utils into Java, cache the persistable data of shared files to reduce object churn 2019-10-05 22:50:32 +01:00
730d2202fd bundles for linux available now 2019-10-05 18:53:43 +01:00
69906a986d set i2p.dir.base to prevent router creating files in PWD 2019-10-05 15:03:59 +01:00
5bc8fa8633 Preserve selection on refresh #18 2019-10-05 05:13:49 +01:00
7de7c9d8f3 Add 'Clear Hits' button to content control panel #18 2019-10-05 05:03:25 +01:00
e943f6019d disable all GUI unit tests, enable host-cache unit tests. The 'build' target now succeeds 2019-10-05 04:31:11 +01:00
2eec7bec5b fix most core tests 2019-10-05 04:20:14 +01:00
c36110cf76 update readme 2019-10-04 16:41:07 +01:00
abe28517bc Release 0.4.14 2019-10-04 13:00:57 +01:00
15bc4c064d center the button 2019-10-03 21:32:32 +01:00
91d771944b add option for sequential download 2019-10-03 20:45:22 +01:00
e09c456a13 make the download retry interval in seconds, default still 1 minute 2019-10-03 19:31:15 +01:00
d9c1067226 Add Neutral button to search tab, issue #17 2019-10-02 06:02:06 +01:00
eda3e7ad3a Add option to not search extra hop, only considered if connecting only to trusted peers, issue #6 2019-10-02 05:45:46 +01:00
e9798c7eaa remember last rejection and back off from hosts that reject us. Fix return value of retry and hopelessness predicates 2019-10-01 08:34:43 +01:00
66bb4eef5b close outbound establishments on a separate thread 2019-10-01 07:50:29 +01:00
55f260b3f4 update version 2019-09-29 19:21:06 +01:00
32d4c3965e Release 0.4.13 2019-09-29 19:00:20 +01:00
de1534d837 reduce the default host retry interval 2019-09-29 18:45:09 +01:00
7b58e8a88a separate setting for the interval after which a host is considered hopeless 2019-09-29 18:43:39 +01:00
8a03b89985 clean up the filtering logic; allow serialization of hosts that can be retried 2019-09-29 16:49:02 +01:00
1d97374857 track last successful attempt. Only re-attempt hosts if they have ever been successful. Do not serialize hosts considered hopeless 2019-09-29 16:19:19 +01:00
549e8c2d98 Release 0.4.12 2019-09-22 16:55:04 +01:00
b54d24db0d new update server destination 2019-09-22 16:47:35 +01:00
fa12e84345 stronger sig type 2019-09-22 16:23:01 +01:00
6430ff2691 bump i2p libs version 2019-09-22 16:13:12 +01:00
591313c81c point to the pkg project 2019-09-20 21:09:53 +01:00
ce7b6a0c65 change to gasp AA font table, try metal lnf if the others fail 2019-09-16 15:06:45 +01:00
5c4d4c4580 embedded router will not work without reseed certificates, so remove it 2019-09-16 15:04:34 +01:00
4cb864ff9f update version 2019-09-16 15:03:20 +01:00
417675ad07 update dark_trion's hostcache address 2019-07-22 21:48:29 +01:00
9513e5ba3c update todo 2019-07-20 13:15:44 +01:00
85610cf169 add new host-cache 2019-07-15 22:05:09 +01:00
70 changed files with 1218 additions and 605 deletions

View File

@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.4.15 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew clean assemble
@ -19,18 +19,16 @@ If you want to run the unit tests, type
./gradlew clean build
```
Some of the UI tests will fail because they haven't been written yet :-/
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
### Running
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt.
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
[Default I2CP port]\: `7654`
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
### GPG Fingerprint
```

View File

@ -12,10 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface
@ -27,5 +23,4 @@ To enable parsing of metadata from known file types and the user editing it or a
### Small Items
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Multiple-selection download, Ctrl-A
* Automatic adjustment of number of I2P tunnels

View File

@ -2,7 +2,7 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile 'net.i2p:i2p:0.9.41'
compile 'net.i2p:i2p:0.9.42'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
}

View File

@ -35,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.4.11")
core = new Core(props, home, "0.5.0")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.4.11")
core = new Core(props, home, "0.5.0")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@ -2,9 +2,9 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile 'net.i2p:router:0.9.41'
compile 'net.i2p.client:mstreaming:0.9.41'
compile 'net.i2p.client:streaming:0.9.41'
compile 'net.i2p:router:0.9.42'
compile 'net.i2p.client:mstreaming:0.9.42'
compile 'net.i2p.client:streaming:0.9.42'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'

View File

@ -1,13 +0,0 @@
package com.muwire.core
import net.i2p.crypto.SigType
class Constants {
public static final byte PERSONA_VERSION = (byte)1
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
public static final int MAX_HEADER_SIZE = 0x1 << 14
public static final int MAX_HEADERS = 16
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
}

View File

@ -28,6 +28,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.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatcher
@ -135,6 +136,7 @@ public class Core {
} else {
log.info("launching embedded router")
Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
@ -222,6 +224,7 @@ public class Core {
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@ -279,7 +282,7 @@ public class Core {
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
@ -287,6 +290,8 @@ public class Core {
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
eventBus.register(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
log.info("initializing trust subscriber")
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
@ -361,7 +366,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.4.11")
Core core = new Core(props, home, "0.5.0")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -6,11 +6,13 @@ import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
boolean searchExtraHop
boolean allowTrustLists
int trustListInterval
Set<Persona> trustSubscriptions
@ -24,7 +26,7 @@ class MuWireSettings {
boolean shareDownloadedFiles
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval
int hostClearInterval, hostHopelessInterval, hostRejectInterval
int meshExpiration
boolean embeddedRouter
int inBw, outBw
@ -38,19 +40,22 @@ class MuWireSettings {
MuWireSettings(Properties props) {
isLeaf = Boolean.valueOf(props.get("leaf","false"))
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
@ -74,6 +79,7 @@ class MuWireSettings {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
props.setProperty("crawlerResponse", crawlerResponse.toString())
@ -86,6 +92,8 @@ class MuWireSettings {
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw))
@ -106,7 +114,7 @@ class MuWireSettings {
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new HashSet<>()
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }

View File

@ -0,0 +1,7 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
}

View File

@ -31,7 +31,7 @@ class ConnectionEstablisher {
final HostCache hostCache
final Timer timer
final ExecutorService executor
final ExecutorService executor, closer
final Set inProgress = new ConcurrentHashSet()
@ -51,6 +51,8 @@ class ConnectionEstablisher {
rv.setName("connector-${System.currentTimeMillis()}")
rv
} as ThreadFactory)
closer = Executors.newSingleThreadExecutor()
}
void start() {
@ -60,6 +62,7 @@ class ConnectionEstablisher {
void stop() {
timer.cancel()
executor.shutdownNow()
closer.shutdown()
}
private void connectIfNeeded() {
@ -120,8 +123,10 @@ class ConnectionEstablisher {
}
private void fail(Endpoint endpoint) {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
closer.execute {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
}
private void readK(Endpoint e) {
@ -175,7 +180,7 @@ class ConnectionEstablisher {
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
} finally {
// the end
e.close()
closer.execute({e.close()} as Runnable)
}
}

View File

@ -74,7 +74,7 @@ public class DownloadManager {
destinations.addAll(e.sources)
destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize)
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations,
@ -122,8 +122,12 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root)
}
boolean sequential = false
if (json.sequential != null)
sequential = json.sequential
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
@ -137,12 +141,12 @@ public class DownloadManager {
}
}
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
int pieceSize = 0x1 << pieceSizePow2
int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0)
nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
mesh.pieces
}
@ -188,6 +192,9 @@ public class DownloadManager {
json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused
json.sequential = downloader.pieces.ratio == 0f
writer.println(JsonOutput.toJson(json))
}
}

View File

@ -17,7 +17,7 @@ class Pieces {
done = new BitSet(nPieces)
claimed = new BitSet(nPieces)
}
synchronized int[] claim() {
int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces) {
@ -30,7 +30,7 @@ class Pieces {
}
// if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
int rv = claimed.nextClearBit(0)
claimed.set(rv)
return [rv, partials.getOrDefault(rv, 0), 0]
@ -59,7 +59,8 @@ class Pieces {
return [rv, partials.getOrDefault(rv, 0), 1]
}
List<Integer> toList = availableCopy.toList()
Collections.shuffle(toList)
if (ratio > 0f)
Collections.shuffle(toList)
int rv = toList[0]
claimed.set(rv)
[rv, partials.getOrDefault(rv, 0), 0]

View File

@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
UIResultEvent[] result
Set<Destination> sources
File target
boolean sequential
}

View File

@ -13,6 +13,7 @@ import java.nio.file.WatchService
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import groovy.util.logging.Log
@ -31,6 +32,8 @@ class DirectoryWatcher {
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
}
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus
private final FileManager fileManager
private final Thread watcherThread, publisherThread
@ -39,7 +42,9 @@ class DirectoryWatcher {
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
this.home = home
this.muOptions = muOptions
this.eventBus = eventBus
this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
@ -64,15 +69,28 @@ class DirectoryWatcher {
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
Path path = e.file.getCanonicalFile().toPath()
File canonical = e.file.getCanonicalFile()
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(e.file, wk)
watchedDirectories.put(canonical, wk)
if (muOptions.watchedDirectories.add(canonical.toString()))
saveMuSettings()
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
if (muOptions.watchedDirectories.remove(e.directory.toString()))
saveMuSettings()
}
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muOptions.write(it)
}
}
private void watch() {

View File

@ -11,6 +11,7 @@ class HasherService {
final FileHasher hasher
final EventBus eventBus
final FileManager fileManager
final Set<File> hashed = new HashSet<>()
Executor executor
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
@ -24,13 +25,22 @@ class HasherService {
}
void onFileSharedEvent(FileSharedEvent evt) {
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
File canonical = evt.file.getCanonicalFile()
if (fileManager.fileToSharedFile.containsKey(canonical))
return
executor.execute( { -> process(evt.file) } as Runnable)
if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}
void onFileUnsharedEvent(FileUnsharedEvent evt) {
hashed.remove(evt.unsharedFile.file)
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent evt) {
hashed.remove(evt.directory)
}
private void process(File f) {
f = f.getCanonicalFile()
if (f.isDirectory()) {
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
} else {

View File

@ -3,6 +3,9 @@ package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
import java.util.stream.Collectors
@ -28,13 +31,16 @@ class PersisterService extends Service {
final int interval
final Timer timer
final FileManager fileManager
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location
this.listener = listener
this.interval = interval
this.fileManager = fileManager
timer = new Timer("file persister", true)
timer = new Timer("file persister timer", true)
}
void stop() {
@ -44,9 +50,16 @@ class PersisterService extends Service {
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isFile()) {
int loaded = 0
def slurper = new JsonSlurper()
try {
location.eachLine {
@ -56,6 +69,9 @@ class PersisterService extends Service {
if (event != null) {
log.fine("loaded file $event.loadedFile.file")
listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
}
}
}
@ -109,44 +125,44 @@ class PersisterService extends Service {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df)
}
SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
return new FileLoadedEvent(loadedFile: sf)
}
private void persistFiles() {
def sharedFiles = fileManager.getSharedFiles()
persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
}
}
}
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
} as Runnable)
}
private def toJson(File f, SharedFile sf) {
def json = [:]
json.file = Base64.encode DataUtil.encodei18nString(f.toString())
json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength()
InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot()
json.infoHash = sf.getB64EncodedHashRoot()
json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32]
json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) {
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
json.hashList.add Base64.encode(tmp)
}
json.hashList = sf.getB64EncodedHashList()
json.comment = sf.getComment()
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())

View File

@ -0,0 +1,6 @@
package com.muwire.core.files
import com.muwire.core.Event
class UIPersistFilesEvent extends Event {
}

View File

@ -6,8 +6,12 @@ class CacheServers {
private static final int TO_GIVE = 3
private static Set<Destination> CACHES = [
// zlatinb
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA==")
// sNL
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
// dark_trion
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
]
static List<Destination> getCacheServers() {

View File

@ -7,21 +7,35 @@ class Host {
private static final int MAX_FAILURES = 3
final Destination destination
private final int clearInterval
private final int clearInterval, hopelessInterval, rejectionInterval
int failures,successes
long lastAttempt
long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval) {
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
this.destination = destination
this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
}
synchronized void onConnect() {
private void connectSuccessful() {
failures = 0
successes++
lastAttempt = System.currentTimeMillis()
}
synchronized void onConnect() {
connectSuccessful()
lastSuccessfulAttempt = lastAttempt
}
synchronized void onReject() {
connectSuccessful()
lastRejection = lastAttempt;
}
synchronized void onFailure() {
failures++
successes = 0
@ -40,7 +54,17 @@ class Host {
failures = 0
}
synchronized void canTryAgain() {
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
synchronized boolean canTryAgain() {
lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
}
synchronized boolean isHopeless() {
isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
}
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
}
}

View File

@ -52,7 +52,7 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures()
return
}
Host host = new Host(e.destination, settings.hostClearInterval)
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
if (allowHost(host)) {
hosts.put(e.destination, host)
}
@ -64,15 +64,17 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)
if (host == null) {
host = new Host(dest, settings.hostClearInterval)
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
hosts.put(dest, host)
}
switch(e.status) {
case ConnectionAttemptStatus.SUCCESSFUL:
case ConnectionAttemptStatus.REJECTED:
host.onConnect()
break
case ConnectionAttemptStatus.REJECTED:
host.onReject()
break
case ConnectionAttemptStatus.FAILED:
host.onFailure()
break
@ -82,6 +84,10 @@ class HostCache extends Service {
List<Destination> getHosts(int n) {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {allowHost(hosts[it])}
rv.removeAll {
def h = hosts[it];
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
}
if (rv.size() <= n)
return rv
Collections.shuffle(rv)
@ -106,12 +112,16 @@ class HostCache extends Service {
storage.eachLine {
def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt
if (allowHost(host))
if (entry.lastSuccessfulAttempt != null)
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
if (entry.lastRejection != null)
host.lastRejection = entry.lastRejection
if (allowHost(host))
hosts.put(dest, host)
}
}
@ -120,8 +130,6 @@ class HostCache extends Service {
}
private boolean allowHost(Host host) {
if (host.isFailed() && !host.canTryAgain())
return false
if (host.destination == myself)
return false
TrustLevel trust = trustService.getLevel(host.destination)
@ -140,12 +148,14 @@ class HostCache extends Service {
storage.delete()
storage.withPrintWriter { writer ->
hosts.each { dest, host ->
if (allowHost(host)) {
if (allowHost(host) && !host.isHopeless()) {
def map = [:]
map.destination = dest.toBase64()
map.failures = host.failures
map.successes = host.successes
map.lastAttempt = host.lastAttempt
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
map.lastRejection = host.lastRejection
def json = JsonOutput.toJson(map)
writer.println json
}

View File

@ -33,11 +33,12 @@ class MeshManager {
meshes.get(infoHash)
}
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
synchronized(meshes) {
if (meshes.containsKey(infoHash))
return meshes.get(infoHash)
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio)
float ratio = sequential ? 0f : settings.downloadSequentialRatio
Pieces pieces = new Pieces(nPieces, ratio)
if (fileManager.rootToFiles.containsKey(infoHash)) {
for (int i = 0; i < nPieces; i++)
pieces.markDownloaded(i)

View File

@ -90,6 +90,10 @@ class ResultsParser {
Set<Destination> sources = Collections.emptySet()
if (json.sources != null)
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
String comment = null
if (json.comment != null)
comment = DataUtil.readi18nString(Base64.decode(json.comment))
return new UIResultEvent( sender : p,
name : name,
@ -97,6 +101,7 @@ class ResultsParser {
infohash : new InfoHash(infoHash),
pieceSize : pieceSize,
sources : sources,
comment : comment,
uuid: uuid)
} catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e)

View File

@ -4,6 +4,7 @@ import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import com.muwire.core.Persona
import java.nio.charset.StandardCharsets
@ -60,13 +61,18 @@ class ResultsSender {
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def comment = null
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : pieceSize,
uuid : uuid,
sources : suggested
sources : suggested,
comment : comment
)
eventBus.publish(uiResultEvent)
}
@ -121,6 +127,9 @@ class ResultsSender {
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (it.getComment() != null)
obj.comment = it.getComment()
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())

View File

@ -1,6 +1,6 @@
package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.SplitPattern
class SearchIndex {
@ -32,7 +32,7 @@ class SearchIndex {
}
private static String[] split(String source) {
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = source.split(" ")
def rv = []
split.each { if (it.length() > 0) rv << it }

View File

@ -14,6 +14,7 @@ class UIResultEvent extends Event {
long size
InfoHash infohash
int pieceSize
String comment
@Override
public String toString() {

View File

@ -3,5 +3,5 @@ package com.muwire.core.update
import net.i2p.data.Destination
class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
}

View File

@ -92,7 +92,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}
@ -217,7 +217,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}

View File

@ -0,0 +1,11 @@
package com.muwire.core;
import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
public static final int MAX_HEADER_SIZE = 0x1 << 14;
public static final int MAX_HEADERS = 16;
}

View File

@ -2,6 +2,12 @@ package com.muwire.core;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.util.DataUtil;
import net.i2p.data.Base64;
public class SharedFile {
@ -11,6 +17,12 @@ public class SharedFile {
private final String cachedPath;
private final long cachedLength;
private final String b64EncodedFileName;
private final String b64EncodedHashRoot;
private final List<String> b64EncodedHashList;
private volatile String comment;
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file;
@ -18,6 +30,16 @@ public class SharedFile {
this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length();
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
List<String> b64List = new ArrayList<String>();
byte[] tmp = new byte[32];
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
b64List.add(Base64.encode(tmp));
}
this.b64EncodedHashList = b64List;
}
public File getFile() {
@ -40,6 +62,18 @@ public class SharedFile {
rv++;
return rv;
}
public String getB64EncodedFileName() {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() {
return cachedPath;
@ -48,6 +82,14 @@ public class SharedFile {
public long getCachedLength() {
return cachedLength;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getComment() {
return comment;
}
@Override
public int hashCode() {

View File

@ -1,122 +1,134 @@
package com.muwire.core.util
package com.muwire.core.util;
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.Constants
import com.muwire.core.Constants;
import net.i2p.data.Base64
import net.i2p.data.Base64;
class DataUtil {
public class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1
private final static int MAX_SHORT = (0x1 << 16) - 1;
static void writeUnsignedShort(int value, OutputStream os) {
static void writeUnsignedShort(int value, OutputStream os) throws IOException {
if (value > MAX_SHORT || value < 0)
throw new IllegalArgumentException("$value invalid")
throw new IllegalArgumentException("$value invalid");
byte lsb = (byte) (value & 0xFF)
byte msb = (byte) (value >> 8)
byte lsb = (byte) (value & 0xFF);
byte msb = (byte) (value >> 8);
os.write(msb)
os.write(lsb)
os.write(msb);
os.write(lsb);
}
private final static int MAX_HEADER = 0x7FFFFF
private final static int MAX_HEADER = 0x7FFFFF;
static void packHeader(int length, byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length")
throw new IllegalArgumentException("header length $header.length");
if (length < 0 || length > MAX_HEADER)
throw new IllegalArgumentException("length $length")
throw new IllegalArgumentException("length $length");
header[2] = (byte) (length & 0xFF)
header[1] = (byte) ((length >> 8) & 0xFF)
header[0] = (byte) ((length >> 16) & 0x7F)
header[2] = (byte) (length & 0xFF);
header[1] = (byte) ((length >> 8) & 0xFF);
header[0] = (byte) ((length >> 16) & 0x7F);
}
static int readLength(byte [] header) {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length")
throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF)
((int)header[2] & 0xFF);
}
static String readi18nString(byte [] encoded) {
if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length")
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF)
throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
if (encoded.length != length + 2)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length")
byte [] string = new byte[length]
System.arraycopy(encoded, 2, string, 0, length)
new String(string, StandardCharsets.UTF_8)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
byte [] string = new byte[length];
System.arraycopy(encoded, 2, string, 0, length);
return new String(string, StandardCharsets.UTF_8);
}
static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8)
public static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
if (utf8.length > Short.MAX_VALUE)
throw new IllegalArgumentException("String in utf8 too long $utf8.length")
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) utf8.length)
daos.write(utf8)
daos.close()
baos.toByteArray()
throw new IllegalArgumentException("String in utf8 too long $utf8.length");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream daos = new DataOutputStream(baos);
try {
daos.writeShort((short) utf8.length);
daos.write(utf8);
daos.close();
} catch (IOException impossible) {
throw new IllegalStateException(impossible);
}
return baos.toByteArray();
}
public static String readTillRN(InputStream is) {
def baos = new ByteArrayOutputStream()
public static String readTillRN(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
byte read = is.read()
int read = is.read();
if (read == -1)
throw new IOException()
throw new IOException();
if (read == '\r') {
if (is.read() != '\n')
throw new IOException("invalid header")
break
throw new IOException("invalid header");
break;
}
baos.write(read)
baos.write(read);
}
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
}
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8
int bytes = totalPieces / 8;
if (totalPieces % 8 != 0)
bytes++
byte[] raw = new byte[bytes]
pieces.each {
int byteIdx = it / 8
int offset = it % 8
int mask = 0x80 >>> offset
raw[byteIdx] |= mask
bytes++;
byte[] raw = new byte[bytes];
for (int it : pieces) {
int byteIdx = it / 8;
int offset = it % 8;
int mask = 0x80 >>> offset;
raw[byteIdx] |= mask;
}
Base64.encode(raw)
return Base64.encode(raw);
}
public static List<Integer> decodeXHave(String xHave) {
byte [] availablePieces = Base64.decode(xHave)
List<Integer> available = new ArrayList<>()
availablePieces.eachWithIndex {b, i ->
byte [] availablePieces = Base64.decode(xHave);
List<Integer> available = new ArrayList<>();
for (int i = 0; i < availablePieces.length; i ++) {
byte b = availablePieces[i];
for (int j = 0; j < 8 ; j++) {
byte mask = 0x80 >>> j
byte mask = (byte) (0x80 >>> j);
if ((b & mask) == mask) {
available.add(i * 8 + j)
available.add(i * 8 + j);
}
}
}
available
return available;
}
public static Exception findRoot(Exception e) {
public static Throwable findRoot(Throwable e) {
while(e.getCause() != null)
e = e.getCause()
e
e = e.getCause();
return e;
}
public static void tryUnmap(ByteBuffer cb) {

View File

@ -4,6 +4,7 @@ import static org.junit.Assert.fail
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import com.muwire.core.EventBus
@ -180,10 +181,11 @@ class DownloadSessionTest {
}
@Test
@Ignore // this needs to be rewritten with stealing in mind
public void testSmallFileClaimed() {
initSession(20, [0])
long now = System.currentTimeMillis()
downloadThread.join(100)
downloadThread.join(150)
assert 100 >= (System.currentTimeMillis() - now)
assert !performed
assert available.isEmpty()

View File

@ -16,7 +16,7 @@ class PiecesTest {
public void testSinglePiece() {
pieces = new Pieces(1)
assert !pieces.isComplete()
assert pieces.claim() == 0
assert pieces.claim() == [0,0,0]
pieces.markDownloaded(0)
assert pieces.isComplete()
}
@ -25,28 +25,28 @@ class PiecesTest {
public void testTwoPieces() {
pieces = new Pieces(2)
assert !pieces.isComplete()
int piece = pieces.claim()
assert piece == 0 || piece == 1
pieces.markDownloaded(piece)
int[] piece = pieces.claim()
assert piece[0] == 0 || piece[0] == 1
pieces.markDownloaded(piece[0])
assert !pieces.isComplete()
int piece2 = pieces.claim()
assert piece != piece2
pieces.markDownloaded(piece2)
int[] piece2 = pieces.claim()
assert piece[0] != piece2[0]
pieces.markDownloaded(piece2[0])
assert pieces.isComplete()
}
@Test
public void testClaimAvailable() {
pieces = new Pieces(2)
int claimed = pieces.claim([0].toSet())
assert claimed == 0
assert -1 == pieces.claim([0].toSet())
int[] claimed = pieces.claim([0].toSet())
assert claimed == [0,0,0]
assert [0,0,1] == pieces.claim([0].toSet())
}
@Test
public void testClaimNoneAvailable() {
pieces = new Pieces(20)
int claimed = pieces.claim()
assert -1 == pieces.claim([claimed].toSet())
int[] claimed = pieces.claim()
assert [0,0,0] == pieces.claim(claimed.toSet())
}
}

View File

@ -72,6 +72,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL
}
settingsMock.ignore.allowUntrusted { true }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
@ -91,6 +94,10 @@ class HostCacheTest {
TrustLevel.DISTRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -104,6 +111,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL
}
settingsMock.ignore.allowUntrusted { false }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
@ -123,6 +133,9 @@ class HostCacheTest {
}
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -139,7 +152,15 @@ class HostCacheTest {
assert d == destinations.dest1
TrustLevel.TRUSTED
}
trustMock.demand.getLevel { d ->
assert d == destinations.dest1
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 100 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -158,6 +179,10 @@ class HostCacheTest {
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -183,6 +208,10 @@ class HostCacheTest {
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -214,6 +243,10 @@ class HostCacheTest {
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -229,6 +262,11 @@ class HostCacheTest {
assert d == destinations.dest1
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
Thread.sleep(150)
@ -260,6 +298,10 @@ class HostCacheTest {
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks()
def rv = cache.getHosts(5)
assert rv.size() == 1

View File

@ -9,6 +9,9 @@ import org.junit.Test
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
import com.muwire.core.download.Pieces
import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
class UploaderTest {
@ -52,7 +55,13 @@ class UploaderTest {
}
private void startUpload() {
uploader = new ContentUploader(file, request, endpoint)
def hasher = new FileHasher()
InfoHash infoHash = hasher.hashFile(file)
Pieces pieces = new Pieces(FileHasher.getPieceSize(file.length()))
for (int i = 0; i < pieces.nPieces; i++)
pieces.markDownloaded(i)
Mesh mesh = new Mesh(infoHash, pieces)
uploader = new ContentUploader(file, request, endpoint, mesh, FileHasher.getPieceSize(file.length()))
uploadThread = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true)
uploadThread.start()
@ -81,6 +90,7 @@ class UploaderTest {
startUpload()
assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN()
byte [] data = new byte[20]
@ -96,6 +106,7 @@ class UploaderTest {
startUpload()
assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN()
byte [] data = new byte[11]
@ -111,6 +122,7 @@ class UploaderTest {
request = new ContentRequest(range : new Range(0,20))
startUpload()
assert "416 Range Not Satisfiable" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN()
}
@ -123,6 +135,7 @@ class UploaderTest {
readUntilRN()
readUntilRN()
readUntilRN()
readUntilRN()
byte [] data = new byte[length]
DataInputStream dis = new DataInputStream(is)

View File

@ -1,5 +1,5 @@
group = com.muwire
version = 0.4.11
version = 0.5.0
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle'
apply from: 'gradle/code-quality.gradle'
apply from: 'gradle/integration-test.gradle'
// apply from: 'gradle/code-coverage.gradle'
// apply from: 'gradle/code-quality.gradle'
// apply from: 'gradle/integration-test.gradle'
// apply from: 'gradle/package.gradle'
apply from: 'gradle/docs.gradle'
apply plugin: 'com.github.johnrengelman.shadow'
@ -119,6 +119,7 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
}
}
/*
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
destinationFile = file("${buildDir}/jacoco/root.exec")
@ -138,4 +139,5 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
}
}
*/

BIN
gui/griffon-app/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -46,4 +46,14 @@ mvcGroups {
view = 'com.muwire.gui.ContentPanelView'
controller = 'com.muwire.gui.ContentPanelController'
}
'show-comment' {
model = 'com.muwire.gui.ShowCommentModel'
view = 'com.muwire.gui.ShowCommentView'
controller = 'com.muwire.gui.ShowCommentController'
}
'add-comment' {
model = 'com.muwire.gui.AddCommentModel'
view = 'com.muwire.gui.AddCommentView'
controller = 'com.muwire.gui.AddCommentController'
}
}

View File

@ -0,0 +1,39 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class AddCommentController {
@MVCMember @Nonnull
AddCommentModel model
@MVCMember @Nonnull
AddCommentView view
@ControllerAction
void save() {
String comment = view.textarea.getText()
if (comment.trim().length() == 0)
comment = null
else
comment = Base64.encode(DataUtil.encodei18nString(comment))
model.selectedFiles.each {
it.setComment(comment)
}
mvcGroup.parentGroup.view.refreshSharedFiles()
cancel()
}
@ControllerAction
void cancel() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -68,6 +68,16 @@ class ContentPanelController {
model.refresh()
}
@ControllerAction
void clearHits() {
int selectedRule = view.getSelectedRule()
if (selectedRule < 0)
return
Matcher matcher = model.rules[selectedRule]
matcher.matches.clear()
model.refresh()
}
@ControllerAction
void trust() {
int selectedHit = view.getSelectedHit()

View File

@ -13,10 +13,10 @@ import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
@ -25,6 +25,7 @@ import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList
@ -80,13 +81,14 @@ class MainFrameController {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
}
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
}
@ -269,22 +271,24 @@ class MainFrameController {
}
void unshareSelectedFile() {
SharedFile sf = view.selectedSharedFile()
def sf = view.selectedSharedFiles()
if (sf == null)
return
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
}
void stopWatchingDirectory() {
String directory = mvcGroup.view.getSelectedWatchedDirectory()
if (directory == null)
@ControllerAction
void addComment() {
def selectedFiles = view.selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return
core.muOptions.watchedDirectories.remove(directory)
saveMuWireSettings()
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory)))
model.watched.remove(directory)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
Map<String, Object> params = new HashMap<>()
params['selectedFiles'] = selectedFiles
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
}
void saveMuWireSettings() {

View File

@ -96,6 +96,10 @@ class OptionsController {
model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted)
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected()
model.searchExtraHop = searchExtraHop
settings.searchExtraHop = searchExtraHop
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
model.trustLists = trustLists
settings.allowTrustLists = trustLists
@ -139,7 +143,7 @@ class OptionsController {
// boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
// model.showSearchHashes = showSearchHashes
// uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
@ -163,5 +167,5 @@ class OptionsController {
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
}
}
}

View File

@ -8,63 +8,81 @@ import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
@ArtifactProviderFor(GriffonController)
class SearchTabController {
@MVCMember @Nonnull
SearchTabModel model
@MVCMember @Nonnull
SearchTabView view
Core core
private def selectedResult() {
int row = view.resultsTable.getSelectedRow()
if (row == -1)
private def selectedResults() {
int[] rows = view.resultsTable.getSelectedRows()
if (rows.length == 0)
return null
def sortEvt = view.lastSortEvent
if (sortEvt != null) {
row = view.resultsTable.rowSorter.convertRowIndexToModel(row)
for (int i = 0; i < rows.length; i++) {
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> results = new ArrayList<>()
rows.each { results.add(model.results[it]) }
results
}
model.results[row]
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return
if (!mvcGroup.parentGroup.model.canDownload(result.infohash))
return
@ControllerAction
void download() {
def results = selectedResults()
if (results == null)
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
results.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
mvcGroup.parentGroup.view.showDownloadsWindow.call()
}
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
@ControllerAction
void trust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
}
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
}
mvcGroup.parentGroup.view.showDownloadsWindow.call()
}
@ControllerAction
void distrust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
}
}
@ControllerAction
void trust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
}
@ControllerAction
void distrust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void neutral() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
}
}

View File

@ -0,0 +1,19 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class ShowCommentController {
@MVCMember @Nonnull
ShowCommentView view
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -43,7 +43,7 @@ class Initialize extends AbstractLifecycleHandler {
application.context.put("muwire-home", home.getAbsolutePath())
System.getProperties().setProperty("awt.useSystemAAFontSettings", "true")
System.getProperties().setProperty("awt.useSystemAAFontSettings", "gasp")
def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings
@ -86,6 +86,8 @@ class Initialize extends AbstractLifecycleHandler {
}
} else {
LookAndFeel chosen = lookAndFeel('system', 'gtk')
if (chosen == null)
chosen = lookAndFeel('metal')
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
}

View File

@ -0,0 +1,12 @@
package com.muwire.gui
import com.muwire.core.SharedFile
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class AddCommentModel {
List<SharedFile> selectedFiles
}

View File

@ -40,11 +40,15 @@ class ContentPanelModel {
}
void refresh() {
int selectedRule = view.getSelectedRule()
rules.clear()
rules.addAll(contentManager.matchers)
hits.clear()
view.rulesTable.model.fireTableDataChanged()
view.hitsTable.model.fireTableDataChanged()
if (selectedRule >= 0) {
view.rulesTable.selectionModel.setSelectionInterval(selectedRule,selectedRule)
}
}
void onContentControlEvent(ContentControlEvent e) {

View File

@ -1,6 +1,8 @@
package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Calendar
import java.util.UUID
@ -8,12 +10,16 @@ import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeNode
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.RouterDisconnectedEvent
import com.muwire.core.SharedFile
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
@ -21,6 +27,7 @@ import com.muwire.core.content.ContentControlEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
@ -57,6 +64,8 @@ class MainFrameModel {
FactoryBuilderSupport builder
@MVCMember @Nonnull
MainFrameController controller
@MVCMember @Nonnull
MainFrameView view
@Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false
@Observable boolean routerPresent
@ -64,8 +73,11 @@ class MainFrameModel {
def results = new ConcurrentHashMap<>()
def downloads = []
def uploads = []
def shared = []
def watched = []
boolean treeVisible = true
def shared
def sharedTree
def treeRoot
final Map<SharedFile, TreeNode> fileToNode = new HashMap<>()
def connectionList = []
def searches = new LinkedList()
def trusted = []
@ -81,6 +93,7 @@ class MainFrameModel {
@Observable boolean pauseButtonEnabled
@Observable boolean clearButtonEnabled
@Observable String resumeButtonText
@Observable boolean addCommentButtonEnabled
@Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled
@ -109,13 +122,22 @@ class MainFrameModel {
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
while(true) {
try {
downloadTable.model.fireTableDataChanged()
break
} catch (IllegalArgumentException iae) {} // caused by underlying model changing while table is sorted
}
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
void mvcGroupInit(Map<String, Object> args) {
uiSettings = application.context.get("ui-settings")
shared = []
treeRoot = new DefaultMutableTreeNode()
sharedTree = new DefaultTreeModel(treeRoot)
Timer timer = new Timer("download-pumper", true)
timer.schedule({
@ -191,7 +213,7 @@ class MainFrameModel {
return
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
retryInterval *= 1000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
@ -207,7 +229,7 @@ class MainFrameModel {
}
}
}, 60000, 60000)
}, 1000, 1000)
runInsideUIAsync {
trusted.addAll(core.trustService.good.values())
@ -227,9 +249,7 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
watched.addAll(core.muOptions.watchedDirectories)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
@ -297,7 +317,6 @@ class MainFrameModel {
void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile
}
}
@ -313,6 +332,8 @@ class MainFrameModel {
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.sharedFile)
loadedFiles = fileToNode.size()
}
}
@ -322,6 +343,8 @@ class MainFrameModel {
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.loadedFile)
loadedFiles = fileToNode.size()
}
}
@ -329,8 +352,26 @@ class MainFrameModel {
runInsideUIAsync {
shared.remove(e.unsharedFile)
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
def dmtn = fileToNode.remove(e.unsharedFile)
if (dmtn != null) {
loadedFiles = fileToNode.size()
while (true) {
def parent = dmtn.getParent()
parent.remove(dmtn)
if (parent == treeRoot)
break
if (parent.getChildCount() == 0) {
File file = parent.getUserObject().file
if (core.muOptions.watchedDirectories.contains(file.toString()))
core.eventBus.publish(new DirectoryUnsharedEvent(directory : parent.getUserObject().file))
dmtn = parent
continue
}
break
}
}
view.refreshSharedFiles()
}
}
@ -473,8 +514,44 @@ class MainFrameModel {
shared << e.downloadedFile
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.downloadedFile)
loadedFiles = fileToNode.size()
}
}
private void insertIntoTree(SharedFile file) {
List<File> parents = new ArrayList<>()
File tmp = file.file.getParentFile()
while(tmp.getParent() != null) {
parents << tmp
tmp = tmp.getParentFile()
}
Collections.reverse(parents)
TreeNode node = treeRoot
for(File path : parents) {
boolean exists = false
def children = node.children()
def child = null
while(children.hasMoreElements()) {
child = children.nextElement()
def userObject = child.getUserObject()
if (userObject != null && userObject.file == path) {
exists = true
break
}
}
if (!exists) {
child = new DefaultMutableTreeNode(new InterimTreeNode(path))
node.add(child)
}
node = child
}
def dmtn = new DefaultMutableTreeNode(file)
fileToNode.put(file, dmtn)
node.add(dmtn)
view.refreshSharedFiles()
}
private static class UIConnection {
Destination destination

View File

@ -38,6 +38,7 @@ class OptionsModel {
// trust options
@Observable boolean onlyTrusted
@Observable boolean searchExtraHop
@Observable boolean trustLists
@Observable String trustListInterval
@ -73,6 +74,7 @@ class OptionsModel {
}
onlyTrusted = !settings.allowUntrusted()
searchExtraHop = settings.searchExtraHop
trustLists = settings.allowTrustLists
trustListInterval = String.valueOf(settings.trustListInterval)
}

View File

@ -0,0 +1,12 @@
package com.muwire.gui
import com.muwire.core.search.UIResultEvent
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class ShowCommentModel {
UIResultEvent result
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

View File

@ -0,0 +1,68 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.swing.JDialog
import javax.swing.SwingConstants
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class AddCommentView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
AddCommentModel model
def mainFrame
def dialog
def p
def textarea
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
String title = "Add comment to multiple files"
String comment = ""
if (model.selectedFiles.size() == 1) {
title = "Add comments to " + model.selectedFiles[0].getFile().getName()
if (model.selectedFiles[0].comment != null)
comment = DataUtil.readi18nString(Base64.decode(model.selectedFiles[0].comment))
}
dialog = new JDialog(mainFrame, title, true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
scrollPane {
textarea = textArea(text : comment, rows : 20, columns : 100, editable : true)
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Save", saveAction)
button(text : "Cancel", cancelAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@ -84,6 +84,7 @@ class ContentPanelView {
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Refresh", refreshAction)
button(text : "Clear Hits", clearHitsAction)
button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}

View File

@ -16,13 +16,18 @@ import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.JTree
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.TransferHandler
import javax.swing.border.Border
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
import com.muwire.core.Constants
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.trust.RemoteTrustList
@ -33,6 +38,7 @@ import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
@ -53,16 +59,17 @@ class MainFrameView {
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
def lastWatchedSortEvent
def trustTablesSortEvents = [:]
UISettings settings
void initUI() {
UISettings settings = application.context.get("ui-settings")
settings = application.context.get("ui-settings")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"],
metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/MuWire-48x48.png').image,
iconImages: [imageIcon('/MuWire-48x48.png').image,
imageIcon('/MuWire-32x32.png').image,
@ -99,7 +106,7 @@ class MainFrameView {
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout()
label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) {
@ -158,8 +165,8 @@ class MainFrameView {
panel(constraints : "download-selected") {
gridBagLayout()
label(text : "Download Location:", constraints : gbc(gridx:0, gridy:0))
label(text : bind {model.downloader?.file?.getAbsolutePath()},
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
label(text : bind {model.downloader?.file?.getAbsolutePath()},
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
label(text : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, gridy:1))
label(text : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
@ -182,46 +189,55 @@ class MainFrameView {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text : bind {
if (model.hashingFile == null) {
""
} else {
"hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)"
}
})
if (model.hashingFile == null) {
"You can drag-and-drop files and directories here"
} else {
"hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)"
}
})
}
panel (border : etchedBorder(), constraints : BorderLayout.CENTER) {
gridLayout(cols : 2, rows : 1)
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "watched-directories-table", autoCreateRowSorter: true) {
tableModel(list : model.watched) {
closureColumn(header: "Watched Directories", type : String, read : { it })
borderLayout()
panel (id : "shared-files-panel", constraints : BorderLayout.CENTER){
cardLayout()
panel (constraints : "shared files table") {
borderLayout()
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.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
}
}
}
}
}
panel {
borderLayout()
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.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
}
panel (constraints : "shared files tree") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, jtree)
}
}
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2)
gridLayout(rows:1, cols:3)
panel {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
button(text : "Share files", actionPerformed : shareFiles)
buttonGroup(id : "sharedViewType")
radioButton(text : "Tree", selected : true, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTree)
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
}
panel {
label("Shared:")
label(text : bind {model.loadedFiles.toString()})
button(text : "Share files", actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
}
panel {
panel {
label("Shared:")
label(text : bind {model.loadedFiles}, id : "shared-files-count")
}
}
}
}
@ -305,8 +321,8 @@ class MainFrameView {
})
closureColumn(header : "Timestamp", type : String, read : {
String.format("%02d", it.timestamp.get(Calendar.HOUR_OF_DAY)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.MINUTE)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.SECOND))
String.format("%02d", it.timestamp.get(Calendar.MINUTE)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.SECOND))
})
}
}
@ -392,6 +408,22 @@ class MainFrameView {
}
void mvcGroupInit(Map<String, String> args) {
def mainFrame = builder.getVariable("main-frame")
mainFrame.setTransferHandler(new TransferHandler() {
public boolean canImport(TransferHandler.TransferSupport support) {
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
}
public boolean importData(TransferHandler.TransferSupport support) {
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
files.each {
model.core.eventBus.publish(new FileSharedEvent(file : it))
}
showUploadsWindow.call()
true
}
})
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
@ -407,7 +439,7 @@ class MainFrameView {
return
}
def downloader = model.downloads[selectedRow]?.downloader
if (downloader == null)
if (downloader == null)
return
model.downloader = downloader
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
@ -415,26 +447,26 @@ class MainFrameView {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true
model.pauseButtonEnabled = true
model.retryButtonEnabled = false
break
model.cancelButtonEnabled = true
model.pauseButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
model.resumeButtonText = "Retry"
model.pauseButtonEnabled = false
break
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
model.resumeButtonText = "Retry"
model.pauseButtonEnabled = false
break
case Downloader.DownloadState.PAUSED:
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
model.resumeButtonText = "Resume"
model.pauseButtonEnabled = false
break
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
model.resumeButtonText = "Resume"
model.pauseButtonEnabled = false
break
default:
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
model.pauseButtonEnabled = false
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
model.pauseButtonEnabled = false
}
})
@ -448,25 +480,19 @@ class MainFrameView {
downloadsTable.rowSorter.setComparator(2, new DownloaderComparator())
downloadsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
})
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
})
// shared files menu
JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
@ -474,17 +500,46 @@ class MainFrameView {
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFile()})
sharedFilesMenu.add(unshareSelectedFiles)
sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
sharedFilesMenu.add(commentSelectedFiles)
def sharedFilesMouseListener = new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
}
// shared files table and tree
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
sharedFilesTable.addMouseListener(sharedFilesMouseListener)
selectionModel = sharedFilesTable.getSelectionModel()
selectionModel.addListSelectionListener({
def selectedFiles = selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return
model.addCommentButtonEnabled = true
})
def sharedFilesTree = builder.getVariable("shared-files-tree")
sharedFilesTree.addMouseListener(sharedFilesMouseListener)
sharedFilesTree.addTreeSelectionListener({
def selectedNode = sharedFilesTree.getLastSelectedPathComponent()
model.addCommentButtonEnabled = selectedNode != null
})
// searches table
@ -503,38 +558,17 @@ class MainFrameView {
searchTableMenu.add(distrustSearcher)
searchesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
})
// watched directories table
def watchedTable = builder.getVariable("watched-directories-table")
watchedTable.rowSorter.addRowSorterListener({evt -> lastWatchedSortEvent = evt})
watchedTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu watchedMenu = new JPopupMenu()
JMenuItem stopWatching = new JMenuItem("Stop sharing")
stopWatching.addActionListener({mvcGroup.controller.stopWatchingDirectory()})
watchedMenu.add(stopWatching)
watchedTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(watchedMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(watchedMenu, e)
}
})
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
})
// subscription table
def subscriptionTable = builder.getVariable("subscription-table")
@ -557,20 +591,20 @@ class MainFrameView {
switch(trustList.status) {
case RemoteTrustList.Status.NEW:
case RemoteTrustList.Status.UPDATING:
model.reviewButtonEnabled = false
model.updateButtonEnabled = false
model.unsubscribeButtonEnabled = false
break
model.reviewButtonEnabled = false
model.updateButtonEnabled = false
model.unsubscribeButtonEnabled = false
break
case RemoteTrustList.Status.UPDATED:
model.reviewButtonEnabled = true
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
model.reviewButtonEnabled = true
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
case RemoteTrustList.Status.UPDATE_FAILED:
model.reviewButtonEnabled = false
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
model.reviewButtonEnabled = false
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
}
})
@ -609,28 +643,65 @@ class MainFrameView {
model.markNeutralFromDistrustedButtonEnabled = true
}
})
// show tree by default
showSharedFilesTree.call()
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY())
}
def selectedSharedFile() {
def sharedFilesTable = builder.getVariable("shared-files-table")
int selected = sharedFilesTable.getSelectedRow()
if (selected < 0)
return null
if (lastSharedSortEvent != null)
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
model.shared[selected]
def selectedSharedFiles() {
if (!model.treeVisible) {
def sharedFilesTable = builder.getVariable("shared-files-table")
int[] selected = sharedFilesTable.getSelectedRows()
if (selected.length == 0)
return null
List<SharedFile> rv = new ArrayList<>()
if (lastSharedSortEvent != null) {
for (int i = 0; i < selected.length; i ++) {
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i])
}
}
selected.each {
rv.add(model.shared[it])
}
return rv
} else {
def sharedFilesTree = builder.getVariable("shared-files-tree")
List<SharedFile> rv = new ArrayList<>()
for (TreePath path : sharedFilesTree.getSelectionPaths()) {
getLeafs(path.getLastPathComponent(), rv)
}
return rv
}
}
def copyHashToClipboard() {
def selected = selectedSharedFile()
if (selected == null)
private static void getLeafs(TreeNode node, List<SharedFile> dest) {
if (node.isLeaf()) {
dest.add(node.getUserObject())
return
String root = Base64.encode(selected.infoHash.getRoot())
StringSelection selection = new StringSelection(root)
}
def children = node.children()
while(children.hasMoreElements()) {
getLeafs(children.nextElement(), dest)
}
}
def copyHashToClipboard() {
def selectedFiles = selectedSharedFiles()
if (selectedFiles == null)
return
String roots = ""
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
SharedFile selected = iterator.next()
String root = Base64.encode(selected.infoHash.getRoot())
roots += root
if (iterator.hasNext())
roots += "\n"
}
StringSelection selection = new StringSelection(roots)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
@ -726,7 +797,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
}
def showDownloadsWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "downloads window")
@ -766,44 +837,34 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false
}
def showSharedFilesTable = {
model.treeVisible = false
def cardsPanel = builder.getVariable("shared-files-panel")
cardsPanel.getLayout().show(cardsPanel, "shared files table")
}
def showSharedFilesTree = {
model.treeVisible = true
def cardsPanel = builder.getVariable("shared-files-panel")
cardsPanel.getLayout().show(cardsPanel, "shared files tree")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
chooser.getSelectedFiles().each {
File canonical = it.getCanonicalFile()
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
}
}
}
def watchDirectories = {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
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))
}
}
String getSelectedWatchedDirectory() {
def watchedTable = builder.getVariable("watched-directories-table")
int selectedRow = watchedTable.getSelectedRow()
if (selectedRow < 0)
return null
if (lastWatchedSortEvent != null)
selectedRow = watchedTable.rowSorter.convertRowIndexToModel(selectedRow)
model.watched[selectedRow]
}
int getSelectedTrustTablesRow(String tableName) {
def table = builder.getVariable(tableName)
int selectedRow = table.getSelectedRow()
@ -813,4 +874,9 @@ class MainFrameView {
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
selectedRow
}
public void refreshSharedFiles() {
model.sharedTree.nodeStructureChanged(model.treeRoot)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
}

View File

@ -55,6 +55,7 @@ class OptionsView {
def outBwField
def allowUntrustedCheckbox
def searchExtraHopCheckbox
def allowTrustListsCheckbox
def trustListIntervalField
@ -70,7 +71,7 @@ class OptionsView {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
label(text : "seconds", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
@ -138,11 +139,13 @@ class OptionsView {
gridBagLayout()
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2))
label(text : "hours", constraints : gbc(gridx: 2, gridy:2))
label(text : "Search extra hop", constraints : gbc(gridx:0, gridy:1))
searchExtraHopCheckbox = checkBox(selected : bind {model.searchExtraHop}, constraints : gbc(gridx: 1, gridy : 1))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 2))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 2))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:3))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:3))
label(text : "hours", constraints : gbc(gridx: 2, gridy:3))
}

View File

@ -18,10 +18,13 @@ import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.Persona
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.Color
import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
@ -43,11 +46,13 @@ class SearchTabView {
def lastSendersSortEvent
def resultsTable
def lastSortEvent
def sequentialDownloadCheckbox
void initUI() {
builder.with {
def resultsTable
def sendersTable
def sequentialDownloadCheckbox
def pane = panel {
gridLayout(rows :1, cols : 1)
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
@ -66,6 +71,7 @@ class SearchTabView {
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
}
@ -78,11 +84,22 @@ class SearchTabView {
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
gridLayout(rows: 1, cols: 3)
panel()
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
}
panel {
gridBagLayout()
panel (constraints : gbc(gridx : 0, gridy : 0, weightx : 100))
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 1, gridy: 0, weightx : 0),selected : false, enabled : bind {model.downloadActionEnabled})
label(constraints: gbc(gridx: 2, gridy: 0, weightx : 0),text : "Download sequentially")
}
}
}
}
@ -94,18 +111,26 @@ class SearchTabView {
this.resultsTable = resultsTable
this.sendersTable = sendersTable
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener( {
int row = resultsTable.getSelectedRow()
if (row < 0) {
int[] rows = resultsTable.getSelectedRows()
if (rows.length == 0) {
model.downloadActionEnabled = false
return
}
if (lastSortEvent != null)
row = resultsTable.rowSorter.convertRowIndexToModel(row)
model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
})
}
}
@ -190,21 +215,38 @@ class SearchTabView {
def showPopupMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
boolean showMenu = false
if (model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.controller.download()})
menu.add(download)
showMenu = true
}
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
menu.show(e.getComponent(), e.getX(), e.getY())
if (resultsTable.getSelectedRows().length == 1) {
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
showMenu = true
// show comment if any
int selectedRow = resultsTable.getSelectedRow()
if (lastSortEvent != null)
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
if (model.results[selectedRow].comment != null) {
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({mvcGroup.view.showComment()})
menu.add(showComment)
}
}
if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY())
}
def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow()
if (selected < 0)
int[] selectedRows = resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
int selected = selectedRows[0]
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
@ -213,6 +255,23 @@ class SearchTabView {
clipboard.setContents(selection, null)
}
def showComment() {
int selectedRow = resultsTable.getSelectedRow()
if (selectedRow < 0)
return
if (lastSortEvent != null)
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
UIResultEvent event = model.results[selectedRow]
if (event.comment == null)
return
String groupId = Base64.encode(event.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = event
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
int selectedSenderRow() {
int row = sendersTable.getSelectedRow()
if (row < 0)

View File

@ -0,0 +1,61 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.swing.JDialog
import javax.swing.SwingConstants
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class ShowCommentView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
ShowCommentModel model
def mainFrame
def dialog
def p
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.result.name, true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
scrollPane {
textArea(text : model.result.comment, rows : 20, columns : 100, editable : false)
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Dismiss", dismissAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@ -0,0 +1,18 @@
package com.muwire.gui
class InterimTreeNode {
private final File file
InterimTreeNode(File file) {
this.file = file
}
public boolean equals(Object o) {
if (!(o instanceof InterimTreeNode))
return false
file == o.file
}
public String toString() {
file.getName()
}
}

View File

@ -0,0 +1,44 @@
package com.muwire.gui
import java.awt.Component
import javax.swing.ImageIcon
import javax.swing.JTree
import javax.swing.tree.DefaultTreeCellRenderer
import com.muwire.core.SharedFile
import net.i2p.data.DataHelper
class SharedTreeRenderer extends DefaultTreeCellRenderer {
private final ImageIcon commentIcon
SharedTreeRenderer() {
commentIcon = new ImageIcon((URL) SharedTreeRenderer.class.getResource("/comment.png"))
}
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
def userObject = value.getUserObject()
def defaultRenderer = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
if (userObject instanceof InterimTreeNode || userObject == null)
return defaultRenderer
SharedFile sf = (SharedFile) userObject
String name = sf.getFile().getName()
long length = sf.getCachedLength()
String formatted = DataHelper.formatSize2Decimal(length, false)+"B"
setText("$name ($formatted)")
setEnabled(true)
if (sf.comment != null) {
setIcon(commentIcon)
}
this
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(ContentPanelController)
class ContentPanelControllerTest {
private ContentPanelController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(EventListController)
class EventListControllerTest {
private EventListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(I2PStatusController)
class I2PStatusControllerTest {
private I2PStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MainFrameController)
class MainFrameControllerTest {
private MainFrameController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MuWireStatusController)
class MuWireStatusControllerTest {
private MuWireStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(SearchTabController)
class SearchTabControllerTest {
private SearchTabController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(TrustListController)
class TrustListControllerTest {
private TrustListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,3 +1,8 @@
apply plugin : 'application'
mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
}

View File

@ -9,6 +9,7 @@ import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.crypto.SigType
@Log
@ -28,7 +29,7 @@ class UpdateServer {
def session
if (!keyFile.exists()) {
def os = new FileOutputStream(keyFile);
myDest = i2pClient.createDestination(os)
myDest = i2pClient.createDestination(os, SigType.EdDSA_SHA512_Ed25519)
os.close()
log.info "No key.dat file was found, so creating a new destination."
log.info "This is the destination you want to give out for your new UpdateServer"