Compare commits

..

31 Commits

Author SHA1 Message Date
Zlatin Balevsky
e69a5eac18 0.0.6 2019-06-03 18:30:27 +01:00
Zlatin Balevsky
6e0f1778b7 rudimentary speed gauge 2019-06-03 18:02:10 +01:00
Zlatin Balevsky
abbb741d73 show the number of sources for a result, counted by infohash 2019-06-03 17:21:08 +01:00
Zlatin Balevsky
07dfc0a1d1 destroy mvc group on options window close 2019-06-03 15:33:16 +01:00
Zlatin Balevsky
00c12cfd49 hook up download retry logic 2019-06-03 15:02:04 +01:00
Zlatin Balevsky
1ee389ff91 options dialog 2019-06-03 14:40:32 +01:00
Zlatin Balevsky
3642736cfe options dialog, wip 2019-06-03 11:32:34 +01:00
Zlatin Balevsky
b6f7f51476 verify X-Persona header if present 2019-06-03 08:12:33 +01:00
Zlatin Balevsky
4c21f2d5ae show full persona in searches 2019-06-03 08:06:51 +01:00
Zlatin Balevsky
9e0d52d548 show source in incoming searches 2019-06-03 07:43:28 +01:00
Zlatin Balevsky
fad01603de fix replyTo field 2019-06-03 07:35:09 +01:00
Zlatin Balevsky
da007795fb learn about new hosts from incoming connections too 2019-06-03 07:27:12 +01:00
Zlatin Balevsky
881d755dd3 update test work with personas 2019-06-02 22:47:43 +01:00
Zlatin Balevsky
bc3b6f500f 0.0.5 for trust panel 2019-06-02 12:18:44 +01:00
Zlatin Balevsky
8f8710801c update any result tabs on trust events 2019-06-02 12:16:28 +01:00
Zlatin Balevsky
43f3cf9b7a small ui tweak 2019-06-02 12:00:14 +01:00
Zlatin Balevsky
6fe4155678 delete accidental commit 2019-06-02 11:57:15 +01:00
Zlatin Balevsky
32f944a089 trust panel ui 2019-06-02 11:56:19 +01:00
Zlatin Balevsky
b19b5ef315 Fix for java 9+ #1 2019-06-02 10:04:27 +01:00
Zlatin Balevsky
5138935c20 add options for portable installation, issue #2 2019-06-02 09:33:28 +01:00
Zlatin Balevsky
ba596af778 Trust panel, wip 2019-06-02 05:40:44 +01:00
Zlatin Balevsky
0f4533c867 persist personas in trust files instead of destinations 2019-06-02 05:12:14 +01:00
Zlatin Balevsky
727834390c slightly better looking message 2019-06-02 04:18:15 +01:00
Zlatin Balevsky
c51e3874da show a message instead of search bar while disconnected 2019-06-02 04:12:11 +01:00
Zlatin Balevsky
d18a618575 focus on the tab of the new search 2019-06-02 03:54:34 +01:00
Zlatin Balevsky
15508f417d hack to add some horizontal space 2019-06-02 01:33:53 +01:00
Zlatin Balevsky
44dad55178 update test 2019-06-02 01:28:00 +01:00
Zlatin Balevsky
5c17e77190 change groovy version to match griffon 2019-06-02 01:20:55 +01:00
Zlatin Balevsky
de856cd085 canonize search terms 2019-06-02 00:42:18 +01:00
Zlatin Balevsky
d2533cc4d6 retry failed downloads, every 15 minutes by default 2019-06-02 00:22:33 +01:00
Zlatin Balevsky
f41cc39659 show who is downloading 2019-06-01 21:53:14 +01:00
34 changed files with 541 additions and 90 deletions

View File

