Compare commits
93 Commits
muwire-0.0
...
muwire-0.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a2531b264 | ||
![]() |
9a8dadff57 | ||
![]() |
4a274010f9 | ||
![]() |
1eb930435b | ||
![]() |
9df28552ad | ||
![]() |
ac0204dffc | ||
![]() |
e5c402a400 | ||
![]() |
7704c73b68 | ||
![]() |
a9aa8dd840 | ||
![]() |
de682a802a | ||
![]() |
5435518212 | ||
![]() |
bd01f983c9 | ||
![]() |
8b63864b90 | ||
![]() |
ed3943c1af | ||
![]() |
e195141a27 | ||
![]() |
bb02fdbee9 | ||
![]() |
6e3a2c0d08 | ||
![]() |
bd5fecc19d | ||
![]() |
d5db49fa79 | ||
![]() |
f2ea8619bb | ||
![]() |
b129e79196 | ||
![]() |
404d5b60bc | ||
![]() |
de2753ac50 | ||
![]() |
2d53999c8e | ||
![]() |
5aecf72d6f | ||
![]() |
a574a67ec6 | ||
![]() |
6b5ad969b7 | ||
![]() |
617209c4e4 | ||
![]() |
16b475bd9a | ||
![]() |
3cea1870cd | ||
![]() |
e7240dcb6f | ||
![]() |
c91440cbfc | ||
![]() |
294605f5c7 | ||
![]() |
986caf3a75 | ||
![]() |
8524d5309f | ||
![]() |
48b3ac2b4a | ||
![]() |
18f21dc247 | ||
![]() |
e69a5eac18 | ||
![]() |
6e0f1778b7 | ||
![]() |
abbb741d73 | ||
![]() |
07dfc0a1d1 | ||
![]() |
00c12cfd49 | ||
![]() |
1ee389ff91 | ||
![]() |
3642736cfe | ||
![]() |
b6f7f51476 | ||
![]() |
4c21f2d5ae | ||
![]() |
9e0d52d548 | ||
![]() |
fad01603de | ||
![]() |
da007795fb | ||
![]() |
881d755dd3 | ||
![]() |
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 {
|
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 {
|
||||||
|
8
cli/build.gradle
Normal file
8
cli/build.gradle
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apply plugin : 'application'
|
||||||
|
|
||||||
|
mainClassName = 'com.muwire.cli.Cli'
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(":core")
|
||||||
|
}
|
68
cli/src/main/groovy/com/muwire/cli/Cli.groovy
Normal file
68
cli/src/main/groovy/com/muwire/cli/Cli.groovy
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.files.FileHashedEvent
|
||||||
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
|
||||||
|
class Cli {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||||
|
home = new File(home)
|
||||||
|
if (!home.exists())
|
||||||
|
home.mkdirs()
|
||||||
|
|
||||||
|
def propsFile = new File(home,"MuWire.properties")
|
||||||
|
if (!propsFile.exists()) {
|
||||||
|
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def props = new Properties()
|
||||||
|
propsFile.withInputStream { props.load(it) }
|
||||||
|
props = new MuWireSettings(props)
|
||||||
|
|
||||||
|
Core core
|
||||||
|
try {
|
||||||
|
core = new Core(props, home, "0.0.9")
|
||||||
|
} catch (Exception bad) {
|
||||||
|
bad.printStackTrace(System.out)
|
||||||
|
println "Failed to initialize core, exiting"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.startServices()
|
||||||
|
|
||||||
|
// now we begin
|
||||||
|
println "MuWire is ready"
|
||||||
|
|
||||||
|
def filesList
|
||||||
|
if (args.length == 0) {
|
||||||
|
println "Enter a file containing list of files to share"
|
||||||
|
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||||
|
filesList = reader.readLine()
|
||||||
|
} else
|
||||||
|
filesList = args[0]
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
println "loading shared files from $filesList"
|
||||||
|
|
||||||
|
core.eventBus.register(FileHashedEvent.class, new Object() {
|
||||||
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
|
if (e.error != null)
|
||||||
|
println "ERROR $e.error"
|
||||||
|
else
|
||||||
|
println "Shared file : $e.sharedFile.file"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filesList = new File(filesList)
|
||||||
|
filesList.withReader {
|
||||||
|
def toShare = it.readLine()
|
||||||
|
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
|
}
|
||||||
|
}
|
@@ -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 = "[\\.,_-]"
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,7 @@ import com.muwire.core.search.SearchEvent
|
|||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
import com.muwire.core.update.UpdateClient
|
||||||
import com.muwire.core.upload.UploadManager
|
import com.muwire.core.upload.UploadManager
|
||||||
import com.muwire.core.util.MuWireLogManager
|
import com.muwire.core.util.MuWireLogManager
|
||||||
|
|
||||||
@@ -54,17 +55,21 @@ public class Core {
|
|||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Persona me
|
final Persona me
|
||||||
|
final File home
|
||||||
|
final Properties i2pOptions
|
||||||
|
|
||||||
private final TrustService trustService
|
private final TrustService trustService
|
||||||
private final PersisterService persisterService
|
private final PersisterService persisterService
|
||||||
private final HostCache hostCache
|
private final HostCache hostCache
|
||||||
private final ConnectionManager connectionManager
|
private final ConnectionManager connectionManager
|
||||||
private final CacheClient cacheClient
|
private final CacheClient cacheClient
|
||||||
|
private final UpdateClient updateClient
|
||||||
private final ConnectionAcceptor connectionAcceptor
|
private final ConnectionAcceptor connectionAcceptor
|
||||||
private final ConnectionEstablisher connectionEstablisher
|
private final ConnectionEstablisher connectionEstablisher
|
||||||
private final HasherService hasherService
|
private final HasherService hasherService
|
||||||
|
|
||||||
public Core(MuWireSettings props, File home) {
|
public Core(MuWireSettings props, File home, String myVersion) {
|
||||||
|
this.home = home
|
||||||
log.info "Initializing I2P context"
|
log.info "Initializing I2P context"
|
||||||
I2PAppContext.getGlobalContext().logManager()
|
I2PAppContext.getGlobalContext().logManager()
|
||||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||||
@@ -79,12 +84,23 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def sysProps = System.getProperties().clone()
|
i2pOptions = new Properties()
|
||||||
sysProps["inbound.nickname"] = "MuWire"
|
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||||
|
if (i2pOptionsFile.exists()) {
|
||||||
|
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||||
|
} else {
|
||||||
|
i2pOptions["inbound.nickname"] = "MuWire"
|
||||||
|
i2pOptions["inbound.length"] = "3"
|
||||||
|
i2pOptions["inbound.quantity"] = "2"
|
||||||
|
i2pOptions["outbound.length"] = "3"
|
||||||
|
i2pOptions["outbound.quantity"] = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
I2PSession i2pSession
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, sysProps)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions)
|
||||||
}
|
}
|
||||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||||
@@ -118,8 +134,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)
|
||||||
|
|
||||||
@@ -152,6 +168,9 @@ public class Core {
|
|||||||
log.info("initializing cache client")
|
log.info("initializing cache client")
|
||||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||||
|
|
||||||
|
log.info("initializing update client")
|
||||||
|
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
|
||||||
|
|
||||||
log.info("initializing connector")
|
log.info("initializing connector")
|
||||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||||
|
|
||||||
@@ -164,7 +183,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)
|
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")
|
||||||
@@ -180,7 +199,7 @@ public class Core {
|
|||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
|
|
||||||
log.info("initializing hasher service")
|
log.info("initializing hasher service")
|
||||||
hasherService = new HasherService(new FileHasher(), eventBus)
|
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||||
eventBus.register(FileSharedEvent.class, hasherService)
|
eventBus.register(FileSharedEvent.class, hasherService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +214,11 @@ public class Core {
|
|||||||
connectionAcceptor.start()
|
connectionAcceptor.start()
|
||||||
connectionEstablisher.start()
|
connectionEstablisher.start()
|
||||||
hostCache.waitForLoad()
|
hostCache.waitForLoad()
|
||||||
|
updateClient.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
connectionManager.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
static main(args) {
|
static main(args) {
|
||||||
@@ -221,7 +245,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home)
|
Core core = new Core(props, home, "0.0.9")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -6,6 +6,8 @@ class MuWireSettings {
|
|||||||
|
|
||||||
final boolean isLeaf
|
final boolean isLeaf
|
||||||
boolean allowUntrusted
|
boolean allowUntrusted
|
||||||
|
int downloadRetryInterval
|
||||||
|
int updateCheckInterval
|
||||||
String nickname
|
String nickname
|
||||||
File downloadLocation
|
File downloadLocation
|
||||||
String sharedFiles
|
String sharedFiles
|
||||||
@@ -23,6 +25,8 @@ 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"))
|
||||||
|
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -32,6 +36,8 @@ 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))
|
||||||
|
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||||
if (sharedFiles != null)
|
if (sharedFiles != null)
|
||||||
props.setProperty("sharedFiles", sharedFiles)
|
props.setProperty("sharedFiles", sharedFiles)
|
||||||
props.store(out, "")
|
props.store(out, "")
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
@@ -82,7 +84,6 @@ abstract class Connection implements Closeable {
|
|||||||
read()
|
read()
|
||||||
}
|
}
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
close()
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -120,9 +121,11 @@ abstract class Connection implements Closeable {
|
|||||||
query.type = "Search"
|
query.type = "Search"
|
||||||
query.version = 1
|
query.version = 1
|
||||||
query.uuid = e.searchEvent.getUuid()
|
query.uuid = e.searchEvent.getUuid()
|
||||||
// TODO: first hop figure out
|
query.firstHop = e.firstHop
|
||||||
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,13 +161,24 @@ 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 : Boolean.parseBoolean(search.firstHop) )
|
firstHop : search.firstHop )
|
||||||
eventBus.publish(event)
|
eventBus.publish(event)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -204,7 +204,7 @@ class ConnectionAcceptor {
|
|||||||
byte [] payload = new byte[jsonSize]
|
byte [] payload = new byte[jsonSize]
|
||||||
dis.readFully(payload)
|
dis.readFully(payload)
|
||||||
def json = slurper.parse(payload)
|
def json = slurper.parse(payload)
|
||||||
eventBus.publish(ResultsParser.parse(sender, json))
|
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
|
||||||
}
|
}
|
||||||
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
||||||
log.log(Level.WARNING, "failed to process POST", bad)
|
log.log(Level.WARNING, "failed to process POST", bad)
|
||||||
|
@@ -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)
|
||||||
@@ -59,6 +59,8 @@ abstract class ConnectionManager {
|
|||||||
|
|
||||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||||
|
|
||||||
|
abstract void shutdown()
|
||||||
|
|
||||||
protected void sendPings() {
|
protected void sendPings() {
|
||||||
final long now = System.currentTimeMillis()
|
final long now = System.currentTimeMillis()
|
||||||
getConnections().each {
|
getConnections().each {
|
||||||
|
@@ -71,4 +71,8 @@ class LeafConnectionManager extends ConnectionManager {
|
|||||||
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void shutdown() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -101,6 +101,14 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
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) {
|
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,12 @@
|
|||||||
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 net.i2p.data.Destination
|
||||||
|
|
||||||
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
|
||||||
@@ -11,10 +16,17 @@ public class DownloadManager {
|
|||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Executor executor
|
private final Executor executor
|
||||||
|
private final File incompletes
|
||||||
|
private final Persona me
|
||||||
|
|
||||||
public DownloadManager(EventBus eventBus, I2PConnector connector) {
|
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.me = me
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -25,9 +37,24 @@ public class DownloadManager {
|
|||||||
|
|
||||||
|
|
||||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
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 size = e.result[0].size
|
||||||
|
def infohash = e.result[0].infohash
|
||||||
|
def pieceSize = e.result[0].pieceSize
|
||||||
|
|
||||||
|
Set<Destination> destinations = new HashSet<>()
|
||||||
|
e.result.each {
|
||||||
|
destinations.add(it.sender.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
def downloader = new Downloader(this, me, e.target, size,
|
||||||
|
infohash, pieceSize, connector, destinations,
|
||||||
|
incompletes)
|
||||||
executor.execute({downloader.download()} as Runnable)
|
executor.execute({downloader.download()} as Runnable)
|
||||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resume(Downloader downloader) {
|
||||||
|
executor.execute({downloader.download() as Runnable})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,10 @@ import java.security.NoSuchAlgorithmException
|
|||||||
@Log
|
@Log
|
||||||
class DownloadSession {
|
class DownloadSession {
|
||||||
|
|
||||||
private final Pieces pieces
|
private static int SAMPLES = 10
|
||||||
|
|
||||||
|
private final String meB64
|
||||||
|
private final Pieces downloaded, claimed
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final Endpoint endpoint
|
private final Endpoint endpoint
|
||||||
private final File file
|
private final File file
|
||||||
@@ -28,11 +31,16 @@ class DownloadSession {
|
|||||||
private final long fileLength
|
private final long fileLength
|
||||||
private final MessageDigest digest
|
private final MessageDigest digest
|
||||||
|
|
||||||
|
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||||
|
private final LinkedList<Integer> reads = new LinkedList<>()
|
||||||
|
|
||||||
private ByteBuffer mapped
|
private ByteBuffer mapped
|
||||||
|
|
||||||
DownloadSession(Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
|
||||||
int pieceSize, long fileLength) {
|
int pieceSize, long fileLength) {
|
||||||
this.pieces = pieces
|
this.meB64 = meB64
|
||||||
|
this.downloaded = downloaded
|
||||||
|
this.claimed = claimed
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.file = file
|
this.file = file
|
||||||
@@ -46,11 +54,31 @@ class DownloadSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void request() throws IOException {
|
/**
|
||||||
|
* @return if the request will proceed. The only time it may not
|
||||||
|
* is if all the pieces have been claimed by other sessions.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public boolean request() throws IOException {
|
||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
InputStream is = endpoint.getInputStream()
|
InputStream is = endpoint.getInputStream()
|
||||||
|
|
||||||
int piece = pieces.getRandomPiece()
|
int piece
|
||||||
|
while(true) {
|
||||||
|
piece = downloaded.getRandomPiece()
|
||||||
|
if (claimed.isMarked(piece)) {
|
||||||
|
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
|
||||||
|
log.info("all pieces claimed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
claimed.markDownloaded(piece)
|
||||||
|
|
||||||
|
log.info("will download piece $piece")
|
||||||
|
|
||||||
long start = piece * pieceSize
|
long start = piece * pieceSize
|
||||||
long end = Math.min(fileLength, start + pieceSize) - 1
|
long end = Math.min(fileLength, start + pieceSize) - 1
|
||||||
long length = end - start + 1
|
long length = end - start + 1
|
||||||
@@ -60,7 +88,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 +148,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,16 +163,15 @@ class DownloadSession {
|
|||||||
byte [] hash = digest.digest()
|
byte [] hash = digest.digest()
|
||||||
byte [] expected = new byte[32]
|
byte [] expected = new byte[32]
|
||||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||||
if (hash != expected) {
|
if (hash != expected)
|
||||||
log.warning("hash mismatch")
|
throw new BadHashException()
|
||||||
endpoint.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pieces.markDownloaded(piece)
|
downloaded.markDownloaded(piece)
|
||||||
} finally {
|
} finally {
|
||||||
|
claimed.clear(piece)
|
||||||
try { channel?.close() } catch (IOException ignore) {}
|
try { channel?.close() } catch (IOException ignore) {}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int positionInPiece() {
|
synchronized int positionInPiece() {
|
||||||
@@ -144,4 +179,25 @@ class DownloadSession {
|
|||||||
return 0
|
return 0
|
||||||
mapped.position()
|
mapped.position()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized int speed() {
|
||||||
|
if (timestamps.size() < SAMPLES)
|
||||||
|
return 0
|
||||||
|
int totalRead = 0
|
||||||
|
int idx = 0
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
|
||||||
|
idx++
|
||||||
|
if (idx == SAMPLES)
|
||||||
|
return 0
|
||||||
|
if (idx == SAMPLES - 1)
|
||||||
|
return reads[idx]
|
||||||
|
|
||||||
|
long interval = timestamps.last - timestamps[idx]
|
||||||
|
|
||||||
|
for (int i = idx; i < SAMPLES; i++)
|
||||||
|
totalRead += reads[idx]
|
||||||
|
(int)(totalRead * 1000.0 / interval)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +1,59 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.connection.I2PConnector
|
import com.muwire.core.connection.I2PConnector
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
@Log
|
||||||
public class Downloader {
|
public class Downloader {
|
||||||
public enum DownloadState { CONNECTING, DOWNLOADING, FINISHED }
|
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||||
|
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
|
||||||
|
|
||||||
|
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||||
|
Thread rv = new Thread(r)
|
||||||
|
rv.setName("download worker")
|
||||||
|
rv.setDaemon(true)
|
||||||
|
rv
|
||||||
|
})
|
||||||
|
|
||||||
|
private final DownloadManager downloadManager
|
||||||
|
private final Persona me
|
||||||
private final File file
|
private final File file
|
||||||
private final Pieces pieces
|
private final Pieces downloaded, claimed
|
||||||
private final long length
|
private final long length
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Destination destination
|
private final Set<Destination> destinations
|
||||||
private final int nPieces
|
private final int nPieces
|
||||||
|
private final File piecesFile
|
||||||
|
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
private Endpoint endpoint
|
|
||||||
private volatile DownloadSession currentSession
|
|
||||||
private volatile DownloadState currentState
|
|
||||||
|
|
||||||
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) {
|
private volatile boolean cancelled
|
||||||
|
|
||||||
|
public Downloader(DownloadManager downloadManager, Persona me, File file, long length, InfoHash infoHash,
|
||||||
|
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||||
|
File incompletes) {
|
||||||
|
this.me = me
|
||||||
|
this.downloadManager = downloadManager
|
||||||
this.file = file
|
this.file = file
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.length = length
|
this.length = length
|
||||||
this.connector = connector
|
this.connector = connector
|
||||||
this.destination = destination
|
this.destinations = destinations
|
||||||
|
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||||
this.pieceSize = 1 << pieceSizePow2
|
this.pieceSize = 1 << pieceSizePow2
|
||||||
|
|
||||||
int nPieces
|
int nPieces
|
||||||
@@ -38,32 +63,150 @@ public class Downloader {
|
|||||||
nPieces = length / pieceSize + 1
|
nPieces = length / pieceSize + 1
|
||||||
this.nPieces = nPieces
|
this.nPieces = nPieces
|
||||||
|
|
||||||
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
||||||
currentState = DownloadState.CONNECTING
|
claimed = new Pieces(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
Endpoint endpoint = connector.connect(destination)
|
readPieces()
|
||||||
currentState = DownloadState.DOWNLOADING
|
destinations.each {
|
||||||
while(!pieces.isComplete()) {
|
if (it != me.destination) {
|
||||||
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
|
def worker = new DownloadWorker(it)
|
||||||
currentSession.request()
|
activeWorkers.put(it, worker)
|
||||||
|
executorService.submit(worker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readPieces() {
|
||||||
|
if (!piecesFile.exists())
|
||||||
|
return
|
||||||
|
piecesFile.withReader {
|
||||||
|
int piece = Integer.parseInt(it.readLine())
|
||||||
|
downloaded.markDownloaded(piece)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void writePieces() {
|
||||||
|
piecesFile.withPrintWriter { writer ->
|
||||||
|
downloaded.getDownloaded().each { piece ->
|
||||||
|
writer.println(piece)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentState = DownloadState.FINISHED
|
|
||||||
endpoint.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long donePieces() {
|
public long donePieces() {
|
||||||
pieces.donePieces()
|
downloaded.donePieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
public int positionInPiece() {
|
|
||||||
if (currentSession == null)
|
public int speed() {
|
||||||
return 0
|
int total = 0
|
||||||
currentSession.positionInPiece()
|
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.DOWNLOADING)
|
||||||
|
total += it.speed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadState getCurrentState() {
|
public DownloadState getCurrentState() {
|
||||||
currentState
|
if (cancelled)
|
||||||
|
return DownloadState.CANCELLED
|
||||||
|
boolean allFinished = true
|
||||||
|
activeWorkers.values().each {
|
||||||
|
allFinished &= it.currentState == WorkerState.FINISHED
|
||||||
|
}
|
||||||
|
if (allFinished) {
|
||||||
|
if (downloaded.isComplete())
|
||||||
|
return DownloadState.FINISHED
|
||||||
|
return DownloadState.FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
// if at least one is downloading...
|
||||||
|
boolean oneDownloading = false
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.DOWNLOADING) {
|
||||||
|
oneDownloading = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneDownloading)
|
||||||
|
return DownloadState.DOWNLOADING
|
||||||
|
|
||||||
|
return DownloadState.CONNECTING
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
cancelled = true
|
||||||
|
activeWorkers.values().each {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int activeWorkers() {
|
||||||
|
int active = 0
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState != WorkerState.FINISHED)
|
||||||
|
active++
|
||||||
|
}
|
||||||
|
active
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
activeWorkers.each { destination, worker ->
|
||||||
|
if (worker.currentState == WorkerState.FINISHED) {
|
||||||
|
def newWorker = new DownloadWorker(destination)
|
||||||
|
activeWorkers.put(destination, newWorker)
|
||||||
|
executorService.submit(newWorker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadWorker implements Runnable {
|
||||||
|
private final Destination destination
|
||||||
|
private volatile WorkerState currentState
|
||||||
|
private volatile Thread downloadThread
|
||||||
|
private Endpoint endpoint
|
||||||
|
private volatile DownloadSession currentSession
|
||||||
|
|
||||||
|
DownloadWorker(Destination destination) {
|
||||||
|
this.destination = destination
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
downloadThread = Thread.currentThread()
|
||||||
|
currentState = WorkerState.CONNECTING
|
||||||
|
Endpoint endpoint = null
|
||||||
|
try {
|
||||||
|
endpoint = connector.connect(destination)
|
||||||
|
currentState = WorkerState.DOWNLOADING
|
||||||
|
boolean requestPerformed
|
||||||
|
while(!downloaded.isComplete()) {
|
||||||
|
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
|
||||||
|
requestPerformed = currentSession.request()
|
||||||
|
if (!requestPerformed)
|
||||||
|
break
|
||||||
|
writePieces()
|
||||||
|
}
|
||||||
|
} catch (Exception bad) {
|
||||||
|
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||||
|
} finally {
|
||||||
|
currentState = WorkerState.FINISHED
|
||||||
|
endpoint?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int speed() {
|
||||||
|
if (currentSession == null)
|
||||||
|
return 0
|
||||||
|
currentSession.speed()
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
downloadThread?.interrupt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,19 +28,36 @@ class Pieces {
|
|||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
int start = random.nextInt(nPieces)
|
int start = random.nextInt(nPieces)
|
||||||
while(bitSet.get(start) && ++start < nPieces);
|
if (bitSet.get(start))
|
||||||
|
continue
|
||||||
return start
|
return start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
synchronized void markDownloaded(int piece) {
|
||||||
bitSet.set(piece)
|
bitSet.set(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void clear(int piece) {
|
||||||
|
bitSet.clear(piece)
|
||||||
|
}
|
||||||
|
|
||||||
synchronized boolean isComplete() {
|
synchronized boolean isComplete() {
|
||||||
bitSet.cardinality() == nPieces
|
bitSet.cardinality() == nPieces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized boolean isMarked(int piece) {
|
||||||
|
bitSet.get(piece)
|
||||||
|
}
|
||||||
|
|
||||||
synchronized int donePieces() {
|
synchronized int donePieces() {
|
||||||
bitSet.cardinality()
|
bitSet.cardinality()
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,6 @@ import com.muwire.core.search.UIResultEvent
|
|||||||
|
|
||||||
class UIDownloadEvent extends Event {
|
class UIDownloadEvent extends Event {
|
||||||
|
|
||||||
UIResultEvent result
|
UIResultEvent[] result
|
||||||
File target
|
File target
|
||||||
}
|
}
|
||||||
|
@@ -52,11 +52,11 @@ class FileHasher {
|
|||||||
try {
|
try {
|
||||||
MappedByteBuffer buf
|
MappedByteBuffer buf
|
||||||
for (int i = 0; i < numPieces - 1; i++) {
|
for (int i = 0; i < numPieces - 1; i++) {
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
}
|
}
|
||||||
def lastPieceLength = length - (numPieces - 1) * size
|
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
|
@@ -10,11 +10,13 @@ class HasherService {
|
|||||||
|
|
||||||
final FileHasher hasher
|
final FileHasher hasher
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
|
final FileManager fileManager
|
||||||
Executor executor
|
Executor executor
|
||||||
|
|
||||||
HasherService(FileHasher hasher, EventBus eventBus) {
|
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||||
this.hasher = hasher
|
this.hasher = hasher
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
this.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
@@ -22,6 +24,8 @@ class HasherService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileSharedEvent(FileSharedEvent evt) {
|
void onFileSharedEvent(FileSharedEvent evt) {
|
||||||
|
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||||
|
return
|
||||||
executor.execute( { -> process(evt.file) } as Runnable)
|
executor.execute( { -> process(evt.file) } as Runnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ class PersisterService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
timer.schedule({load()} as TimerTask, 1000)
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
|
@@ -65,7 +65,7 @@ class CacheClient {
|
|||||||
options.setSendLeaseSet(true)
|
options.setSendLeaseSet(true)
|
||||||
CacheServers.getCacheServers().each {
|
CacheServers.getCacheServers().each {
|
||||||
log.info "Querying hostcache ${it.toBase32()}"
|
log.info "Querying hostcache ${it.toBase32()}"
|
||||||
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
|
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import com.muwire.core.util.DataUtil
|
|||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class ResultsParser {
|
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")
|
if (json.type != "Result")
|
||||||
throw new InvalidSearchResultException("not a result json")
|
throw new InvalidSearchResultException("not a result json")
|
||||||
if (json.version != 1)
|
if (json.version != 1)
|
||||||
@@ -46,7 +46,8 @@ class ResultsParser {
|
|||||||
name : name,
|
name : name,
|
||||||
size : size,
|
size : size,
|
||||||
infohash : parsedIH,
|
infohash : parsedIH,
|
||||||
pieceSize : pieceSize)
|
pieceSize : pieceSize,
|
||||||
|
uuid : uuid)
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
}
|
}
|
||||||
|
@@ -55,7 +55,8 @@ class ResultsSender {
|
|||||||
name : it.getFile().getName(),
|
name : it.getFile().getName(),
|
||||||
size : length,
|
size : length,
|
||||||
infohash : it.getInfoHash(),
|
infohash : it.getInfoHash(),
|
||||||
pieceSize : FileHasher.getPieceSize(length)
|
pieceSize : FileHasher.getPieceSize(length),
|
||||||
|
uuid : uuid
|
||||||
)
|
)
|
||||||
eventBus.publish(uiResultEvent)
|
eventBus.publish(uiResultEvent)
|
||||||
}
|
}
|
||||||
|
@@ -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(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import com.muwire.core.Persona
|
|||||||
|
|
||||||
class UIResultEvent extends Event {
|
class UIResultEvent extends Event {
|
||||||
Persona sender
|
Persona sender
|
||||||
|
UUID uuid
|
||||||
String name
|
String name
|
||||||
long size
|
long size
|
||||||
InfoHash infohash
|
InfoHash infohash
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UpdateAvailableEvent extends Event {
|
||||||
|
String version
|
||||||
|
String signer
|
||||||
|
}
|
132
core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy
Normal file
132
core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.client.I2PSession
|
||||||
|
import net.i2p.client.I2PSessionMuxedListener
|
||||||
|
import net.i2p.client.SendMessageOptions
|
||||||
|
import net.i2p.client.datagram.I2PDatagramDissector
|
||||||
|
import net.i2p.client.datagram.I2PDatagramMaker
|
||||||
|
import net.i2p.util.VersionComparator
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class UpdateClient {
|
||||||
|
final EventBus eventBus
|
||||||
|
final I2PSession session
|
||||||
|
final String myVersion
|
||||||
|
final MuWireSettings settings
|
||||||
|
|
||||||
|
private final Timer timer
|
||||||
|
|
||||||
|
private long lastUpdateCheckTime
|
||||||
|
|
||||||
|
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.session = session
|
||||||
|
this.myVersion = myVersion
|
||||||
|
this.settings = settings
|
||||||
|
timer = new Timer("update-client",true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||||
|
timer.schedule({checkUpdate()} as TimerTask, 30000, 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
timer.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkUpdate() {
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
if (lastUpdateCheckTime > 0) {
|
||||||
|
if (now - lastUpdateCheckTime < settings.updateCheckInterval * 60 * 60 * 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastUpdateCheckTime = now
|
||||||
|
|
||||||
|
log.info("checking for update")
|
||||||
|
|
||||||
|
def ping = [version : 1, myVersion : myVersion]
|
||||||
|
ping = JsonOutput.toJson(ping)
|
||||||
|
def maker = new I2PDatagramMaker(session)
|
||||||
|
ping = maker.makeI2PDatagram(ping.bytes)
|
||||||
|
def options = new SendMessageOptions()
|
||||||
|
options.setSendLeaseSet(true)
|
||||||
|
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
|
final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||||
|
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||||
|
log.warning "Received unexpected protocol $proto"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
def payload = session.receiveMessage(msgId)
|
||||||
|
def dissector = new I2PDatagramDissector()
|
||||||
|
try {
|
||||||
|
dissector.loadI2PDatagram(payload)
|
||||||
|
def sender = dissector.getSender()
|
||||||
|
if (sender != UpdateServers.UPDATE_SERVER) {
|
||||||
|
log.warning("received something not from update server " + sender.toBase32())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Received something from update server")
|
||||||
|
|
||||||
|
payload = dissector.getPayload()
|
||||||
|
payload = slurper.parse(payload)
|
||||||
|
|
||||||
|
if (payload.version == null) {
|
||||||
|
log.warning("version missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.signer == null) {
|
||||||
|
log.warning("signer missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
|
||||||
|
log.info("no new version available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("new version $payload.version available, publishing event")
|
||||||
|
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer))
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING,"Invalid datagram",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportAbuse(I2PSession session, int severity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnected(I2PSession session) {
|
||||||
|
log.severe("I2P session disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
|
log.log(Level.SEVERE, message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
class UpdateServers {
|
||||||
|
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
|
||||||
|
}
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
11
core/src/test/groovy/com/muwire/core/Personas.groovy
Normal file
11
core/src/test/groovy/com/muwire/core/Personas.groovy
Normal 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)))
|
||||||
|
}
|
@@ -15,7 +15,7 @@ class DownloadSessionTest {
|
|||||||
private File source, target
|
private File source, target
|
||||||
private InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private Endpoint endpoint
|
private Endpoint endpoint
|
||||||
private Pieces pieces
|
private Pieces pieces, claimed
|
||||||
private String rootBase64
|
private String rootBase64
|
||||||
|
|
||||||
private DownloadSession session
|
private DownloadSession session
|
||||||
@@ -24,7 +24,7 @@ class DownloadSessionTest {
|
|||||||
private InputStream fromDownloader, fromUploader
|
private InputStream fromDownloader, fromUploader
|
||||||
private OutputStream toDownloader, toUploader
|
private OutputStream toDownloader, toUploader
|
||||||
|
|
||||||
private void initSession(int size) {
|
private void initSession(int size, def claimedPieces = []) {
|
||||||
Random r = new Random()
|
Random r = new Random()
|
||||||
byte [] content = new byte[size]
|
byte [] content = new byte[size]
|
||||||
r.nextBytes(content)
|
r.nextBytes(content)
|
||||||
@@ -48,6 +48,8 @@ class DownloadSessionTest {
|
|||||||
else
|
else
|
||||||
nPieces = size / pieceSize + 1
|
nPieces = size / pieceSize + 1
|
||||||
pieces = new Pieces(nPieces)
|
pieces = new Pieces(nPieces)
|
||||||
|
claimed = new Pieces(nPieces)
|
||||||
|
claimedPieces.each {claimed.markDownloaded(it)}
|
||||||
|
|
||||||
fromDownloader = new PipedInputStream()
|
fromDownloader = new PipedInputStream()
|
||||||
fromUploader = new PipedInputStream()
|
fromUploader = new PipedInputStream()
|
||||||
@@ -55,7 +57,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, claimed, 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 +76,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 +98,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 +126,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)
|
||||||
@@ -135,4 +140,29 @@ class DownloadSessionTest {
|
|||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
assert 1 == pieces.donePieces()
|
assert 1 == pieces.donePieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSmallFileClaimed() {
|
||||||
|
initSession(20, [0])
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
downloadThread.join(100)
|
||||||
|
assert 100 > (System.currentTimeMillis() - now)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimedPiecesAvoided() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) * 10
|
||||||
|
initSession(size, [1,2,3,4,5,6,7,8,9])
|
||||||
|
assert !claimed.isMarked(0)
|
||||||
|
|
||||||
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
|
String range = readTillRN(fromDownloader)
|
||||||
|
def matcher = (range =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
int start = Integer.parseInt(matcher[0][1])
|
||||||
|
int end = Integer.parseInt(matcher[0][2])
|
||||||
|
|
||||||
|
assert claimed.isMarked(0)
|
||||||
|
assert start == 0 && end == (1 << pieceSize) - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.0.2
|
version = 0.0.9
|
||||||
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
|
||||||
|
@@ -41,6 +41,7 @@ griffon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mainClassName = 'com.muwire.gui.Launcher'
|
mainClassName = 'com.muwire.gui.Launcher'
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
|
|
||||||
apply from: 'gradle/publishing.gradle'
|
apply from: 'gradle/publishing.gradle'
|
||||||
apply from: 'gradle/code-coverage.gradle'
|
apply from: 'gradle/code-coverage.gradle'
|
||||||
@@ -58,6 +59,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}"
|
||||||
|
@@ -16,4 +16,14 @@ mvcGroups {
|
|||||||
view = 'com.muwire.gui.MainFrameView'
|
view = 'com.muwire.gui.MainFrameView'
|
||||||
controller = 'com.muwire.gui.MainFrameController'
|
controller = 'com.muwire.gui.MainFrameController'
|
||||||
}
|
}
|
||||||
|
'SearchTab' {
|
||||||
|
model = 'com.muwire.gui.SearchTabModel'
|
||||||
|
view = 'com.muwire.gui.SearchTabView'
|
||||||
|
controller = 'com.muwire.gui.SearchTabController'
|
||||||
|
}
|
||||||
|
'Options' {
|
||||||
|
model = 'com.muwire.gui.OptionsModel'
|
||||||
|
view = 'com.muwire.gui.OptionsView'
|
||||||
|
controller = 'com.muwire.gui.OptionsController'
|
||||||
|
}
|
||||||
}
|
}
|
@@ -3,11 +3,14 @@ package com.muwire.gui
|
|||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonController
|
import griffon.core.artifact.GriffonController
|
||||||
import griffon.core.controller.ControllerAction
|
import griffon.core.controller.ControllerAction
|
||||||
|
import griffon.core.mvc.MVCGroup
|
||||||
|
import griffon.core.mvc.MVCGroupConfiguration
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
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
|
||||||
@@ -29,35 +32,109 @@ 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 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,
|
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() {
|
||||||
def resultsTable = builder.getVariable("results-table")
|
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||||
int row = resultsTable.getSelectedRow()
|
def group = selected.getClientProperty("mvc-group")
|
||||||
model.results[row]
|
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
|
@ControllerAction
|
||||||
void download() {
|
void download() {
|
||||||
def result = selectedResult()
|
def result = selectedResult()
|
||||||
|
if (result == null)
|
||||||
|
return // TODO disable button
|
||||||
|
|
||||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||||
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
|
|
||||||
|
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||||
|
def group = selected.getClientProperty("mvc-group")
|
||||||
|
|
||||||
|
def resultsBucket = group.model.hashBucket[result.infohash]
|
||||||
|
|
||||||
|
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void trust() {
|
void trust() {
|
||||||
def result = selectedResult()
|
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
|
@ControllerAction
|
||||||
void distrust() {
|
void distrust() {
|
||||||
def result = selectedResult()
|
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) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonController)
|
||||||
|
class OptionsController {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
OptionsModel model
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
OptionsView view
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void save() {
|
||||||
|
String text
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
|
||||||
|
def i2pProps = core.i2pOptions
|
||||||
|
|
||||||
|
text = view.inboundLengthField.text
|
||||||
|
model.inboundLength = text
|
||||||
|
i2pProps["inbound.length"] = text
|
||||||
|
|
||||||
|
text = view.inboundQuantityField.text
|
||||||
|
model.inboundQuantity = text
|
||||||
|
i2pProps["inbound.quantity"] = text
|
||||||
|
|
||||||
|
text = view.outboundQuantityField.text
|
||||||
|
model.outboundQuantity = text
|
||||||
|
i2pProps["outbound.quantity"] = text
|
||||||
|
|
||||||
|
text = view.outboundLengthField.text
|
||||||
|
model.outboundLength = text
|
||||||
|
i2pProps["outbound.length"] = text
|
||||||
|
|
||||||
|
File i2pSettingsFile = new File(core.home, "i2p.properties")
|
||||||
|
i2pSettingsFile.withOutputStream {
|
||||||
|
i2pProps.store(it,"")
|
||||||
|
}
|
||||||
|
|
||||||
|
text = view.retryField.text
|
||||||
|
model.downloadRetryInterval = text
|
||||||
|
|
||||||
|
def settings = application.context.get("muwire-settings")
|
||||||
|
settings.downloadRetryInterval = Integer.valueOf(text)
|
||||||
|
|
||||||
|
text = view.updateField.text
|
||||||
|
model.updateCheckInterval = text
|
||||||
|
settings.updateCheckInterval = Integer.valueOf(text)
|
||||||
|
|
||||||
|
File settingsFile = new File(core.home, "MuWire.properties")
|
||||||
|
settingsFile.withOutputStream {
|
||||||
|
settings.write(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void cancel() {
|
||||||
|
view.d.setVisible(false)
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
@Override
|
||||||
void execute() {
|
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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
|
import griffon.core.env.Metadata
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||||
@@ -16,9 +17,13 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
|
|||||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||||
|
|
||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class Ready extends AbstractLifecycleHandler {
|
class Ready extends AbstractLifecycleHandler {
|
||||||
|
|
||||||
|
@Inject Metadata metadata
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Ready(@Nonnull GriffonApplication application) {
|
Ready(@Nonnull GriffonApplication application) {
|
||||||
super(application)
|
super(application)
|
||||||
@@ -27,7 +32,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")
|
||||||
@@ -63,8 +72,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)
|
||||||
@@ -74,15 +88,22 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home)
|
Core core
|
||||||
|
try {
|
||||||
|
core = new Core(props, home, metadata["application.version"])
|
||||||
|
} 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()
|
core.startServices()
|
||||||
application.context.put("muwire-settings", props)
|
application.context.put("muwire-settings", props)
|
||||||
application.context.put("core",core)
|
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,29 +1,38 @@
|
|||||||
package com.muwire.gui
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.JTable
|
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
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
import com.muwire.core.update.UpdateAvailableEvent
|
||||||
import com.muwire.core.upload.UploadEvent
|
import com.muwire.core.upload.UploadEvent
|
||||||
import com.muwire.core.upload.UploadFinishedEvent
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
|
||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.core.mvc.MVCGroup
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.transform.FXObservable
|
import griffon.transform.FXObservable
|
||||||
import griffon.transform.Observable
|
import griffon.transform.Observable
|
||||||
|
import net.i2p.data.Destination
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
@ArtifactProviderFor(GriffonModel)
|
@ArtifactProviderFor(GriffonModel)
|
||||||
@@ -33,18 +42,49 @@ class MainFrameModel {
|
|||||||
@Inject @Nonnull GriffonApplication application
|
@Inject @Nonnull GriffonApplication application
|
||||||
@Observable boolean coreInitialized = false
|
@Observable boolean coreInitialized = false
|
||||||
|
|
||||||
@Observable def results = []
|
def results = new ConcurrentHashMap<>()
|
||||||
@Observable def downloads = []
|
def downloads = []
|
||||||
@Observable def uploads = []
|
def uploads = []
|
||||||
@Observable def shared = []
|
def shared = []
|
||||||
|
def connectionList = []
|
||||||
|
def searches = new LinkedList()
|
||||||
|
def trusted = []
|
||||||
|
def distrusted = []
|
||||||
|
|
||||||
@Observable int connections
|
@Observable int connections
|
||||||
@Observable String me
|
@Observable String me
|
||||||
|
@Observable boolean searchButtonsEnabled
|
||||||
|
@Observable boolean cancelButtonEnabled
|
||||||
|
@Observable boolean retryButtonEnabled
|
||||||
|
|
||||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||||
|
|
||||||
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()
|
||||||
@@ -58,22 +98,41 @@ class MainFrameModel {
|
|||||||
core.eventBus.register(UploadEvent.class, this)
|
core.eventBus.register(UploadEvent.class, this)
|
||||||
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)
|
||||||
Timer timer = new Timer("download-pumper", true)
|
core.eventBus.register(UpdateAvailableEvent.class, this)
|
||||||
|
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
|
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||||
|
if (retryInterval > 0) {
|
||||||
|
retryInterval *= 60000
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
if (now - lastRetryTime > retryInterval) {
|
||||||
|
lastRetryTime = now
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
builder.getVariable("downloads-table").model.fireTableDataChanged()
|
downloads.each {
|
||||||
builder.getVariable("uploads-table").model.fireTableDataChanged()
|
def state = it.downloader.currentState
|
||||||
|
if (state == Downloader.DownloadState.FAILED ||
|
||||||
|
state == Downloader.DownloadState.DOWNLOADING)
|
||||||
|
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) {
|
||||||
runInsideUIAsync {
|
MVCGroup resultsGroup = results.get(e.uuid)
|
||||||
results << e
|
resultsGroup?.model.handleResult(e)
|
||||||
JTable table = builder.getVariable("results-table")
|
|
||||||
table.model.fireTableDataChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||||
@@ -83,14 +142,34 @@ class MainFrameModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onConnectionEvent(ConnectionEvent e) {
|
void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
return
|
||||||
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)
|
||||||
|
JTable table = builder.getVariable("connections-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
JTable table = builder.getVariable("connections-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +215,50 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void onTrustEvent(TrustEvent e) {
|
void onTrustEvent(TrustEvent e) {
|
||||||
runInsideUIAsync {
|
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(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
||||||
|
while(searches.size() > 200)
|
||||||
|
searches.removeLast()
|
||||||
|
JTable table = builder.getVariable("searches-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IncomingSearch {
|
||||||
|
String search
|
||||||
|
Destination replyTo
|
||||||
|
Persona originator
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
JOptionPane.showMessageDialog(null, "A new version of MuWire is available from $e.signer. Please update to $e.version")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
30
gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy
Normal file
30
gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.transform.Observable
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonModel)
|
||||||
|
class OptionsModel {
|
||||||
|
@Observable String downloadRetryInterval
|
||||||
|
@Observable String updateCheckInterval
|
||||||
|
|
||||||
|
// i2p options
|
||||||
|
@Observable String inboundLength
|
||||||
|
@Observable String inboundQuantity
|
||||||
|
@Observable String outboundLength
|
||||||
|
@Observable String outboundQuantity
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
|
downloadRetryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||||
|
updateCheckInterval = application.context.get("muwire-settings").updateCheckInterval
|
||||||
|
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
inboundLength = core.i2pOptions["inbound.length"]
|
||||||
|
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||||
|
outboundLength = core.i2pOptions["outbound.length"]
|
||||||
|
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||||
|
}
|
||||||
|
}
|
50
gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy
Normal file
50
gui/griffon-app/models/com/muwire/gui/SearchTabModel.groovy
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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 = []
|
||||||
|
def hashBucket = [:]
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
def bucket = hashBucket.get(e.infohash)
|
||||||
|
if (bucket == null) {
|
||||||
|
bucket = []
|
||||||
|
hashBucket[e.infohash] = bucket
|
||||||
|
}
|
||||||
|
bucket << e
|
||||||
|
|
||||||
|
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 |
@@ -3,15 +3,21 @@ package com.muwire.gui
|
|||||||
import griffon.core.artifact.GriffonView
|
import griffon.core.artifact.GriffonView
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
import javax.swing.Box
|
import javax.swing.Box
|
||||||
import javax.swing.BoxLayout
|
import javax.swing.BoxLayout
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
|
import javax.swing.JLabel
|
||||||
import javax.swing.JSplitPane
|
import javax.swing.JSplitPane
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.border.Border
|
import javax.swing.border.Border
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.download.Downloader
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
@@ -20,6 +26,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
|
||||||
|
|
||||||
@@ -41,6 +48,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()
|
||||||
@@ -48,10 +60,18 @@ class MainFrameView {
|
|||||||
gridLayout(rows:1, cols: 2)
|
gridLayout(rows:1, cols: 2)
|
||||||
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: "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)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -59,6 +79,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") {
|
||||||
@@ -67,22 +89,11 @@ class MainFrameView {
|
|||||||
continuousLayout : true, constraints : BorderLayout.CENTER) {
|
continuousLayout : true, constraints : BorderLayout.CENTER) {
|
||||||
panel (constraints : JSplitPane.TOP) {
|
panel (constraints : JSplitPane.TOP) {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
scrollPane (constraints : BorderLayout.CENTER){
|
tabbedPane(id : "result-tabs", 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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panel(constraints : BorderLayout.SOUTH) {
|
panel(constraints : BorderLayout.SOUTH) {
|
||||||
button(text : "Download", downloadAction)
|
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
|
||||||
button(text : "Trust", trustAction)
|
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
|
||||||
button(text : "Distrust", distrustAction)
|
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel (constraints : JSplitPane.BOTTOM) {
|
panel (constraints : JSplitPane.BOTTOM) {
|
||||||
@@ -90,21 +101,24 @@ class MainFrameView {
|
|||||||
scrollPane (constraints : BorderLayout.CENTER) {
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
table(id : "downloads-table") {
|
table(id : "downloads-table") {
|
||||||
tableModel(list: model.downloads) {
|
tableModel(list: model.downloads) {
|
||||||
closureColumn(header: "Name", type: String, read : {row -> row.downloader.file.getName()})
|
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
|
||||||
closureColumn(header: "Status", type: String, read : {row -> row.downloader.getCurrentState()})
|
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState()})
|
||||||
closureColumn(header: "Progress", type: String, read: { row ->
|
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
|
||||||
int pieces = row.downloader.nPieces
|
int pieces = row.downloader.nPieces
|
||||||
int done = row.downloader.donePieces()
|
int done = row.downloader.donePieces()
|
||||||
"$done/$pieces pieces"
|
"$done/$pieces pieces"
|
||||||
})
|
})
|
||||||
closureColumn(header: "Piece", type: String, read: { row ->
|
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
||||||
int position = row.downloader.positionInPiece()
|
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
||||||
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
|
DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
|
||||||
"$position/$pieceSize bytes"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
panel (constraints : BorderLayout.SOUTH) {
|
||||||
|
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
|
||||||
|
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,20 +127,23 @@ class MainFrameView {
|
|||||||
panel {
|
panel {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
panel (constraints : BorderLayout.NORTH) {
|
panel (constraints : BorderLayout.NORTH) {
|
||||||
button(text : "Shared files", actionPerformed : shareFiles)
|
button(text : "Click here to share files", actionPerformed : shareFiles)
|
||||||
}
|
}
|
||||||
scrollPane ( constraints : BorderLayout.CENTER) {
|
scrollPane ( constraints : BorderLayout.CENTER) {
|
||||||
table(id : "shared-files-table") {
|
table(id : "shared-files-table") {
|
||||||
tableModel(list : model.shared) {
|
tableModel(list : model.shared) {
|
||||||
closureColumn(header : "Name", type : String, read : {row -> row.file.getAbsolutePath()})
|
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
|
||||||
closureColumn(header : "Size", type : Long, read : {row -> row.file.length()})
|
closureColumn(header : "Size", preferredWidth : 50, type : String,
|
||||||
|
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel {
|
panel {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
label("Uploads", constraints : BorderLayout.NORTH)
|
panel (constraints : BorderLayout.NORTH){
|
||||||
|
label("Uploads")
|
||||||
|
}
|
||||||
scrollPane (constraints : BorderLayout.CENTER) {
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
table(id : "uploads-table") {
|
table(id : "uploads-table") {
|
||||||
tableModel(list : model.uploads) {
|
tableModel(list : model.uploads) {
|
||||||
@@ -138,11 +155,86 @@ 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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 : {
|
||||||
|
sanitized = it.search.replace('<', ' ')
|
||||||
|
sanitized
|
||||||
|
})
|
||||||
|
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()
|
||||||
@@ -157,6 +249,34 @@ 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 centerRenderer = new DefaultTableCellRenderer()
|
||||||
|
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||||
|
builder.getVariable("downloads-table").setDefaultRenderer(Integer.class, centerRenderer)
|
||||||
|
}
|
||||||
|
|
||||||
def showSearchWindow = {
|
def showSearchWindow = {
|
||||||
def cardsPanel = builder.getVariable("cards-panel")
|
def cardsPanel = builder.getVariable("cards-panel")
|
||||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||||
@@ -167,6 +287,16 @@ class MainFrameView {
|
|||||||
cardsPanel.getLayout().show(cardsPanel, "uploads window")
|
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 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")
|
||||||
|
95
gui/griffon-app/views/com/muwire/gui/OptionsView.groovy
Normal file
95
gui/griffon-app/views/com/muwire/gui/OptionsView.groovy
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.JTabbedPane
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
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 i
|
||||||
|
def retryField
|
||||||
|
def updateField
|
||||||
|
|
||||||
|
def inboundLengthField
|
||||||
|
def inboundQuantityField
|
||||||
|
def outboundLengthField
|
||||||
|
def outboundQuantityField
|
||||||
|
|
||||||
|
def buttonsPanel
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
|
||||||
|
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
|
||||||
|
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
|
||||||
|
|
||||||
|
}
|
||||||
|
i = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||||
|
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
|
||||||
|
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
|
||||||
|
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
|
||||||
|
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
|
||||||
|
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
|
||||||
|
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
|
||||||
|
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
|
||||||
|
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
||||||
|
}
|
||||||
|
buttonsPanel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
||||||
|
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
|
def tabbedPane = new JTabbedPane()
|
||||||
|
tabbedPane.addTab("MuWire Options", p)
|
||||||
|
tabbedPane.addTab("I2P Options", i)
|
||||||
|
|
||||||
|
JPanel panel = new JPanel()
|
||||||
|
panel.setLayout(new BorderLayout())
|
||||||
|
panel.add(tabbedPane, BorderLayout.CENTER)
|
||||||
|
panel.add(buttonsPanel, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
d.getContentPane().add(panel)
|
||||||
|
d.pack()
|
||||||
|
d.setLocationRelativeTo(mainFrame)
|
||||||
|
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||||
|
d.addWindowListener(new WindowAdapter() {
|
||||||
|
public void windowClosed(WindowEvent e) {
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
d.show()
|
||||||
|
}
|
||||||
|
}
|
95
gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy
Normal file
95
gui/griffon-app/views/com/muwire/gui/SearchTabView.groovy
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.core.mvc.MVCGroup
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
|
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
|
||||||
|
def resultsTable
|
||||||
|
|
||||||
|
void initUI() {
|
||||||
|
builder.with {
|
||||||
|
def resultsTable
|
||||||
|
def pane = scrollPane {
|
||||||
|
resultsTable = table(id : "results-table") {
|
||||||
|
tableModel(list: model.results) {
|
||||||
|
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||||
|
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
|
||||||
|
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||||
|
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
|
||||||
|
closureColumn(header: "Trust", preferredWidth: 50, 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)
|
||||||
|
|
||||||
|
this.resultsTable = 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.indexOfComponent(pane)
|
||||||
|
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 centerRenderer = new DefaultTableCellRenderer()
|
||||||
|
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||||
|
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
|
||||||
|
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
|
||||||
|
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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!')
|
||||||
|
}
|
||||||
|
}
|
@@ -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(OptionsController)
|
||||||
|
class OptionsControllerTest {
|
||||||
|
private OptionsController controller
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final GriffonUnitRule griffon = new GriffonUnitRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void smokeTest() {
|
||||||
|
fail('Not yet implemented!')
|
||||||
|
}
|
||||||
|
}
|
@@ -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!')
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
include 'pinger'
|
include 'pinger'
|
||||||
include 'host-cache'
|
include 'host-cache'
|
||||||
|
include 'update-server'
|
||||||
include 'core'
|
include 'core'
|
||||||
include 'gui'
|
include 'gui'
|
||||||
|
include 'cli'
|
||||||
|
3
update-server/build.gradle
Normal file
3
update-server/build.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apply plugin : 'application'
|
||||||
|
mainClassName = 'com.muwire.update.UpdateServer'
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
@@ -0,0 +1,105 @@
|
|||||||
|
package com.muwire.update
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.client.I2PClientFactory
|
||||||
|
import net.i2p.client.I2PSession
|
||||||
|
import net.i2p.client.I2PSessionMuxedListener
|
||||||
|
import net.i2p.client.datagram.I2PDatagramDissector
|
||||||
|
import net.i2p.client.datagram.I2PDatagramMaker
|
||||||
|
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class UpdateServer {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
def home = System.getProperty("user.home") + "/.MuWireUpdateServer"
|
||||||
|
home = new File(home)
|
||||||
|
if (!home.exists())
|
||||||
|
home.mkdirs()
|
||||||
|
|
||||||
|
def keyFile = new File(home, "key.dat")
|
||||||
|
|
||||||
|
def i2pClientFactory = new I2PClientFactory()
|
||||||
|
def i2pClient = i2pClientFactory.createClient()
|
||||||
|
|
||||||
|
def myDest
|
||||||
|
def session
|
||||||
|
if (!keyFile.exists()) {
|
||||||
|
def os = new FileOutputStream(keyFile);
|
||||||
|
myDest = i2pClient.createDestination(os)
|
||||||
|
os.close()
|
||||||
|
log.info "No key.dat file was found, so creating a new destination."
|
||||||
|
log.info "This is the destination you want to give out for your new UpdateServer"
|
||||||
|
log.info myDest.toBase64()
|
||||||
|
}
|
||||||
|
|
||||||
|
def update = new File(home, "update.json")
|
||||||
|
if (!update.exists()) {
|
||||||
|
log.warning("update file doesn't exist, exiting")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def props = System.getProperties().clone()
|
||||||
|
props.putAt("inbound.nickname", "MuWire UpdateServer")
|
||||||
|
session = i2pClient.createSession(new FileInputStream(keyFile), props)
|
||||||
|
myDest = session.getMyDestination()
|
||||||
|
|
||||||
|
session.addMuxedSessionListener(new Listener(update), I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
|
||||||
|
session.connect()
|
||||||
|
log.info("Connected, going to sleep")
|
||||||
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
|
private final File json
|
||||||
|
|
||||||
|
Listener(File json) {
|
||||||
|
this.json = json
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||||
|
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||||
|
log.warning("received uknown protocol $proto")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
def payload = session.receiveMessage(msgId)
|
||||||
|
def dissector = new I2PDatagramDissector()
|
||||||
|
try {
|
||||||
|
dissector.loadI2PDatagram(payload)
|
||||||
|
def sender = dissector.getSender()
|
||||||
|
log.info("Got an update ping from "+sender.toBase32())
|
||||||
|
// I don't think we care about the payload at this point
|
||||||
|
def maker = new I2PDatagramMaker(session)
|
||||||
|
def response = maker.makeI2PDatagram(json.bytes)
|
||||||
|
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "exception responding to update request",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportAbuse(I2PSession session, int severity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnected(I2PSession session) {
|
||||||
|
Log.severe("Disconnected from I2P router")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
|
log.log(Level.SEVERE, message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user