Compare commits
43 Commits
muwire-0.0
...
muwire-0.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc3b6f500f | ||
![]() |
8f8710801c | ||
![]() |
43f3cf9b7a | ||
![]() |
6fe4155678 | ||
![]() |
32f944a089 | ||
![]() |
b19b5ef315 | ||
![]() |
5138935c20 | ||
![]() |
ba596af778 | ||
![]() |
0f4533c867 | ||
![]() |
727834390c | ||
![]() |
c51e3874da | ||
![]() |
d18a618575 | ||
![]() |
15508f417d | ||
![]() |
44dad55178 | ||
![]() |
5c17e77190 | ||
![]() |
de856cd085 | ||
![]() |
d2533cc4d6 | ||
![]() |
f41cc39659 | ||
![]() |
656b62fc2e | ||
![]() |
13b3f0f63b | ||
![]() |
98ea8154a5 | ||
![]() |
82377aa9df | ||
![]() |
bd2368e23a | ||
![]() |
70078c309b | ||
![]() |
15a0eda713 | ||
![]() |
9645716e18 | ||
![]() |
03d6af39ed | ||
![]() |
9435cb003b | ||
![]() |
63399803d5 | ||
![]() |
4d6541030f | ||
![]() |
16c51e7cd6 | ||
![]() |
9d75550b6f | ||
![]() |
1996681677 | ||
![]() |
9dac1891b2 | ||
![]() |
1255ac936b | ||
![]() |
2db3276b07 | ||
![]() |
7e3b0795af | ||
![]() |
4bb27b84de | ||
![]() |
d5b8c0c694 | ||
![]() |
1e314b3cca | ||
![]() |
6f02e3e9c0 | ||
![]() |
9f5f21376a | ||
![]() |
d2231b8e38 |
@@ -3,7 +3,7 @@ subprojects {
|
||||
|
||||
dependencies {
|
||||
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 {
|
||||
|
@@ -10,4 +10,6 @@ class Constants {
|
||||
public static final int MAX_HEADERS = 16
|
||||
|
||||
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\.,_-]"
|
||||
}
|
||||
|
@@ -54,7 +54,8 @@ public class Core {
|
||||
|
||||
final EventBus eventBus
|
||||
final Persona me
|
||||
|
||||
final File home
|
||||
|
||||
private final TrustService trustService
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
@@ -64,7 +65,8 @@ public class Core {
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
|
||||
public Core(MuWireSettings props, File home) {
|
||||
public Core(MuWireSettings props, File home) {
|
||||
this.home = home
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
@@ -118,8 +120,8 @@ public class Core {
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trust.good")
|
||||
File badTrust = new File(home, "trust.bad")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||
eventBus.register(TrustEvent.class, trustService)
|
||||
|
||||
@@ -164,7 +166,7 @@ public class Core {
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing download manager")
|
||||
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector)
|
||||
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
@@ -196,6 +198,10 @@ public class Core {
|
||||
connectionEstablisher.start()
|
||||
hostCache.waitForLoad()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
connectionManager.shutdown()
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
|
@@ -6,6 +6,7 @@ class MuWireSettings {
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
int downloadRetryInterval
|
||||
String nickname
|
||||
File downloadLocation
|
||||
String sharedFiles
|
||||
@@ -23,6 +24,7 @@ class MuWireSettings {
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
sharedFiles = props.getProperty("sharedFiles")
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
@@ -32,6 +34,7 @@ class MuWireSettings {
|
||||
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
||||
props.setProperty("nickname", nickname)
|
||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", "15")
|
||||
if (sharedFiles != null)
|
||||
props.setProperty("sharedFiles", sharedFiles)
|
||||
props.store(out, "")
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPublicKey
|
||||
@@ -14,6 +15,7 @@ public class Persona {
|
||||
private final Destination destination
|
||||
private final byte[] sig
|
||||
private volatile String humanReadableName
|
||||
private volatile String base64
|
||||
private volatile byte[] payload
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
|
||||
@@ -59,6 +61,15 @@ public class Persona {
|
||||
humanReadableName
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
write(baos)
|
||||
base64 = Base64.encode(baos.toByteArray())
|
||||
}
|
||||
base64
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode() ^ destination.hashCode()
|
||||
|
@@ -82,7 +82,6 @@ abstract class Connection implements Closeable {
|
||||
read()
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
close()
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||
} finally {
|
||||
@@ -120,6 +119,7 @@ abstract class Connection implements Closeable {
|
||||
query.type = "Search"
|
||||
query.version = 1
|
||||
query.uuid = e.searchEvent.getUuid()
|
||||
query.firstHop = e.firstHop
|
||||
// TODO: first hop figure out
|
||||
query.keywords = e.searchEvent.getSearchTerms()
|
||||
query.replyTo = e.getReceivedOn().toBase64()
|
||||
@@ -164,7 +164,7 @@ abstract class Connection implements Closeable {
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : Boolean.parseBoolean(search.firstHop) )
|
||||
firstHop : search.firstHop )
|
||||
eventBus.publish(event)
|
||||
|
||||
}
|
||||
|
@@ -204,7 +204,7 @@ class ConnectionAcceptor {
|
||||
byte [] payload = new byte[jsonSize]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
eventBus.publish(ResultsParser.parse(sender, json))
|
||||
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
|
||||
}
|
||||
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
||||
log.log(Level.WARNING, "failed to process POST", bad)
|
||||
|
@@ -40,7 +40,7 @@ abstract class ConnectionManager {
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.DISTRUSTED)
|
||||
drop(e.destination)
|
||||
drop(e.persona.destination)
|
||||
}
|
||||
|
||||
abstract void drop(Destination d)
|
||||
@@ -58,6 +58,8 @@ abstract class ConnectionManager {
|
||||
abstract void onConnectionEvent(ConnectionEvent e)
|
||||
|
||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||
|
||||
abstract void shutdown()
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
|
@@ -71,4 +71,8 @@ class LeafConnectionManager extends ConnectionManager {
|
||||
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
|
||||
|
||||
UltrapeerConnectionManager() {}
|
||||
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
@@ -100,6 +100,14 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
if (removed == null)
|
||||
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
peerConnections.each {k,v -> v.close() }
|
||||
leafConnections.each {k,v -> v.close() }
|
||||
peerConnections.clear()
|
||||
leafConnections.clear()
|
||||
}
|
||||
|
||||
void forwardQueryToLeafs(QueryEvent e) {
|
||||
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
class BadHashException extends Exception {
|
||||
|
||||
public BadHashException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BadHashException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public BadHashException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public BadHashException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BadHashException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,11 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
@@ -11,10 +15,20 @@ public class DownloadManager {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes
|
||||
private final String meB64
|
||||
|
||||
public DownloadManager(EventBus eventBus, I2PConnector connector) {
|
||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
|
||||
this.eventBus = eventBus
|
||||
this.connector = connector
|
||||
this.incompletes = incompletes
|
||||
|
||||
def baos = new ByteArrayOutputStream()
|
||||
me.write(baos)
|
||||
this.meB64 = Base64.encode(baos.toByteArray())
|
||||
|
||||
incompletes.mkdir()
|
||||
|
||||
this.executor = Executors.newCachedThreadPool({ r ->
|
||||
Thread rv = new Thread(r)
|
||||
rv.setName("download-worker")
|
||||
@@ -25,9 +39,14 @@ public class DownloadManager {
|
||||
|
||||
|
||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||
def downloader = new Downloader(e.target, e.result.size,
|
||||
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination)
|
||||
def downloader = new Downloader(this, meB64, e.target, e.result.size,
|
||||
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
|
||||
incompletes)
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
void resume(Downloader downloader) {
|
||||
executor.execute({downloader.download() as Runnable})
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import java.security.NoSuchAlgorithmException
|
||||
@Log
|
||||
class DownloadSession {
|
||||
|
||||
private final String meB64
|
||||
private final Pieces pieces
|
||||
private final InfoHash infoHash
|
||||
private final Endpoint endpoint
|
||||
@@ -30,8 +31,9 @@ class DownloadSession {
|
||||
|
||||
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) {
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
this.endpoint = endpoint
|
||||
this.infoHash = infoHash
|
||||
@@ -60,7 +62,8 @@ class DownloadSession {
|
||||
FileChannel channel
|
||||
try {
|
||||
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()
|
||||
String code = readTillRN(is)
|
||||
if (code.startsWith("404 ")) {
|
||||
@@ -127,11 +130,8 @@ class DownloadSession {
|
||||
byte [] hash = digest.digest()
|
||||
byte [] expected = new byte[32]
|
||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||
if (hash != expected) {
|
||||
log.warning("hash mismatch")
|
||||
endpoint.close()
|
||||
return
|
||||
}
|
||||
if (hash != expected)
|
||||
throw new BadHashException()
|
||||
|
||||
pieces.markDownloaded(piece)
|
||||
} finally {
|
||||
|
@@ -2,14 +2,21 @@ package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
public class Downloader {
|
||||
public enum DownloadState { CONNECTING, DOWNLOADING, FINISHED }
|
||||
|
||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||
|
||||
private final DownloadManager downloadManager
|
||||
private final String meB64
|
||||
private final File file
|
||||
private final Pieces pieces
|
||||
private final long length
|
||||
@@ -18,17 +25,25 @@ public class Downloader {
|
||||
private final I2PConnector connector
|
||||
private final Destination destination
|
||||
private final int nPieces
|
||||
private final File piecesFile
|
||||
|
||||
private Endpoint endpoint
|
||||
private volatile DownloadSession currentSession
|
||||
private volatile DownloadState currentState
|
||||
private volatile boolean cancelled
|
||||
private volatile Thread downloadThread
|
||||
|
||||
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) {
|
||||
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
|
||||
int pieceSizePow2, I2PConnector connector, Destination destination,
|
||||
File incompletes) {
|
||||
this.meB64 = meB64
|
||||
this.downloadManager = downloadManager
|
||||
this.file = file
|
||||
this.infoHash = infoHash
|
||||
this.length = length
|
||||
this.connector = connector
|
||||
this.destination = destination
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
|
||||
int nPieces
|
||||
@@ -43,14 +58,45 @@ public class Downloader {
|
||||
}
|
||||
|
||||
void download() {
|
||||
Endpoint endpoint = connector.connect(destination)
|
||||
currentState = DownloadState.DOWNLOADING
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
|
||||
currentSession.request()
|
||||
readPieces()
|
||||
downloadThread = Thread.currentThread()
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
endpoint = connector.connect(destination)
|
||||
currentState = DownloadState.DOWNLOADING
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
|
||||
currentSession.request()
|
||||
writePieces()
|
||||
}
|
||||
currentState = DownloadState.FINISHED
|
||||
piecesFile.delete()
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||
if (cancelled)
|
||||
currentState = DownloadState.CANCELLED
|
||||
else if (currentState != DownloadState.FINISHED)
|
||||
currentState = DownloadState.FAILED
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
}
|
||||
|
||||
void readPieces() {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.withReader {
|
||||
int piece = Integer.parseInt(it.readLine())
|
||||
pieces.markDownloaded(piece)
|
||||
}
|
||||
}
|
||||
|
||||
void writePieces() {
|
||||
piecesFile.withPrintWriter { writer ->
|
||||
pieces.getDownloaded().each { piece ->
|
||||
writer.println(piece)
|
||||
}
|
||||
}
|
||||
currentState = DownloadState.FINISHED
|
||||
endpoint.close()
|
||||
}
|
||||
|
||||
public long donePieces() {
|
||||
@@ -66,4 +112,13 @@ public class Downloader {
|
||||
public DownloadState getCurrentState() {
|
||||
currentState
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancelled = true
|
||||
downloadThread?.interrupt()
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
downloadManager.resume(this)
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,14 @@ class Pieces {
|
||||
}
|
||||
}
|
||||
|
||||
def getDownloaded() {
|
||||
def rv = []
|
||||
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
|
||||
rv << i
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
synchronized void markDownloaded(int piece) {
|
||||
bitSet.set(piece)
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import com.muwire.core.util.DataUtil
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class ResultsParser {
|
||||
public static UIResultEvent parse(Persona p, def json) throws InvalidSearchResultException {
|
||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||
if (json.type != "Result")
|
||||
throw new InvalidSearchResultException("not a result json")
|
||||
if (json.version != 1)
|
||||
@@ -46,7 +46,8 @@ class ResultsParser {
|
||||
name : name,
|
||||
size : size,
|
||||
infohash : parsedIH,
|
||||
pieceSize : pieceSize)
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
}
|
||||
|
@@ -55,7 +55,8 @@ class ResultsSender {
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : FileHasher.getPieceSize(length)
|
||||
pieceSize : FileHasher.getPieceSize(length),
|
||||
uuid : uuid
|
||||
)
|
||||
eventBus.publish(uiResultEvent)
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
|
||||
class SearchIndex {
|
||||
|
||||
@@ -31,7 +32,7 @@ class SearchIndex {
|
||||
}
|
||||
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll("[\\.,_-]", " ")
|
||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||
source.split(" ")
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import com.muwire.core.Persona
|
||||
|
||||
class UIResultEvent extends Event {
|
||||
Persona sender
|
||||
UUID uuid
|
||||
String name
|
||||
long size
|
||||
InfoHash infohash
|
||||
|
@@ -1,11 +1,10 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
import net.i2p.data.Destination
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class TrustEvent extends Event {
|
||||
|
||||
Destination destination
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@@ -10,8 +14,8 @@ class TrustService extends Service {
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
|
||||
final Set<Destination> good = new ConcurrentHashSet<>()
|
||||
final Set<Destination> bad = new ConcurrentHashSet<>()
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
|
||||
final Timer timer
|
||||
|
||||
@@ -35,12 +39,16 @@ class TrustService extends Service {
|
||||
void load() {
|
||||
if (persistGood.exists()) {
|
||||
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()) {
|
||||
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)
|
||||
@@ -50,22 +58,22 @@ class TrustService extends Service {
|
||||
private void persist() {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {
|
||||
writer.println it.toBase64()
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each {
|
||||
writer.println it.toBase64()
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TrustLevel getLevel(Destination dest) {
|
||||
if (good.contains(dest))
|
||||
if (good.containsKey(dest))
|
||||
return TrustLevel.TRUSTED
|
||||
else if (bad.contains(dest))
|
||||
else if (bad.containsKey(dest))
|
||||
return TrustLevel.DISTRUSTED
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
@@ -73,16 +81,16 @@ class TrustService extends Service {
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.destination)
|
||||
good.add(e.destination)
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.destination)
|
||||
bad.add(e.destination)
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.destination)
|
||||
bad.remove(e.destination)
|
||||
good.remove(e.persona.destination)
|
||||
bad.remove(e.persona.destination)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
@@ -16,6 +17,7 @@ class Request {
|
||||
|
||||
InfoHash infoHash
|
||||
Range range
|
||||
Persona downloader
|
||||
Map<String, String> headers
|
||||
|
||||
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
|
||||
@@ -85,7 +87,13 @@ class Request {
|
||||
if (start < 0 || end < start)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -55,7 +55,7 @@ class DownloadSessionTest {
|
||||
toUploader = new PipedOutputStream(fromDownloader)
|
||||
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.setDaemon(true)
|
||||
downloadThread.start()
|
||||
@@ -74,6 +74,7 @@ class DownloadSessionTest {
|
||||
initSession(20)
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
assert "Range: 0-19" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@@ -95,6 +96,7 @@ class DownloadSessionTest {
|
||||
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@@ -122,6 +124,7 @@ class DownloadSessionTest {
|
||||
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
||||
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
||||
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.0.2
|
||||
version = 0.0.5
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@@ -58,6 +58,7 @@ dependencies {
|
||||
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
|
||||
|
||||
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.spockframework:spock-core:${spockVersion}"
|
||||
|
@@ -16,4 +16,9 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.MainFrameView'
|
||||
controller = 'com.muwire.gui.MainFrameController'
|
||||
}
|
||||
'SearchTab' {
|
||||
model = 'com.muwire.gui.SearchTabModel'
|
||||
view = 'com.muwire.gui.SearchTabView'
|
||||
controller = 'com.muwire.gui.SearchTabController'
|
||||
}
|
||||
}
|
@@ -3,11 +3,14 @@ package com.muwire.gui
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
@@ -29,21 +32,44 @@ class MainFrameController {
|
||||
|
||||
@ControllerAction
|
||||
void search() {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||
|
||||
def search = builder.getVariable("search-field").text
|
||||
def searchEvent = new SearchEvent(searchTerms : [search], uuid : UUID.randomUUID())
|
||||
def uuid = UUID.randomUUID()
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
// 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,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination))
|
||||
}
|
||||
|
||||
private def selectedResult() {
|
||||
def resultsTable = builder.getVariable("results-table")
|
||||
int row = resultsTable.getSelectedRow()
|
||||
model.results[row]
|
||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
def table = selected.getClientProperty("results-table")
|
||||
int row = table.getSelectedRow()
|
||||
if (row == -1)
|
||||
return
|
||||
group.model.results[row]
|
||||
}
|
||||
|
||||
private def selectedDownload() {
|
||||
def selected = builder.getVariable("downloads-table").getSelectedRow()
|
||||
model.downloads[selected].downloader
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
|
||||
}
|
||||
@@ -51,13 +77,56 @@ class MainFrameController {
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def result = selectedResult()
|
||||
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED))
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def result = selectedResult()
|
||||
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
def downloader = selectedDownload()
|
||||
downloader.cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void resume() {
|
||||
def downloader = selectedDownload()
|
||||
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) {
|
||||
|
@@ -0,0 +1,11 @@
|
||||
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 SearchTabController {
|
||||
}
|
@@ -21,7 +21,11 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
@Override
|
||||
void execute() {
|
||||
lookAndFeel((isMacOSX ? 'system' : 'nimbus'), 'gtk', ['metal', [boldFonts: false]])
|
||||
if (isMacOSX()) {
|
||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||
} else {
|
||||
lookAndFeel('system', 'gtk')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
class Ready extends AbstractLifecycleHandler {
|
||||
@@ -27,7 +28,11 @@ class Ready extends AbstractLifecycleHandler {
|
||||
@Override
|
||||
void execute() {
|
||||
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)
|
||||
if (!home.exists()) {
|
||||
log.info("creating home dir")
|
||||
@@ -63,8 +68,13 @@ class Ready extends AbstractLifecycleHandler {
|
||||
nickname = nickname.trim()
|
||||
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()
|
||||
chooser.setDialogTitle("Select a directory where downloads will be saved")
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
@@ -74,15 +84,21 @@ class Ready extends AbstractLifecycleHandler {
|
||||
System.exit(0)
|
||||
}
|
||||
props.downloadLocation = chooser.getSelectedFile()
|
||||
break
|
||||
}
|
||||
props.setNickname(nickname)
|
||||
|
||||
propsFile.withOutputStream {
|
||||
props.write(it)
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home)
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home)
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.SEVERE,"couldn't initialize core",bad)
|
||||
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
|
||||
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
||||
System.exit(0)
|
||||
}
|
||||
core.startServices()
|
||||
application.context.put("muwire-settings", props)
|
||||
application.context.put("core",core)
|
||||
|
25
gui/griffon-app/lifecycle/Shutdown.groovy
Normal file
25
gui/griffon-app/lifecycle/Shutdown.groovy
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
|
||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Core
|
||||
|
||||
import griffon.core.GriffonApplication
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class Shutdown extends AbstractLifecycleHandler {
|
||||
@Inject
|
||||
Shutdown(@Nonnull GriffonApplication application) {
|
||||
super(application)
|
||||
}
|
||||
|
||||
@Override
|
||||
void execute() {
|
||||
log.info("shutting down")
|
||||
Core core = application.context.get("core")
|
||||
core.shutdown()
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
@@ -10,9 +12,11 @@ import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
@@ -21,9 +25,11 @@ import com.muwire.core.upload.UploadFinishedEvent
|
||||
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.core.mvc.MVCGroup
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.FXObservable
|
||||
import griffon.transform.Observable
|
||||
import net.i2p.data.Destination
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
@@ -33,18 +39,47 @@ class MainFrameModel {
|
||||
@Inject @Nonnull GriffonApplication application
|
||||
@Observable boolean coreInitialized = false
|
||||
|
||||
@Observable def results = []
|
||||
@Observable def downloads = []
|
||||
@Observable def uploads = []
|
||||
@Observable def shared = []
|
||||
def results = new ConcurrentHashMap<>()
|
||||
def downloads = []
|
||||
def uploads = []
|
||||
def shared = []
|
||||
def connectionList = []
|
||||
def searches = new LinkedList()
|
||||
def trusted = []
|
||||
def distrusted = []
|
||||
|
||||
@Observable int connections
|
||||
@Observable String me
|
||||
@Observable boolean searchButtonsEnabled
|
||||
@Observable boolean cancelButtonEnabled
|
||||
@Observable boolean retryButtonEnabled
|
||||
|
||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||
|
||||
volatile Core core
|
||||
|
||||
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) {
|
||||
|
||||
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 ->
|
||||
coreInitialized = (e.getNewValue() != null)
|
||||
core = e.getNewValue()
|
||||
@@ -58,22 +93,32 @@ class MainFrameModel {
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
})
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
runInsideUIAsync {
|
||||
builder.getVariable("downloads-table").model.fireTableDataChanged()
|
||||
builder.getVariable("uploads-table").model.fireTableDataChanged()
|
||||
core.eventBus.register(QueryEvent.class, this)
|
||||
|
||||
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
timer.schedule({
|
||||
runInsideUIAsync {
|
||||
downloads.each {
|
||||
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
|
||||
it.downloader.resume()
|
||||
}
|
||||
}
|
||||
}, retryInterval, retryInterval)
|
||||
}
|
||||
}, 1000, 1000)
|
||||
|
||||
runInsideUIAsync {
|
||||
trusted.addAll(core.trustService.good.values())
|
||||
distrusted.addAll(core.trustService.bad.values())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
void onUIResultEvent(UIResultEvent e) {
|
||||
runInsideUIAsync {
|
||||
results << e
|
||||
JTable table = builder.getVariable("results-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
MVCGroup resultsGroup = results.get(e.uuid)
|
||||
resultsGroup?.model.handleResult(e)
|
||||
}
|
||||
|
||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||
@@ -83,14 +128,34 @@ class MainFrameModel {
|
||||
}
|
||||
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
return
|
||||
runInsideUIAsync {
|
||||
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)
|
||||
JTable table = builder.getVariable("connections-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
runInsideUIAsync {
|
||||
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)
|
||||
JTable table = builder.getVariable("connections-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +201,37 @@ class MainFrameModel {
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
runInsideUIAsync {
|
||||
JTable table = builder.getVariable("results-table")
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.replyTo == core.me.destination)
|
||||
return
|
||||
StringBuilder sb = new StringBuilder()
|
||||
e.searchEvent.searchTerms?.each {
|
||||
sb.append(it)
|
||||
sb.append(" ")
|
||||
}
|
||||
def search = sb.toString()
|
||||
if (search.trim().size() == 0)
|
||||
return
|
||||
runInsideUIAsync {
|
||||
searches.addFirst(search)
|
||||
while(searches.size() > 200)
|
||||
searches.removeLast()
|
||||
JTable table = builder.getVariable("searches-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
42
gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy
Normal file
42
gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.core.mvc.MVCGroup
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class SearchTabModel {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
Core core
|
||||
String uuid
|
||||
def results = []
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
mvcGroup.parentGroup.model.results.remove(uuid)
|
||||
}
|
||||
|
||||
void handleResult(UIResultEvent e) {
|
||||
runInsideUIAsync {
|
||||
results << e
|
||||
JTable table = builder.getVariable("results-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
}
|
BIN
gui/griffon-app/resources/close_tab.png
Normal file
BIN
gui/griffon-app/resources/close_tab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 B |
@@ -9,9 +9,11 @@ import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.JSplitPane
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.border.Border
|
||||
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
|
||||
import java.awt.BorderLayout
|
||||
@@ -20,6 +22,7 @@ import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@@ -48,15 +51,25 @@ class MainFrameView {
|
||||
gridLayout(rows:1, cols: 2)
|
||||
button(text: "Searches", actionPerformed : showSearchWindow)
|
||||
button(text: "Uploads", actionPerformed : showUploadsWindow)
|
||||
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
||||
button(text: "Trust", actionPerformed : showTrustWindow)
|
||||
}
|
||||
panel(constraints: BorderLayout.CENTER) {
|
||||
borderLayout()
|
||||
label("Enter search here:", constraints: BorderLayout.WEST)
|
||||
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
|
||||
|
||||
}
|
||||
panel( constraints: BorderLayout.EAST) {
|
||||
button(text: "Search", searchAction)
|
||||
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) {
|
||||
borderLayout()
|
||||
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
|
||||
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
|
||||
|
||||
}
|
||||
panel( constraints: BorderLayout.EAST) {
|
||||
button(text: "Search", searchAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
|
||||
@@ -67,22 +80,11 @@ class MainFrameView {
|
||||
continuousLayout : true, constraints : BorderLayout.CENTER) {
|
||||
panel (constraints : JSplitPane.TOP) {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
table(id : "results-table") {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", type: String, read : {row -> row.name})
|
||||
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
|
||||
closureColumn(header: "Trust", type: String, read : {row ->
|
||||
model.core.trustService.getLevel(row.sender.destination)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Download", downloadAction)
|
||||
button(text : "Trust", trustAction)
|
||||
button(text : "Distrust", distrustAction)
|
||||
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
|
||||
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
|
||||
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
panel (constraints : JSplitPane.BOTTOM) {
|
||||
@@ -105,6 +107,10 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
|
||||
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +119,7 @@ class MainFrameView {
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
button(text : "Shared files", actionPerformed : shareFiles)
|
||||
button(text : "Click here to share files", actionPerformed : shareFiles)
|
||||
}
|
||||
scrollPane ( constraints : BorderLayout.CENTER) {
|
||||
table(id : "shared-files-table") {
|
||||
@@ -126,7 +132,9 @@ class MainFrameView {
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
label("Uploads", constraints : BorderLayout.NORTH)
|
||||
panel (constraints : BorderLayout.NORTH){
|
||||
label("Uploads")
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
table(id : "uploads-table") {
|
||||
tableModel(list : model.uploads) {
|
||||
@@ -138,11 +146,76 @@ class MainFrameView {
|
||||
int percent = (int)((position * 100.0) / total)
|
||||
"$percent%"
|
||||
})
|
||||
closureColumn(header : "Downloader", type : String, read : { row ->
|
||||
row.request.downloader?.getHumanReadableName()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints: "monitor window") {
|
||||
gridLayout(rows : 1, cols : 2)
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH){
|
||||
label("Connections")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "connections-table") {
|
||||
tableModel(list : model.connectionList) {
|
||||
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH){
|
||||
label("Incoming searches")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "searches-table") {
|
||||
tableModel(list : model.searches) {
|
||||
closureColumn(header : "Keywords", type : String, read : { it })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
borderLayout()
|
||||
@@ -156,6 +229,30 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selectionModel = downloadsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = downloadsTable.getSelectedRow()
|
||||
def downloader = model.downloads[selectedRow].downloader
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
case Downloader.DownloadState.DOWNLOADING :
|
||||
model.cancelButtonEnabled = true
|
||||
model.retryButtonEnabled = false
|
||||
break
|
||||
case Downloader.DownloadState.FAILED:
|
||||
model.cancelButtonEnabled = false
|
||||
model.retryButtonEnabled = true
|
||||
break
|
||||
default:
|
||||
model.cancelButtonEnabled = false
|
||||
model.retryButtonEnabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
def showSearchWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
@@ -167,6 +264,16 @@ class MainFrameView {
|
||||
cardsPanel.getLayout().show(cardsPanel, "uploads window")
|
||||
}
|
||||
|
||||
def showMonitorWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel,"monitor window")
|
||||
}
|
||||
|
||||
def showTrustWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel,"trust window")
|
||||
}
|
||||
|
||||
def shareFiles = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setDialogTitle("Select file or directory to share")
|
||||
|
81
gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy
Normal file
81
gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.core.mvc.MVCGroup
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class SearchTabView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def searchTerms
|
||||
|
||||
void initUI() {
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def pane = scrollPane {
|
||||
resultsTable = table(id : "results-table") {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", type: String, read : {row -> row.name})
|
||||
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
|
||||
closureColumn(header: "Trust", type: String, read : {row ->
|
||||
model.core.trustService.getLevel(row.sender.destination)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
this.pane = pane
|
||||
this.pane.putClientProperty("mvc-group", mvcGroup)
|
||||
this.pane.putClientProperty("results-table",resultsTable)
|
||||
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener( {
|
||||
mvcGroup.parentGroup.model.searchButtonsEnabled = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
searchTerms = args["search-terms"]
|
||||
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
|
||||
parent.addTab(searchTerms, pane)
|
||||
int index = parent.indexOfTab(searchTerms)
|
||||
parent.setSelectedIndex(index)
|
||||
|
||||
def tabPanel
|
||||
builder.with {
|
||||
tabPanel = panel {
|
||||
borderLayout()
|
||||
panel {
|
||||
label(text : searchTerms, constraints : BorderLayout.CENTER)
|
||||
}
|
||||
button(icon : imageIcon("/close_tab.png"), preferredSize : [20,20], constraints : BorderLayout.EAST, // TODO: in osx is probably WEST
|
||||
actionPerformed : closeTab )
|
||||
}
|
||||
}
|
||||
|
||||
parent.setTabComponentAt(index, tabPanel)
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
int index = parent.indexOfTab(searchTerms)
|
||||
parent.removeTabAt(index)
|
||||
mvcGroup.parentGroup.model.searchButtonsEnabled = false
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -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 SearchTabIntegrationTest {
|
||||
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!')
|
||||
}
|
||||
}
|
@@ -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(SearchTabController)
|
||||
class SearchTabControllerTest {
|
||||
private SearchTabController controller
|
||||
|
||||
@Rule
|
||||
public final GriffonUnitRule griffon = new GriffonUnitRule()
|
||||
|
||||
@Test
|
||||
void smokeTest() {
|
||||
fail('Not yet implemented!')
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user