@@ -3,7 +3,7 @@ subprojects {
dependencies { dependencies {
compile 'net.i2p:i2p:0.9.40' compile 'net.i2p:i2p:0.9.40'
compile 'org.codehaus.groovy:groovy-all:2.5.7' compile 'org.codehaus.groovy:groovy-all:2.4.15'
} }
compileGroovy { compileGroovy {

View File

@@ -10,4 +10,6 @@ class Constants {
public static final int MAX_HEADERS = 16 public static final int MAX_HEADERS = 16
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]"
} }

View File

@@ -120,8 +120,8 @@ public class Core {
eventBus = new EventBus() eventBus = new EventBus()
log.info("initializing trust service") log.info("initializing trust service")
File goodTrust = new File(home, "trust.good") File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "trust.bad") File badTrust = new File(home, "distrusted")
trustService = new TrustService(goodTrust, badTrust, 5000) trustService = new TrustService(goodTrust, badTrust, 5000)
eventBus.register(TrustEvent.class, trustService) eventBus.register(TrustEvent.class, trustService)
@@ -166,7 +166,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager) eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager") log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes")) DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
eventBus.register(UIDownloadEvent.class, downloadManager) eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager") log.info("initializing upload manager")

View File

@@ -6,6 +6,7 @@ class MuWireSettings {
final boolean isLeaf final boolean isLeaf
boolean allowUntrusted boolean allowUntrusted
int downloadRetryInterval
String nickname String nickname
File downloadLocation File downloadLocation
String sharedFiles String sharedFiles
@@ -23,6 +24,7 @@ class MuWireSettings {
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles") sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -32,6 +34,7 @@ class MuWireSettings {
props.setProperty("crawlerResponse", crawlerResponse.toString()) props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname) props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath()) props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
if (sharedFiles != null) if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles) props.setProperty("sharedFiles", sharedFiles)
props.store(out, "") props.store(out, "")

View File

@@ -2,6 +2,7 @@ package com.muwire.core
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType import net.i2p.crypto.SigType
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.data.Signature import net.i2p.data.Signature
import net.i2p.data.SigningPublicKey import net.i2p.data.SigningPublicKey
@@ -14,6 +15,7 @@ public class Persona {
private final Destination destination private final Destination destination
private final byte[] sig private final byte[] sig
private volatile String humanReadableName private volatile String humanReadableName
private volatile String base64
private volatile byte[] payload private volatile byte[] payload
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException { public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
@@ -59,6 +61,15 @@ public class Persona {
humanReadableName humanReadableName
} }
public String toBase64() {
if (base64 == null) {
def baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
base64
}
@Override @Override
public int hashCode() { public int hashCode() {
name.hashCode() ^ destination.hashCode() name.hashCode() ^ destination.hashCode()

View File

@@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level import java.util.logging.Level
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
@@ -14,6 +15,7 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
@Log @Log
@@ -120,9 +122,10 @@ abstract class Connection implements Closeable {
query.version = 1 query.version = 1
query.uuid = e.searchEvent.getUuid() query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop query.firstHop = e.firstHop
// TODO: first hop figure out
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64() query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
messages.put(query) messages.put(query)
} }
@@ -158,11 +161,22 @@ abstract class Connection implements Closeable {
} }
// TODO: add option to respond only to trusted peers // TODO: add option to respond only to trusted peers
Persona originator = null
if (search.originator != null) {
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
if (originator.destination != replyTo) {
log.info("originator doesn't match destination")
return
}
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : search.infohash, searchHash : search.infohash,
uuid : uuid) uuid : uuid)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination, receivedOn : endpoint.destination,
firstHop : search.firstHop ) firstHop : search.firstHop )
eventBus.publish(event) eventBus.publish(event)

View File

@@ -40,7 +40,7 @@ abstract class ConnectionManager {
void onTrustEvent(TrustEvent e) { void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.DISTRUSTED) if (e.level == TrustLevel.DISTRUSTED)
drop(e.destination) drop(e.persona.destination)
} }
abstract void drop(Destination d) abstract void drop(Destination d)

View File

@@ -1,7 +1,11 @@
package com.muwire.core.download package com.muwire.core.download
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import net.i2p.data.Base64
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.Persona
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -12,12 +16,19 @@ public class DownloadManager {
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File incompletes private final File incompletes
private final String meB64
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes) { public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.connector = connector this.connector = connector
this.incompletes = incompletes this.incompletes = incompletes
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
incompletes.mkdir() incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r -> this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r) Thread rv = new Thread(r)
rv.setName("download-worker") rv.setName("download-worker")
@@ -28,7 +39,7 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) { public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(this, e.target, e.result.size, def downloader = new Downloader(this, meB64, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination, e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
incompletes) incompletes)
executor.execute({downloader.download()} as Runnable) executor.execute({downloader.download()} as Runnable)

View File

@@ -20,6 +20,9 @@ import java.security.NoSuchAlgorithmException
@Log @Log
class DownloadSession { class DownloadSession {
private static int SAMPLES = 10
private final String meB64
private final Pieces pieces private final Pieces pieces
private final InfoHash infoHash private final InfoHash infoHash
private final Endpoint endpoint private final Endpoint endpoint
@@ -28,10 +31,14 @@ class DownloadSession {
private final long fileLength private final long fileLength
private final MessageDigest digest private final MessageDigest digest
private final ArrayDeque<Long> timestamps = new ArrayDeque<>(SAMPLES)
private final ArrayDeque<Integer> reads = new ArrayDeque<>(SAMPLES)
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) { int pieceSize, long fileLength) {
this.meB64 = meB64
this.pieces = pieces this.pieces = pieces
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
@@ -60,7 +67,8 @@ class DownloadSession {
FileChannel channel FileChannel channel
try { try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
String code = readTillRN(is) String code = readTillRN(is)
if (code.startsWith("404 ")) { if (code.startsWith("404 ")) {
@@ -119,6 +127,13 @@ class DownloadSession {
throw new IOException() throw new IOException()
synchronized(this) { synchronized(this) {
mapped.put(tmp, 0, read) mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
} }
} }
@@ -141,4 +156,13 @@ class DownloadSession {
return 0 return 0
mapped.position() mapped.position()
} }
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
long interval = timestamps.last - timestamps.first
int totalRead = 0
reads.each { totalRead += it }
(int)(totalRead * 1000.0 / interval)
}
} }

View File

@@ -16,6 +16,7 @@ public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED } public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final String meB64
private final File file private final File file
private final Pieces pieces private final Pieces pieces
private final long length private final long length
@@ -32,9 +33,10 @@ public class Downloader {
private volatile boolean cancelled private volatile boolean cancelled
private volatile Thread downloadThread private volatile Thread downloadThread
public Downloader(DownloadManager downloadManager, File file, long length, InfoHash infoHash, public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination, int pieceSizePow2, I2PConnector connector, Destination destination,
File incompletes) { File incompletes) {
this.meB64 = meB64
this.downloadManager = downloadManager this.downloadManager = downloadManager
this.file = file this.file = file
this.infoHash = infoHash this.infoHash = infoHash
@@ -63,7 +65,7 @@ public class Downloader {
endpoint = connector.connect(destination) endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) { while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length) currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request() currentSession.request()
writePieces() writePieces()
} }
@@ -107,6 +109,12 @@ public class Downloader {
currentSession.positionInPiece() currentSession.positionInPiece()
} }
public int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
public DownloadState getCurrentState() { public DownloadState getCurrentState() {
currentState currentState
} }
@@ -117,6 +125,7 @@ public class Downloader {
} }
public void resume() { public void resume() {
currentState = DownloadState.CONNECTING
downloadManager.resume(this) downloadManager.resume(this)
} }
} }

View File

@@ -55,7 +55,7 @@ class HostCache extends Service {
} }
void onConnectionEvent(ConnectionEvent e) { void onConnectionEvent(ConnectionEvent e) {
if (e.incoming || e.leaf) if (e.leaf)
return return
Destination dest = e.endpoint.destination Destination dest = e.endpoint.destination
Host host = hosts.get(dest) Host host = hosts.get(dest)

View File

@@ -1,6 +1,7 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.Persona
import net.i2p.data.Destination import net.i2p.data.Destination
@@ -9,6 +10,7 @@ class QueryEvent extends Event {
SearchEvent searchEvent SearchEvent searchEvent
boolean firstHop boolean firstHop
Destination replyTo Destination replyTo
Persona originator
Destination receivedOn Destination receivedOn
} }

View File

@@ -1,5 +1,6 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Constants
class SearchIndex { class SearchIndex {
@@ -31,7 +32,7 @@ class SearchIndex {
} }
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll("[\\.,_-]", " ") source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ") source.split(" ")
} }

View File

@@ -1,11 +1,10 @@
package com.muwire.core.trust package com.muwire.core.trust
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.Persona
import net.i2p.data.Destination
class TrustEvent extends Event { class TrustEvent extends Event {
Destination destination Persona persona
TrustLevel level TrustLevel level
} }

View File

@@ -1,7 +1,11 @@
package com.muwire.core.trust package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import com.muwire.core.Service import com.muwire.core.Service
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet import net.i2p.util.ConcurrentHashSet
@@ -10,8 +14,8 @@ class TrustService extends Service {
final File persistGood, persistBad final File persistGood, persistBad
final long persistInterval final long persistInterval
final Set<Destination> good = new ConcurrentHashSet<>() final Map<Destination, Persona> good = new ConcurrentHashMap<>()
final Set<Destination> bad = new ConcurrentHashSet<>() final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
final Timer timer final Timer timer
@@ -35,12 +39,16 @@ class TrustService extends Service {
void load() { void load() {
if (persistGood.exists()) { if (persistGood.exists()) {
persistGood.eachLine { persistGood.eachLine {
good.add(new Destination(it)) byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, persona)
} }
} }
if (persistBad.exists()) { if (persistBad.exists()) {
persistBad.eachLine { persistBad.eachLine {
bad.add(new Destination(it)) byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, persona)
} }
} }
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval) timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@@ -50,22 +58,22 @@ class TrustService extends Service {
private void persist() { private void persist() {
persistGood.delete() persistGood.delete()
persistGood.withPrintWriter { writer -> persistGood.withPrintWriter { writer ->
good.each { good.each {k,v ->
writer.println it.toBase64() writer.println v.toBase64()
} }
} }
persistBad.delete() persistBad.delete()
persistBad.withPrintWriter { writer -> persistBad.withPrintWriter { writer ->
bad.each { bad.each { k,v ->
writer.println it.toBase64() writer.println v.toBase64()
} }
} }
} }
TrustLevel getLevel(Destination dest) { TrustLevel getLevel(Destination dest) {
if (good.contains(dest)) if (good.containsKey(dest))
return TrustLevel.TRUSTED return TrustLevel.TRUSTED
else if (bad.contains(dest)) else if (bad.containsKey(dest))
return TrustLevel.DISTRUSTED return TrustLevel.DISTRUSTED
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
@@ -73,16 +81,16 @@ class TrustService extends Service {
void onTrustEvent(TrustEvent e) { void onTrustEvent(TrustEvent e) {
switch(e.level) { switch(e.level) {
case TrustLevel.TRUSTED: case TrustLevel.TRUSTED:
bad.remove(e.destination) bad.remove(e.persona.destination)
good.add(e.destination) good.put(e.persona.destination, e.persona)
break break
case TrustLevel.DISTRUSTED: case TrustLevel.DISTRUSTED:
good.remove(e.destination) good.remove(e.persona.destination)
bad.add(e.destination) bad.put(e.persona.destination, e.persona)
break break
case TrustLevel.NEUTRAL: case TrustLevel.NEUTRAL:
good.remove(e.destination) good.remove(e.persona.destination)
bad.remove(e.destination) bad.remove(e.persona.destination)
break break
} }
} }

View File

@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -16,6 +17,7 @@ class Request {
InfoHash infoHash InfoHash infoHash
Range range Range range
Persona downloader
Map<String, String> headers Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException { static Request parse(InfoHash infoHash, InputStream is) throws IOException {
@@ -85,7 +87,13 @@ class Request {
if (start < 0 || end < start) if (start < 0 || end < start)
throw new IOException("Invalid range $start - $end") throw new IOException("Invalid range $start - $end")
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers) Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
} }
} }

View File

@@ -62,6 +62,11 @@ public class UploadManager {
} }
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream()) Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e) Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {

View File

@@ -0,0 +1,11 @@
package com.muwire.core
import net.i2p.data.Base64
class Personas {
private final String encoded1 = "AQADemFiO~pgSoEo8wQfwncYMvBQWkvPY9I7DYUllHp289UE~zBaLdbl~wbliktAUsW-S70f3UeYgHq34~c7zVuUQjgHZ506iG9hX8B9S3a9gQ3CSG0GuDpeNyiXmZkpHp5m8vT9PZ1zMWzxvzZY~fP9yKFKgO4yrso5I9~DGOPeyJZJ4BFsTJDERv41aZqjFLYUBDmeHGgg9RjYy~93h-nQMVYj9JSO3AgowW-ix49rtiKYIXHMa2PxWHUXkUHWJZtIZntNIDEFeMnPdzLxjAl8so2G6pDcTMZPLLwyb73Ee5ZVfxUynPqyp~fIGVP8Rl4rlaGFli2~ATGBz3XY54aObC~0p7us2JnWaTC~oQT5DVDM7gaOO885o-m8BB8b0duzMBelbdnMZFQJ5jIHVKxkC6Niw4fxTOoXTyOqQmVhtK-9xcwxMuN5DF9IewkR5bhpq5rgnfBP5zvyBaAHMq-d3TCOjTsZ-d3liB98xX5p8G5zmS7gfKArQtM5~CcK~AlX-lGLBQAEAAcAAN5MW1Tq983szfZgY1l8tQFqy8I9tdMf7vc1Ktj~TCIvXYw6AYMbMGy3S67FSPLZVmfHEMQKj2KLAdaRKQkHPAY"
private final String encoded2 = "AQAHemxhdGluYiN~3G-hPoBfJ04mhcC52lC6TYSwWxH-WNWno9Y35JS-WrXlnPsodZtwy96ttEaiKTg-hkRqMsaYKpWar1FwayR6qlo0pZCo5pQOLfR7GIM3~wde0JIBEp8BUpgzF1-QXLhuRG1t7tBbenW2tSgp5jQH61RI-c9flyUlOvf6nrhQMZ3aoviZ4aZW23Fx-ajYQBDk7PIxuyn8qYNwWy3kWOhGan05c54NnumS3XCzQWFDDPlADmco1WROeY9qrwwtmLM8lzDCEtJQXJlk~K5yLbyB63hmAeTK7J4iS6f9nnWv7TbB5r-Z3kC6D9TLYrQbu3h4AAxrqso45P8yHQtKUA4QJicS-6NJoBOnlCCU887wx2k9YSxxwNydlIxb1mZsX65Ke4uY0HDFokZHTzUcxvfLB6G~5JkSPDCyZz~2fREgW2-VXu7gokEdEugkuZRrsiQzyfAOOkv53ti5MzTbMOXinBskSb1vZyN2-XcZNaDJvEqUNj~qpfhe-ov2F7FuwQUABAAHAAAfqq-MneIqWBQY92-sy9Z0s~iQsq6lUFa~sYMdY-5o-94fF8a140dm-emF3rO8vuidUIPNaS-37Rl05mAKUCcB"
Persona persona1 = new Persona(new ByteArrayInputStream(Base64.decode(encoded1)))
Persona persona2 = new Persona(new ByteArrayInputStream(Base64.decode(encoded2)))
}

View File

@@ -55,7 +55,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader) toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null) endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(pieces, infoHash, endpoint, target, pieceSize, size) session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable) downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true) downloadThread.setDaemon(true)
downloadThread.start() downloadThread.start()
@@ -74,6 +74,7 @@ class DownloadSessionTest {
initSession(20) initSession(20)
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader) assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -95,6 +96,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -122,6 +124,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) || assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize)) (start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)

View File

@@ -5,14 +5,17 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import com.muwire.core.Destinations import com.muwire.core.Destinations
import com.muwire.core.Persona
import com.muwire.core.Personas
import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
class TrustServiceTest { class TrustServiceTest {
TrustService service TrustService service
File persistGood, persistBad File persistGood, persistBad
Destinations dests = new Destinations() Personas personas = new Personas()
@Before @Before
void before() { void before() {
@@ -33,51 +36,50 @@ class TrustServiceTest {
@Test @Test
void testEmpty() { void testEmpty() {
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest1) assert TrustLevel.NEUTRAL == service.getLevel(personas.persona1.destination)
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest2) assert TrustLevel.NEUTRAL == service.getLevel(personas.persona2.destination)
} }
@Test @Test
void testOnEvent() { void testOnEvent() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1) service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2) service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1) assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2) assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
} }
@Test @Test
void testPersist() { void testPersist() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1) service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2) service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250) Thread.sleep(250)
def trusted = new HashSet<>() def trusted = new HashSet<>()
persistGood.eachLine { persistGood.eachLine {
trusted.add(new Destination(it)) trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
} }
def distrusted = new HashSet<>() def distrusted = new HashSet<>()
persistBad.eachLine { persistBad.eachLine {
distrusted.add(new Destination(it)) distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
} }
assert trusted.size() == 1 assert trusted.size() == 1
assert trusted.contains(dests.dest1) assert trusted.contains(personas.persona1)
assert distrusted.size() == 1 assert distrusted.size() == 1
assert distrusted.contains(dests.dest2) assert distrusted.contains(personas.persona2)
} }
@Test @Test
void testLoad() { void testLoad() {
service.stop() service.stop()
persistGood.append("${dests.dest1.toBase64()}\n") persistGood.append("${personas.persona1.toBase64()}\n")
persistBad.append("${dests.dest2.toBase64()}\n") persistBad.append("${personas.persona2.toBase64()}\n")
service = new TrustService(persistGood, persistBad, 100) service = new TrustService(persistGood, persistBad, 100)
service.start() service.start()
Thread.sleep(10) Thread.sleep(50)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
} }
} }

View File

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

View File

@@ -58,6 +58,7 @@ dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}" compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}" runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}" testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}" testCompile "org.spockframework:spock-core:${spockVersion}"

View File

@@ -21,4 +21,9 @@ mvcGroups {
view = 'com.muwire.gui.SearchTabView' view = 'com.muwire.gui.SearchTabView'
controller = 'com.muwire.gui.SearchTabController' controller = 'com.muwire.gui.SearchTabController'
} }
'Options' {
model = 'com.muwire.gui.OptionsModel'
view = 'com.muwire.gui.OptionsView'
controller = 'com.muwire.gui.OptionsController'
}
} }

View File

@@ -10,6 +10,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
@@ -31,6 +32,9 @@ class MainFrameController {
@ControllerAction @ControllerAction
void search() { void search() {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text def search = builder.getVariable("search-field").text
def uuid = UUID.randomUUID() def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>() Map<String, Object> params = new HashMap<>()
@@ -39,9 +43,12 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params) def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchTerms : [search], uuid : uuid) // this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination)) replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
} }
private def selectedResult() { private def selectedResult() {
@@ -73,7 +80,7 @@ class MainFrameController {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return // TODO disable button return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED)) core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
} }
@ControllerAction @ControllerAction
@@ -81,7 +88,7 @@ class MainFrameController {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return // TODO disable button return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED)) core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
} }
@ControllerAction @ControllerAction
@@ -96,6 +103,33 @@ class MainFrameController {
downloader.resume() downloader.resume()
} }
private void markTrust(String tableName, TrustLevel level, def list) {
int row = builder.getVariable(tableName).getSelectedRow()
if (row < 0)
return
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
}
@ControllerAction
void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
}
@ControllerAction
void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
}
@ControllerAction
void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
}
@ControllerAction
void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e-> application.addPropertyChangeListener("core", {e->
core = e.getNewValue() core = e.getNewValue()

View File

@@ -0,0 +1,37 @@
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 OptionsController {
@MVCMember @Nonnull
OptionsModel model
@MVCMember @Nonnull
OptionsView view
@ControllerAction
void save() {
String text = view.retryField.text
model.downloadRetryInterval = text
def settings = application.context.get("muwire-settings")
settings.downloadRetryInterval = Integer.valueOf(text)
File settingsFile = new File(application.context.get("core").home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
cancel()
}
@ControllerAction
void cancel() {
view.d.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -28,7 +28,11 @@ class Ready extends AbstractLifecycleHandler {
@Override @Override
void execute() { void execute() {
log.info "starting core services" log.info "starting core services"
def home = System.getProperty("user.home") + File.separator + ".MuWire" def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
portableHome
home = new File(home) home = new File(home)
if (!home.exists()) { if (!home.exists()) {
log.info("creating home dir") log.info("creating home dir")
@@ -64,8 +68,13 @@ class Ready extends AbstractLifecycleHandler {
nickname = nickname.trim() nickname = nickname.trim()
break break
} }
props.setNickname(nickname)
while(true) {
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setDialogTitle("Select a directory where downloads will be saved") chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
@@ -75,9 +84,8 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0) System.exit(0)
} }
props.downloadLocation = chooser.getSelectedFile() props.downloadLocation = chooser.getSelectedFile()
break
} }
props.setNickname(nickname)
propsFile.withOutputStream { propsFile.withOutputStream {
props.write(it) props.write(it)
} }

View File

@@ -8,10 +8,12 @@ import javax.swing.JTable
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
@@ -44,6 +46,8 @@ class MainFrameModel {
def shared = [] def shared = []
def connectionList = [] def connectionList = []
def searches = new LinkedList() def searches = new LinkedList()
def trusted = []
def distrusted = []
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@@ -55,7 +59,30 @@ class MainFrameModel {
volatile Core core volatile Core core
private long lastRetryTime = System.currentTimeMillis()
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
void mvcGroupInit(Map<String, Object> args) { void mvcGroupInit(Map<String, Object> args) {
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
}
}, 1000, 1000)
application.addPropertyChangeListener("core", {e -> application.addPropertyChangeListener("core", {e ->
coreInitialized = (e.getNewValue() != null) coreInitialized = (e.getNewValue() != null)
core = e.getNewValue() core = e.getNewValue()
@@ -70,20 +97,32 @@ class MainFrameModel {
core.eventBus.register(UploadFinishedEvent.class, this) core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this) core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this) core.eventBus.register(QueryEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
def downloadTable = builder.getVariable("downloads-table") timer.schedule({
int selectedRow = downloadTable.getSelectedRow() int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
downloadTable.model.fireTableDataChanged() if (retryInterval > 0) {
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow) retryInterval *= 60000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
runInsideUIAsync {
downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume()
updateTablePreservingSelection("downloads-table")
} }
}, 1000, 1000) }
}
}
}, 60000, 60000)
runInsideUIAsync {
trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values())
}
})
} }
void onUIResultEvent(UIResultEvent e) { void onUIResultEvent(UIResultEvent e) {
@@ -103,6 +142,11 @@ class MainFrameModel {
runInsideUIAsync { runInsideUIAsync {
connections = core.connectionManager.getConnections().size() connections = core.connectionManager.getConnections().size()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination) connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table") JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
@@ -112,6 +156,12 @@ class MainFrameModel {
void onDisconnectionEvent(DisconnectionEvent e) { void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync { runInsideUIAsync {
connections = core.connectionManager.getConnections().size() connections = core.connectionManager.getConnections().size()
if (connections == 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination) connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table") JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
@@ -160,8 +210,18 @@ class MainFrameModel {
void onTrustEvent(TrustEvent e) { void onTrustEvent(TrustEvent e) {
runInsideUIAsync { runInsideUIAsync {
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged() trusted.clear()
trusted.addAll(core.trustService.good.values())
distrusted.clear()
distrusted.addAll(core.trustService.bad.values())
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
} }
} }
@@ -177,11 +237,17 @@ class MainFrameModel {
if (search.trim().size() == 0) if (search.trim().size() == 0)
return return
runInsideUIAsync { runInsideUIAsync {
searches.addFirst(search) searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200) while(searches.size() > 200)
searches.removeLast() searches.removeLast()
JTable table = builder.getVariable("searches-table") JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
} }
} }
class IncomingSearch {
String search
Destination replyTo
Persona originator
}
} }

View File

@@ -0,0 +1,14 @@
package com.muwire.gui
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
void mvcGroupInit(Map<String, String> args) {
downloadRetryInterval = application.context.get("muwire-settings").downloadRetryInterval
}
}

View File

@@ -21,6 +21,7 @@ class SearchTabModel {
Core core Core core
String uuid String uuid
def results = [] def results = []
def hashCount = [:]
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
@@ -34,6 +35,12 @@ class SearchTabModel {
void handleResult(UIResultEvent e) { void handleResult(UIResultEvent e) {
runInsideUIAsync { runInsideUIAsync {
Integer count = hashCount.get(e.infohash)
if (count == null)
count = 0
count++
hashCount[e.infohash] = count
results << e results << e
JTable table = builder.getVariable("results-table") JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()

View File

@@ -22,6 +22,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints import java.awt.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.Insets import java.awt.Insets
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull import javax.annotation.Nonnull
@@ -43,6 +44,11 @@ class MainFrameView {
imageIcon('/griffon-icon-16x16.png').image], imageIcon('/griffon-icon-16x16.png').image],
pack : false, pack : false,
visible : bind { model.coreInitialized }) { visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
}
}
borderLayout() borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) { panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
borderLayout() borderLayout()
@@ -51,10 +57,17 @@ class MainFrameView {
button(text: "Searches", actionPerformed : showSearchWindow) button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow) button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow) button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
} }
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
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) { panel(constraints: BorderLayout.CENTER) {
borderLayout() borderLayout()
label("Enter search here:", constraints: BorderLayout.WEST) label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction) textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
} }
@@ -62,6 +75,8 @@ class MainFrameView {
button(text: "Search", searchAction) button(text: "Search", searchAction)
} }
} }
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) { panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout() cardLayout()
panel (constraints : "search window") { panel (constraints : "search window") {
@@ -94,6 +109,7 @@ class MainFrameView {
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
"$position/$pieceSize bytes" "$position/$pieceSize bytes"
}) })
closureColumn(header: "Speed (bytes/second)", type:Integer, read :{row -> row.downloader.speed()})
} }
} }
} }
@@ -136,6 +152,9 @@ class MainFrameView {
int percent = (int)((position * 100.0) / total) int percent = (int)((position * 100.0) / total)
"$percent%" "$percent%"
}) })
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
})
} }
} }
} }
@@ -164,12 +183,52 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") { table(id : "searches-table") {
tableModel(list : model.searches) { tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : { it }) closureColumn(header : "Keywords", type : String, read : { it.search })
closureColumn(header : "From", type : String, read : {
if (it.originator != null) {
return it.originator.getHumanReadableName()
} else {
return it.replyTo.toBase32()
}
})
} }
} }
} }
} }
} }
panel(constraints : "trust window") {
gridLayout(rows: 1, cols :2)
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel (constraints : BorderLayout.EAST) {
gridBagLayout()
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
}
}
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel(constraints : BorderLayout.WEST) {
gridBagLayout()
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
}
}
}
} }
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) { panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout() borderLayout()
@@ -223,6 +282,11 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel,"monitor window") cardsPanel.getLayout().show(cardsPanel,"monitor window")
} }
def showTrustWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"trust window")
}
def shareFiles = { def shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share") chooser.setDialogTitle("Select file or directory to share")

View File

@@ -0,0 +1,54 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def retryField
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
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))
button(text : "Save", constraints : gbc(gridx : 1, gridy: 1), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 1), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
d.getContentPane().add(p)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

View File

@@ -31,6 +31,7 @@ class SearchTabView {
tableModel(list: model.results) { tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name}) closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size}) closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", type : Integer, read : { row -> model.hashCount[row.infohash]})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()}) closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row -> closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination) model.core.trustService.getLevel(row.sender.destination)
@@ -55,6 +56,7 @@ class SearchTabView {
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs") parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane) parent.addTab(searchTerms, pane)
int index = parent.indexOfTab(searchTerms) int index = parent.indexOfTab(searchTerms)
parent.setSelectedIndex(index)
def tabPanel def tabPanel
builder.with { builder.with {

View File

@@ -0,0 +1,25 @@
package com.muwire.gui
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
class OptionsIntegrationTest {
static {
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
@Test
void smokeTest() {
fail('Not implemented yet!')
}
}

View File

@@ -0,0 +1,21 @@
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!')
}
}