Compare commits

..

49 Commits

Author SHA1 Message Date
Zlatin Balevsky
c81f963e0a Release 0.1.4 2019-06-09 17:37:10 +01:00
Zlatin Balevsky
dc6b1199f3 implement resume across restart 2019-06-09 17:35:32 +01:00
Zlatin Balevsky
42621a2dfb wip on persisting downloads between restarts 2019-06-09 16:26:00 +01:00
Zlatin Balevsky
a7125963a7 DownloadManager listens to events, not FileManager 2019-06-09 16:19:35 +01:00
Zlatin Balevsky
f39d7f4fa8 emit an event when the UI loads 2019-06-09 15:44:06 +01:00
Zlatin Balevsky
b88334f19a Release 0.1.3 for sorting fixes 2019-06-08 17:57:36 +01:00
Zlatin Balevsky
81e186ad1f fix sorting by download status and trust, fix events on downloads table 2019-06-08 17:55:39 +01:00
Zlatin Balevsky
33a45c3835 fix buttons when tables are sorted 2019-06-08 17:09:44 +01:00
Zlatin Balevsky
32b7867e44 Release 0.1.2 for search index test 2019-06-08 13:09:28 +01:00
Zlatin Balevsky
5b313276f4 fix tests broken by piece size change 2019-06-08 13:08:20 +01:00
Zlatin Balevsky
abba4cc6fa fix a bug where multi-term search modifies the index 2019-06-08 12:55:47 +01:00
Zlatin Balevsky
15b4804968 update wire protocol with originator and oobHashlist fields 2019-06-08 12:40:38 +01:00
Zlatin Balevsky
942a01a501 forgot to commit 2019-06-08 09:33:16 +01:00
Zlatin Balevsky
502a8d91da print only the root 2019-06-08 09:30:01 +01:00
Zlatin Balevsky
5414e8679b update readme 2019-06-08 09:07:13 +01:00
Zlatin Balevsky
14e42dd7c2 correct element 2019-06-08 08:46:28 +01:00
Zlatin Balevsky
1299fb2512 Release 0.1.1 for fixes and reduced piece size 2019-06-08 08:04:35 +01:00
Zlatin Balevsky
9bafdfe0b1 reduce piece size 2019-06-08 07:57:36 +01:00
Zlatin Balevsky
36eb632756 do not set the flag until it is implemented 2019-06-08 07:53:33 +01:00
Zlatin Balevsky
83ee620402 sort by columns 2019-06-08 07:45:07 +01:00
Zlatin Balevsky
3fe40d317d update readme for custom host:port 2019-06-08 07:28:23 +01:00
Zlatin Balevsky
e9703a2652 support for custom i2cp host:port 2019-06-08 07:23:14 +01:00
Zlatin Balevsky
a3fe89851f OS-specific home dir 2019-06-08 07:10:24 +01:00
Zlatin Balevsky
b9ea0128cd add oobInfohash flag, filter results by that flag 2019-06-08 02:44:49 +01:00
Zlatin Balevsky
53c6db4ec8 de-hardcode piece sizes in results 2019-06-08 01:48:07 +01:00
Zlatin Balevsky
60776829b9 fix disabling sharing of downloaded files 2019-06-08 01:35:03 +01:00
Zlatin Balevsky
b5cb31c23d proposed infohash upgrade document 2019-06-08 01:04:56 +01:00
Zlatin Balevsky
5052c0c993 note about downloads in progress 2019-06-07 21:52:38 +01:00
Zlatin Balevsky
06de007866 update readme 2019-06-07 21:22:49 +01:00
Zlatin Balevsky
7c8a0c9ad9 update readme for 0.1.0 2019-06-07 19:24:13 +01:00
Zlatin Balevsky
cda81a89a2 Release 0.1.0 2019-06-07 18:39:39 +01:00
Zlatin Balevsky
483773422c fix remaining tests 2019-06-07 18:23:16 +01:00
Zlatin Balevsky
1e1e6d0bb0 fix test 2019-06-07 18:17:16 +01:00
Zlatin Balevsky
668d6e087d fix test 2019-06-07 18:15:03 +01:00
Zlatin Balevsky
49af412b96 status update and auto-retry 2019-06-07 16:13:35 +01:00
Zlatin Balevsky
d5513021ed Release 0.0.14 for split search 2019-06-07 15:00:16 +01:00
Zlatin Balevsky
c3154cf717 stray println 2019-06-07 14:58:03 +01:00
Zlatin Balevsky
114940c4c1 fix searches with spaces 2019-06-07 14:51:09 +01:00
Zlatin Balevsky
d4336e9b5d outbound nickname 2019-06-07 14:24:45 +01:00
Zlatin Balevsky
2c1d5508ed outbound nickname 2019-06-07 14:21:03 +01:00
Zlatin Balevsky
1cebf6c7bd cli downloader 2019-06-07 14:02:10 +01:00
Zlatin Balevsky
e12924a207 shadow jar for cli 2019-06-07 14:01:28 +01:00
Zlatin Balevsky
f3b11895e4 utility for hashing files 2019-06-07 12:10:18 +01:00
Zlatin Balevsky
1e084820fb log tweak 2019-06-07 11:55:17 +01:00
Zlatin Balevsky
2198b4846d change wording 2019-06-07 11:43:02 +01:00
Zlatin Balevsky
a5d442d320 Release 0.0.13 for keyword search fix 2019-06-07 06:37:23 +01:00
Zlatin Balevsky
3f9ee887d6 prevent NPE in toString 2019-06-07 06:31:29 +01:00
Zlatin Balevsky
4a9e6d3b6b prevent npe in keyword searches 2019-06-07 06:14:40 +01:00
Zlatin Balevsky
80f2cc5f99 logging and toString() 2019-06-07 06:07:02 +01:00
40 changed files with 562 additions and 106 deletions

View File

@@ -1,10 +1,10 @@
# MuWire - Easy Anonymous File-Sharing # MuWire - Easy Anonymous File-Sharing
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The project is in development. You can find technical documentation in the "doc" folder. The first stable release - 0.1.0 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
### Building ### Building
@@ -23,15 +23,12 @@ Some of the UI tests will fail because they haven't been written yet :-/
### Running ### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside. You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
At the moment there are very few nodes on the network, so you will see very few connections and search results. It is best to leave MuWire running all the time, just like I2P.
### Known bugs and limitations ### Known bugs and limitations
* Many UI features you would expect are not there yet * Many UI features you would expect are not there yet

View File

@@ -1,8 +1,22 @@
apply plugin : 'application' buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin : 'application'
mainClassName = 'com.muwire.cli.Cli' mainClassName = 'com.muwire.cli.Cli'
apply plugin : 'com.github.johnrengelman.shadow'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies { dependencies {
compile project(":core") compile project(":core")
} }

View File

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

View File

@@ -0,0 +1,166 @@
package com.muwire.cli
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
class CliDownloader {
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
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)
def filesList
int connections
int resultWait
if (args.length != 3) {
println "Enter a file containing list of hashes of files to download, " +
"how many connections you want before searching" +
"and how long to wait for results to arrive"
System.exit(1)
} else {
filesList = args[0]
connections = Integer.parseInt(args[1])
resultWait = Integer.parseInt(args[2])
}
Core core
try {
core = new Core(props, home, "0.1.4")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
System.exit(1)
}
def latch = new CountDownLatch(connections)
def connectionListener = new ConnectionWaiter(latch : latch)
core.eventBus.register(ConnectionEvent.class, connectionListener)
core.startServices()
println "starting to wait until there are $connections connections"
latch.await()
println "connected, searching for files"
def file = new File(filesList)
file.eachLine {
String[] split = it.split(",")
UUID uuid = UUID.randomUUID()
core.eventBus.register(UIResultEvent.class, new ResultsListener(fileName : split[1]))
def hash = Base64.decode(split[0])
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
}
println "waiting for results to arrive"
Thread.sleep(resultWait * 1000)
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
resultsListeners.each { uuid, resultsListener ->
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
File target = new File(resultsListener.fileName)
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
}
Thread.sleep(1000)
Timer timer = new Timer("stats-printer")
timer.schedule({
println "==== STATUS UPDATE ==="
downloaders.each {
int donePieces = it.donePieces()
int totalPieces = it.nPieces
int sources = it.activeWorkers.size()
def root = Base64.encode(it.infoHash.getRoot())
def state = it.getCurrentState()
println "file $it.file hash: $root progress: $donePieces/$totalPieces sources: $sources status: $state}"
it.resume()
}
println "==== END ==="
} as TimerTask, 60000, 60000)
println "waiting for downloads to finish"
while(true) {
boolean allFinished = true
for (Downloader d : downloaders) {
allFinished &= d.getCurrentState() == Downloader.DownloadState.FINISHED
}
if (allFinished)
break
Thread.sleep(1000)
}
println "all downloads finished"
}
static class ResultsHolder {
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
String fileName
void add(UIResultEvent e) {
results.add(e)
}
List getResults() {
results
}
}
static class ResultsListener {
UUID uuid
String fileName
public onUIResultEvent(UIResultEvent e) {
println "got a result for $fileName from ${e.sender.getHumanReadableName()}"
ResultsHolder listener = resultsListeners.get(e.uuid)
if (listener == null) {
listener = new ResultsHolder(fileName : fileName)
resultsListeners.put(e.uuid, listener)
}
listener.add(e)
}
}
static class ConnectionWaiter {
CountDownLatch latch
public void onConnectionEvent(ConnectionEvent e) {
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
latch.countDown()
}
}
static class DownloadListener {
public void onDownloadStartedEvent(DownloadStartedEvent e) {
downloaders.add(e.downloader)
}
}
}

View File

@@ -90,8 +90,13 @@ public class Core {
def i2pOptionsFile = new File(home,"i2p.properties") def i2pOptionsFile = new File(home,"i2p.properties")
if (i2pOptionsFile.exists()) { if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) } i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.hasProperty("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.hasProperty("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else { } else {
i2pOptions["inbound.nickname"] = "MuWire" i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3" i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2" i2pOptions["inbound.quantity"] = "2"
i2pOptions["outbound.length"] = "3" i2pOptions["outbound.length"] = "3"
@@ -186,8 +191,10 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager) eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager") log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me) DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager) eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
log.info("initializing upload manager") log.info("initializing upload manager")
UploadManager uploadManager = new UploadManager(eventBus, fileManager) UploadManager uploadManager = new UploadManager(eventBus, fileManager)
@@ -248,7 +255,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.0.12") Core core = new Core(props, home, "0.1.4")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -24,7 +24,7 @@ class EventBus {
} }
private void publishInternal(Event e) { private void publishInternal(Event e) {
log.fine "publishing event $e of type ${e.getClass().getSimpleName()}" log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
def currentHandlers def currentHandlers
final def clazz = e.getClass() final def clazz = e.getClass()
synchronized(this) { synchronized(this) {

View File

@@ -0,0 +1,4 @@
package com.muwire.core
class UILoadedEvent extends Event {
}

View File

@@ -127,6 +127,7 @@ abstract class Connection implements Closeable {
query.uuid = e.searchEvent.getUuid() query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash
if (e.searchEvent.searchHash != null) if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash) query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64() query.replyTo = e.replyTo.toBase64()
@@ -157,8 +158,11 @@ abstract class Connection implements Closeable {
protected void handleSearch(def search) { protected void handleSearch(def search) {
UUID uuid = UUID.fromString(search.uuid) UUID uuid = UUID.fromString(search.uuid)
if (search.infohash != null) byte [] infohash = null
if (search.infohash != null) {
search.keywords = null search.keywords = null
infohash = Base64.decode(search.infohash)
}
Destination replyTo = new Destination(search.replyTo) Destination replyTo = new Destination(search.replyTo)
TrustLevel trustLevel = trustService.getLevel(replyTo) TrustLevel trustLevel = trustService.getLevel(replyTo)
@@ -180,10 +184,14 @@ abstract class Connection implements Closeable {
} }
} }
boolean oob = false
if (search.oobInfohash != null)
oob = search.oobInfohash
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : Base64.decode(search.infohash), searchHash : infohash,
uuid : uuid) uuid : uuid,
oobInfohash : oob)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,

View File

@@ -144,7 +144,7 @@ class ConnectionAcceptor {
private void handleIncoming(Endpoint e, boolean leaf) { private void handleIncoming(Endpoint e, boolean leaf) {
boolean accept = !manager.isConnected(e.destination) && boolean accept = !manager.isConnected(e.destination) &&
!establisher.inProgress.contains(e.destination) && !establisher.isInProgress(e.destination) &&
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots()) (leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
if (accept) { if (accept) {
log.info("accepting connection, leaf:$leaf") log.info("accepting connection, leaf:$leaf")

View File

@@ -35,6 +35,8 @@ class ConnectionEstablisher {
final Set inProgress = new ConcurrentHashSet() final Set inProgress = new ConcurrentHashSet()
ConnectionEstablisher(){}
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings, ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
ConnectionManager connectionManager, HostCache hostCache) { ConnectionManager connectionManager, HostCache hostCache) {
this.eventBus = eventBus this.eventBus = eventBus
@@ -176,4 +178,8 @@ class ConnectionEstablisher {
e.close() e.close()
} }
} }
public boolean isInProgress(Destination d) {
inProgress.contains(d)
}
} }

View File

@@ -1,12 +1,21 @@
package com.muwire.core.download package com.muwire.core.download
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -16,13 +25,16 @@ 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 File incompletes, home
private final Persona me private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) { private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.connector = connector this.connector = connector
this.incompletes = incompletes this.incompletes = new File(home,"incompletes")
this.home = home
this.me = me this.me = me
incompletes.mkdir() incompletes.mkdir()
@@ -50,6 +62,8 @@ public class DownloadManager {
def downloader = new Downloader(eventBus, this, me, e.target, size, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,
incompletes) incompletes)
downloaders.add(downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable) executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} }
@@ -57,4 +71,53 @@ public class DownloadManager {
void resume(Downloader downloader) { void resume(Downloader downloader) {
executor.execute({downloader.download() as Runnable}) executor.execute({downloader.download() as Runnable})
} }
void onUILoadedEvent(UILoadedEvent e) {
File downloadsFile = new File(home, "downloads.json")
if (!downloadsFile.exists())
return
def slurper = new JsonSlurper()
downloadsFile.eachLine {
def json = slurper.parseText(it)
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
def destinations = new HashSet<>()
json.destinations.each { destination ->
destinations.add new Destination(destination)
}
byte[] hashList = Base64.decode(json.hashList)
InfoHash infoHash = InfoHash.fromHashList(hashList)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
downloaders.remove(e.downloader)
persistDownloaders()
}
private void persistDownloaders() {
File downloadsFile = new File(home,"downloads.json")
downloadsFile.withPrintWriter { writer ->
downloaders.each { downloader ->
if (!downloader.cancelled) {
def json = [:]
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
json.length = downloader.length
json.pieceSizePow2 = downloader.pieceSizePow2
def destinations = []
downloader.destinations.each {
destinations << it.toBase64()
}
json.destinations = destinations
json.hashList = Base64.encode(downloader.infoHash.hashList)
writer.println(JsonOutput.toJson(json))
}
}
}
}
} }

View File

@@ -42,6 +42,7 @@ public class Downloader {
private final Set<Destination> destinations private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File piecesFile private final File piecesFile
final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>() private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
@@ -61,6 +62,7 @@ public class Downloader {
this.connector = connector this.connector = connector
this.destinations = destinations this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
int nPieces int nPieces
@@ -88,8 +90,8 @@ public class Downloader {
void readPieces() { void readPieces() {
if (!piecesFile.exists()) if (!piecesFile.exists())
return return
piecesFile.withReader { piecesFile.eachLine {
int piece = Integer.parseInt(it.readLine()) int piece = Integer.parseInt(it)
downloaded.markDownloaded(piece) downloaded.markDownloaded(piece)
} }
} }
@@ -163,11 +165,18 @@ public class Downloader {
} }
public void resume() { public void resume() {
activeWorkers.each { destination, worker -> destinations.each { destination ->
if (worker.currentState == WorkerState.FINISHED) { def worker = activeWorkers.get(destination)
def newWorker = new DownloadWorker(destination) if (worker != null) {
activeWorkers.put(destination, newWorker) if (worker.currentState == WorkerState.FINISHED) {
executorService.submit(newWorker) def newWorker = new DownloadWorker(destination)
activeWorkers.put(destination, newWorker)
executorService.submit(newWorker)
}
} else {
worker = new DownloadWorker(destination)
activeWorkers.put(destination, worker)
executorService.submit(worker)
} }
} }
} }
@@ -205,7 +214,8 @@ public class Downloader {
if (downloaded.isComplete() && !eventFired) { if (downloaded.isComplete() && !eventFired) {
piecesFile.delete() piecesFile.delete()
eventFired = true eventFired = true
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, Collections.emptySet()))) eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, pieceSize, Collections.emptySet())),
downloader : Downloader.this)
} }
endpoint?.close() endpoint?.close()
} }

View File

@@ -2,10 +2,11 @@ package com.muwire.core.files
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.download.Downloader
import net.i2p.data.Destination import net.i2p.data.Destination
class FileDownloadedEvent extends Event { class FileDownloadedEvent extends Event {
Downloader downloader
DownloadedFile downloadedFile DownloadedFile downloadedFile
} }

View File

@@ -1,6 +1,9 @@
package com.muwire.core.files package com.muwire.core.files
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import net.i2p.data.Base64
import java.nio.MappedByteBuffer import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.channels.FileChannel.MapMode import java.nio.channels.FileChannel.MapMode
@@ -17,12 +20,12 @@ class FileHasher {
* @return the size of each piece in power of 2 * @return the size of each piece in power of 2
*/ */
static int getPieceSize(long size) { static int getPieceSize(long size) {
if (size <= 0x1 << 25) if (size <= 0x1 << 27)
return 18 return 17
for (int i = 26; i <= 37; i++) { for (int i = 28; i <= 37; i++) {
if (size <= 0x1L << i) { if (size <= 0x1L << i) {
return i-7 return i-10
} }
} }
@@ -67,4 +70,18 @@ class FileHasher {
byte [] hashList = output.toByteArray() byte [] hashList = output.toByteArray()
InfoHash.fromHashList(hashList) InfoHash.fromHashList(hashList)
} }
public static void main(String[] args) {
if (args.length != 1) {
println "This utility computes an infohash of a file"
println "Pass absolute path to a file as an argument"
System.exit(1)
}
def file = new File(args[0])
file = file.getAbsoluteFile()
def hasher = new FileHasher()
def infohash = hasher.hashFile(file)
println Base64.encode(infohash.getRoot())
}
} }

View File

@@ -4,6 +4,7 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchIndex import com.muwire.core.search.SearchIndex
@@ -26,20 +27,20 @@ class FileManager {
this.eventBus = eventBus this.eventBus = eventBus
} }
void onFileHashedEvent(FileHashedEvent e) { void onFileHashedEvent(FileHashedEvent e) {
if (settings.shareDownloadedFiles) { if (e.sharedFile != null)
if (e.sharedFile != null) addToIndex(e.sharedFile)
addToIndex(e.sharedFile) }
}
}
void onFileLoadedEvent(FileLoadedEvent e) { void onFileLoadedEvent(FileLoadedEvent e) {
addToIndex(e.loadedFile) addToIndex(e.loadedFile)
} }
void onFileDownloadedEvent(FileDownloadedEvent e) { void onFileDownloadedEvent(FileDownloadedEvent e) {
addToIndex(e.downloadedFile) if (settings.shareDownloadedFiles) {
} addToIndex(e.downloadedFile)
}
}
private void addToIndex(SharedFile sf) { private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile()) log.info("Adding shared file " + sf.getFile())
@@ -105,6 +106,7 @@ class FileManager {
if (e.searchHash != null) { if (e.searchHash != null) {
Set<SharedFile> found Set<SharedFile> found
found = rootToFiles.get new InfoHash(e.searchHash) found = rootToFiles.get new InfoHash(e.searchHash)
found = filter(found, e.oobInfohash)
if (found != null && !found.isEmpty()) if (found != null && !found.isEmpty())
re = new ResultsEvent(results: found.asList(), uuid: e.uuid) re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
} else { } else {
@@ -113,6 +115,7 @@ class FileManager {
names.each { files.addAll nameToFiles.getOrDefault(it, []) } names.each { files.addAll nameToFiles.getOrDefault(it, []) }
Set<SharedFile> sharedFiles = new HashSet<>() Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] } files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty()) if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid) re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
@@ -121,4 +124,15 @@ class FileManager {
if (re != null) if (re != null)
eventBus.publish(re) eventBus.publish(re)
} }
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
if (!oob)
return files
Set<SharedFile> rv = new HashSet<>()
files.each {
if (it.getPieceSize() != 0)
rv.add(it)
}
rv
}
} }

View File

@@ -40,7 +40,7 @@ class HasherService {
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}") eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
} else { } else {
def hash = hasher.hashFile f def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash)) eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
} }
} }
} }

View File

@@ -98,14 +98,19 @@ class PersisterService extends Service {
if (!Arrays.equals(root, ih.getRoot())) if (!Arrays.equals(root, ih.getRoot()))
return null return null
if (json.sources != null) { int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet() Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, sourceSet) DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
return new FileLoadedEvent(loadedFile : df) return new FileLoadedEvent(loadedFile : df)
} }
SharedFile sf = new SharedFile(file, ih) SharedFile sf = new SharedFile(file, ih, pieceSize)
return new FileLoadedEvent(loadedFile: sf) return new FileLoadedEvent(loadedFile: sf)
} }
@@ -132,6 +137,7 @@ class PersisterService extends Service {
json.length = f.length() json.length = f.length()
InfoHash ih = sf.getInfoHash() InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot() json.infoHash = Base64.encode ih.getRoot()
json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32] byte [] tmp = new byte [32]
json.hashList = [] json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) { for (int i = 0;i < ih.getHashList().length / 32; i++) {

View File

@@ -13,4 +13,8 @@ class QueryEvent extends Event {
Persona originator Persona originator
Destination receivedOn Destination receivedOn
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
"originator: ${originator.getHumanReadableName()} receivedOn: ${receivedOn.toBase32()}"
}
} }

View File

@@ -51,11 +51,14 @@ class ResultsSender {
if (target.equals(me.destination)) { if (target.equals(me.destination)) {
results.each { results.each {
long length = it.getFile().length() long length = it.getFile().length()
int pieceSize = it.getPieceSize()
if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length)
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : FileHasher.getPieceSize(length), pieceSize : pieceSize,
uuid : uuid uuid : uuid
) )
eventBus.publish(uiResultEvent) eventBus.publish(uiResultEvent)
@@ -95,7 +98,7 @@ class ResultsSender {
obj.name = encodedName obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot()) obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length() obj.size = it.getFile().length()
obj.pieceSize = FileHasher.getPieceSize(it.getFile().length()) obj.pieceSize = it.getPieceSize()
byte [] hashList = it.getInfoHash().getHashList() byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = [] def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) { for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {

View File

@@ -1,10 +1,19 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.InfoHash
class SearchEvent extends Event { class SearchEvent extends Event {
List<String> searchTerms List<String> searchTerms
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
boolean oobInfohash
String toString() {
def infoHash = null
if (searchHash != null)
infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
}
} }

View File

@@ -42,7 +42,7 @@ class SearchIndex {
terms.each { terms.each {
Set<String> forWord = keywords.getOrDefault(it,[]) Set<String> forWord = keywords.getOrDefault(it,[])
if (rv == null) { if (rv == null) {
rv = forWord rv = new HashSet<>(forWord)
} else { } else {
rv.retainAll(forWord) rv.retainAll(forWord)
} }

View File

@@ -9,8 +9,8 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources; private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, Set<Destination> sources) { public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
super(file, infoHash); super(file, infoHash, pieceSize);
this.sources = sources; this.sources = sources;
} }

View File

@@ -7,6 +7,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import net.i2p.data.Base32; import net.i2p.data.Base32;
import net.i2p.data.Base64;
public class InfoHash { public class InfoHash {
@@ -76,14 +77,16 @@ public class InfoHash {
} }
public String toString() { public String toString() {
String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:"; String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
List<String> b32HashList = new ArrayList<>(hashList.length / SIZE); List<String> b64HashList = new ArrayList<>();
byte [] tmp = new byte[SIZE]; if (hashList != null) {
for (int i = 0; i < hashList.length / SIZE; i++) { byte [] tmp = new byte[SIZE];
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE); for (int i = 0; i < hashList.length / SIZE; i++) {
b32HashList.add(Base32.encode(tmp)); System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
b64HashList.add(Base64.encode(tmp));
}
} }
rv += b32HashList.toString(); rv += b64HashList.toString();
rv += "]"; rv += "]";
return rv; return rv;
} }

View File

@@ -6,10 +6,12 @@ public class SharedFile {
private final File file; private final File file;
private final InfoHash infoHash; private final InfoHash infoHash;
private final int pieceSize;
public SharedFile(File file, InfoHash infoHash) { public SharedFile(File file, InfoHash infoHash, int pieceSize) {
this.file = file; this.file = file;
this.infoHash = infoHash; this.infoHash = infoHash;
this.pieceSize = pieceSize;
} }
public File getFile() { public File getFile() {
@@ -20,4 +22,7 @@ public class SharedFile {
return infoHash; return infoHash;
} }
public int getPieceSize() {
return pieceSize;
}
} }

View File

@@ -43,6 +43,9 @@ class ConnectionAcceptorTest {
def uploadManagerMock def uploadManagerMock
UploadManager uploadManager UploadManager uploadManager
def connectionEstablisherMock
ConnectionEstablisher connectionEstablisher
ConnectionAcceptor acceptor ConnectionAcceptor acceptor
List<ConnectionEvent> connectionEvents List<ConnectionEvent> connectionEvents
@@ -57,6 +60,7 @@ class ConnectionAcceptorTest {
trustServiceMock = new MockFor(TrustService.class) trustServiceMock = new MockFor(TrustService.class)
searchManagerMock = new MockFor(SearchManager.class) searchManagerMock = new MockFor(SearchManager.class)
uploadManagerMock = new MockFor(UploadManager.class) uploadManagerMock = new MockFor(UploadManager.class)
connectionEstablisherMock = new MockFor(ConnectionEstablisher.class)
} }
@After @After
@@ -68,6 +72,7 @@ class ConnectionAcceptorTest {
trustServiceMock.verify trustService trustServiceMock.verify trustService
searchManagerMock.verify searchManager searchManagerMock.verify searchManager
uploadManagerMock.verify uploadManager uploadManagerMock.verify uploadManager
connectionEstablisherMock.verify connectionEstablisher
Thread.sleep(100) Thread.sleep(100)
} }
@@ -87,8 +92,10 @@ class ConnectionAcceptorTest {
trustService = trustServiceMock.proxyInstance() trustService = trustServiceMock.proxyInstance()
searchManager = searchManagerMock.proxyInstance() searchManager = searchManagerMock.proxyInstance()
uploadManager = uploadManagerMock.proxyInstance() uploadManager = uploadManagerMock.proxyInstance()
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor, hostCache, trustService, searchManager, uploadManager) acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
acceptor.start() acceptor.start()
Thread.sleep(100) Thread.sleep(100)
} }
@@ -108,6 +115,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null) new Endpoint(destinations.dest1, is, os, null)
} }
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) } i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest -> connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1 assert dest == destinations.dest1
false false
@@ -150,6 +158,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null) new Endpoint(destinations.dest1, is, os, null)
} }
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) } i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest -> connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1 assert dest == destinations.dest1
false false
@@ -264,6 +273,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null) new Endpoint(destinations.dest1, is, os, null)
} }
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) } i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest -> connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1 assert dest == destinations.dest1
false false
@@ -310,6 +320,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null) new Endpoint(destinations.dest1, is, os, null)
} }
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) } i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest -> connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1 assert dest == destinations.dest1
false false
@@ -356,6 +367,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null) new Endpoint(destinations.dest1, is, os, null)
} }
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) } i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest -> connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1 assert dest == destinations.dest1
false false

View File

@@ -24,9 +24,9 @@ class FileHasherTest extends GroovyTestCase {
@Test @Test
void testPieceSize() { void testPieceSize() {
assert 18 == FileHasher.getPieceSize(1000000) assert 17 == FileHasher.getPieceSize(1000000)
assert 20 == FileHasher.getPieceSize(100000000) assert 17 == FileHasher.getPieceSize(100000000)
assert 30 == FileHasher.getPieceSize(FileHasher.MAX_SIZE) assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, { shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE) FileHasher.getPieceSize(Long.MAX_VALUE)
} }
@@ -48,7 +48,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b fos.write b
fos.close() fos.close()
def ih = hasher.hashFile tmp def ih = hasher.hashFile tmp
assert ih.getHashList().length == 32 assert ih.getHashList().length == 64
} }
@Test @Test
@@ -58,7 +58,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b fos.write b
fos.close() fos.close()
def ih = hasher.hashFile tmp def ih = hasher.hashFile tmp
assert ih.getHashList().length == 64 assert ih.getHashList().length == 96
} }
@Test @Test
@@ -68,7 +68,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b fos.write b
fos.close() fos.close()
def ih = hasher.hashFile tmp def ih = hasher.hashFile tmp
assert ih.getHashList().length == 64 assert ih.getHashList().length == 128
} }
@Test @Test
@@ -78,6 +78,6 @@ class FileHasherTest extends GroovyTestCase {
fos.write b fos.write b
fos.close() fos.close()
def ih = hasher.hashFile tmp def ih = hasher.hashFile tmp
assert ih.getHashList().length == 32 * 3 assert ih.getHashList().length == 160
} }
} }

View File

@@ -5,6 +5,7 @@ import org.junit.Test
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
@@ -26,7 +27,7 @@ class FileManagerTest {
void before() { void before() {
eventBus = new EventBus() eventBus = new EventBus()
eventBus.register(ResultsEvent.class, listener) eventBus.register(ResultsEvent.class, listener)
manager = new FileManager(eventBus) manager = new FileManager(eventBus, new MuWireSettings())
results = null results = null
} }
@@ -34,7 +35,7 @@ class FileManagerTest {
void testHash1Result() { void testHash1Result() {
File f = new File("a b.c") File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih) SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf) FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe) manager.onFileHashedEvent(fhe)
@@ -53,8 +54,8 @@ class FileManagerTest {
@Test @Test
void testHash2Results() { void testHash2Results() {
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih) SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih) SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@@ -75,7 +76,7 @@ class FileManagerTest {
void testHash0Results() { void testHash0Results() {
File f = new File("a b.c") File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih) SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf) FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe) manager.onFileHashedEvent(fhe)
@@ -89,7 +90,7 @@ class FileManagerTest {
void testKeyword1Result() { void testKeyword1Result() {
File f = new File("a b.c") File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih) SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf) FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe) manager.onFileHashedEvent(fhe)
@@ -107,12 +108,12 @@ class FileManagerTest {
void testKeyword2Results() { void testKeyword2Results() {
File f1 = new File("a b.c") File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32]) InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1) SharedFile sf1 = new SharedFile(f1, ih1, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e") File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64]) InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2) SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
UUID uuid = UUID.randomUUID() UUID uuid = UUID.randomUUID()
@@ -130,7 +131,7 @@ class FileManagerTest {
void testKeyword0Results() { void testKeyword0Results() {
File f = new File("a b.c") File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih) SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf) FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe) manager.onFileHashedEvent(fhe)
@@ -143,8 +144,8 @@ class FileManagerTest {
@Test @Test
void testRemoveFileExistingHash() { void testRemoveFileExistingHash() {
InfoHash ih = InfoHash.fromHashList(new byte[32]) InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih) SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih) SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@@ -161,12 +162,12 @@ class FileManagerTest {
void testRemoveFile() { void testRemoveFile() {
File f1 = new File("a b.c") File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32]) InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1) SharedFile sf1 = new SharedFile(f1, ih1, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e") File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64]) InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2) SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2) manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2) manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)

View File

@@ -8,6 +8,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
class HasherServiceTest { class HasherServiceTest {
@@ -24,7 +25,7 @@ class HasherServiceTest {
void before() { void before() {
eventBus = new EventBus() eventBus = new EventBus()
hasher = new FileHasher() hasher = new FileHasher()
service = new HasherService(hasher, eventBus) service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
eventBus.register(FileHashedEvent.class, listener) eventBus.register(FileHashedEvent.class, listener)
service.start() service.start()
} }

View File

@@ -99,7 +99,7 @@ class PersisterServiceLoadingTest {
FileHasher fh = new FileHasher() FileHasher fh = new FileHasher()
InfoHash ih1 = fh.hashFile(sharedFile1) InfoHash ih1 = fh.hashFile(sharedFile1)
assert ih1.getHashList().length == 2 * 32 assert ih1.getHashList().length == 96
def json = [:] def json = [:]
json.file = getSharedFileJsonName(sharedFile1) json.file = getSharedFileJsonName(sharedFile1)
@@ -111,7 +111,9 @@ class PersisterServiceLoadingTest {
String hash1 = Base64.encode(tmp) String hash1 = Base64.encode(tmp)
System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32) System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32)
String hash2 = Base64.encode(tmp) String hash2 = Base64.encode(tmp)
json.hashList = [hash1, hash2] System.arraycopy(ih1.getHashList(), 64, tmp, 0, 32)
String hash3 = Base64.encode(tmp)
json.hashList = [hash1, hash2, hash3]
json = JsonOutput.toJson(json) json = JsonOutput.toJson(json)

View File

@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@@ -31,7 +32,7 @@ class PersisterServiceSavingTest {
f = new File("build.gradle") f = new File("build.gradle")
f = f.getCanonicalFile() f = f.getCanonicalFile()
ih = fh.hashFile(f) ih = fh.hashFile(f)
fileSource = new FileManager(eventBus) { fileSource = new FileManager(eventBus, new MuWireSettings()) {
Map<File, SharedFile> getSharedFiles() { Map<File, SharedFile> getSharedFiles() {
Map<File, SharedFile> rv = new HashMap<>() Map<File, SharedFile> rv = new HashMap<>()
rv.put(f, sf) rv.put(f, sf)
@@ -54,7 +55,7 @@ class PersisterServiceSavingTest {
@Test @Test
void testSavingSharedFile() { void testSavingSharedFile() {
sf = new SharedFile(f, ih) sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start() ps.start()
@@ -73,7 +74,7 @@ class PersisterServiceSavingTest {
@Test @Test
void testSavingDownloadedFile() { void testSavingDownloadedFile() {
Destinations dests = new Destinations() Destinations dests = new Destinations()
sf = new DownloadedFile(f, ih, new HashSet([dests.dest1, dests.dest2])) sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start() ps.start()

View File

@@ -30,7 +30,18 @@ class SearchIndexTest {
assert found.size() == 2 assert found.size() == 2
assert found.contains("a b.c") assert found.contains("a b.c")
assert found.contains("c d.e") assert found.contains("c d.e")
} }
@Test
public void testDrillDownDoesNotModifyIndex() {
initIndex(["a b.c", "c d.e"])
index.search(["c","e"])
def found = index.search(["c"])
assert found.size() == 2
assert found.contains("a b.c")
assert found.contains("c d.e")
}
@Test @Test
void testDrillDown() { void testDrillDown() {

37
doc/infohash-upgrade.md Normal file
View File

@@ -0,0 +1,37 @@
# InfoHash Upgrade
An infohash is a list of hashes of the pieces of the file. In MuWire 0.1.0 the piece size is determined by policy based on the file size, with the intention being to keep the list of hashes to maximum 128 in number. The reason for this is that infohashes get returned with search results, and smaller piece size will result in larger infohash which will slow down the transmission of search results.
### The problem
This presents the following problem - larger files have larger piece sizes: a 2GB file will have a 16MB piece size, a 4GB file 32MB and so on. Pieces are atomic, i.e. if a download fails halfway through a piece it will resume since the beginning of the piece. Unfortunately in the current state of I2P the failure rate of streaming connections is too high and transmitting an entire piece over a single connection is not likely to succeed as the size of the piece increases. This makes downloading multi-gigabyte files nearly impossible.
### Out-of-band request proposal
Barring any improvement to the reliability of I2P connections, the following approach can be used to enable smaller piece sizes and the corresponding increase in download success rate of large files:
* Search results do carry the full infohash of the file, instead they carry just the root and the number of 32-byte hashes in the infohash
* When a downloader begins a download, it issues a request for the full infohash first. Only after that is fetched and verified, the download proceeds as usual.
Such approach is more complicated in nature than the current simplistic one, but it has the additional benefit of reducing the size of search results.
### Wire protocol changes
A new request method - "HASHLIST" is introduced. It is immediately followed by the Base64 encoded root of the infohash and '\r\n'. The request may contain HTTP headers in the same format as in HTTP 1.1. One such header may be the X-Persona header, but that is optional. After all the headers a blank line with just '\r\n' is sent.
The response is identical to that of regular GET request, with the same response codes. The response may also carry headers, and is also followed by a blank line with '\r\n'. What follows immediately after that is a binary representation of the hashlist. After sending the full hashlist, the uploader keeps the connection open in anticipation of the first content GET request.
The downloader verifies the hashlist by hashing it with SHA256 and comparing it to the advertised infohash root. If there is a match, it proceeds with the rest of the download as in MuWire 0.1.0.
### Necessary changes to MuWire 0.1.0
To accommodate this proposal in a backwards compatible manner, it is necessary to first de-hardcode the piece count computation logic which is currently hardcoded in a few places. Then it is necessary to:
* persist the piece size to disk when a file is being shared so that it can be returned in search results
* search queries need to carry a flag of some kind that indicates support for out-of-band infohash support
* that in turn requires nodes to support passing of that flag as the queries are being routed through the network
* the returned results need to indicate whether they are returning a full infohash or just a root; the "version" field in the json can be used for that
### Roadmap
Support for this proposal is currently intended for MuWire 0.2.0. However, in order to make rollout smooth, in MuWire 0.1.1 support for the first two items will be introduced. Since there already are users on the network who have shared files without persisting the size of their pieces on disk, those files will not be eligible to participate in this scheme unless re-shared (which implies re-hashing).

View File

@@ -131,12 +131,18 @@ Sent by a leaf or ultrapeer when performing a search. Contains the reply-to per
firstHop: false, firstHop: false,
keywords : ["keyword1","keyword2"...] keywords : ["keyword1","keyword2"...]
infohash: "asdfasdf...", infohash: "asdfasdf...",
replyTo : "asdfasf...b64" replyTo : "asdfasf...b64",
originator : "asfasdf...",
"oobHashlist" : true
} }
``` ```
A search can contain either the query entered by the user in the UI or the infohash if the user is looking for a specific file. If both are present, the infohash takes precedence and the keyword query is ignored. A search can contain either the query entered by the user in the UI or the infohash if the user is looking for a specific file. If both are present, the infohash takes precedence and the keyword query is ignored.
The "originator" field contains the Base64-encoded persona of the originator of the query. It is used for display purposes only. The I2P destination in that persona must match the one in the "replyTo" field.
The oobHashlist flag indicates support for out-of-band hashlist delivery, which is not yet implemented. Nevertheless, this flag gets propagated through the network for future-proofing.
### Ultrapeer to leaf ### Ultrapeer to leaf
The "Search" message is also sent from an ultrapeer to a leaf. The "Search" message is also sent from an ultrapeer to a leaf.

View File

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

View File

@@ -50,8 +50,9 @@ class MainFrameController {
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid) searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
} else { } else {
// this can be improved a lot // this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN) def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid) def terms = replaced.split(" ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: false)
} }
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,
@@ -81,12 +82,19 @@ class MainFrameController {
int row = table.getSelectedRow() int row = table.getSelectedRow()
if (row == -1) if (row == -1)
return return
def sortEvt = group.view.lastSortEvent
if (sortEvt != null) {
row = sortEvt.convertPreviousRowIndexToModel(row)
}
group.model.results[row] group.model.results[row]
} }
private def selectedDownload() { private int selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow() def selected = builder.getVariable("downloads-table").getSelectedRow()
model.downloads[selected].downloader def sortEvt = mvcGroup.view.lastDownloadSortEvent
if (sortEvt != null)
selected = sortEvt.convertPreviousRowIndexToModel(selected)
selected
} }
@ControllerAction @ControllerAction
@@ -123,13 +131,13 @@ class MainFrameController {
@ControllerAction @ControllerAction
void cancel() { void cancel() {
def downloader = selectedDownload() def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel() downloader.cancel()
} }
@ControllerAction @ControllerAction
void resume() { void resume() {
def downloader = selectedDownload() def downloader = model.downloads[selectedDownload()].downloader
downloader.resume() downloader.resume()
} }

View File

@@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JTable
import static griffon.util.GriffonApplicationUtils.isMacOSX import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel import static groovy.swing.SwingBuilder.lookAndFeel

View File

@@ -1,11 +1,13 @@
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.env.Metadata import griffon.core.env.Metadata
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import javax.annotation.Nonnull import javax.annotation.Nonnull
@@ -34,13 +36,13 @@ class Ready extends AbstractLifecycleHandler {
log.info "starting core services" log.info "starting core services"
def portableHome = System.getProperty("portable.home") def portableHome = System.getProperty("portable.home")
def home = portableHome == null ? def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" : selectHome() :
portableHome 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 $home")
home.mkdir() home.mkdirs()
} }
def props = new Properties() def props = new Properties()
@@ -116,6 +118,28 @@ class Ready extends AbstractLifecycleHandler {
core.eventBus.publish(new FileSharedEvent(file : new File(it))) core.eventBus.publish(new FileSharedEvent(file : new File(it)))
} }
} }
core.eventBus.publish(new UILoadedEvent())
}
private static String selectHome() {
def home = new File(System.properties["user.home"])
def defaultHome = new File(home, ".MuWire")
if (defaultHome.exists())
return defaultHome.getAbsolutePath()
if (SystemVersion.isMac()) {
def library = new File(home, "Library")
def appSupport = new File(library, "Application Support")
def muwire = new File(appSupport,"MuWire")
return muwire.getAbsolutePath()
}
if (SystemVersion.isWindows()) {
def appData = new File(home,"AppData")
def roaming = new File(appData, "Roaming")
def muwire = new File(roaming, "MuWire")
return muwire.getAbsolutePath()
}
defaultHome.getAbsolutePath()
} }
} }

View File

@@ -37,6 +37,9 @@ class MainFrameView {
@MVCMember @Nonnull @MVCMember @Nonnull
MainFrameModel model MainFrameModel model
def downloadsTable
def lastDownloadSortEvent
void initUI() { void initUI() {
builder.with { builder.with {
application(size : [1024,768], id: 'main-frame', application(size : [1024,768], id: 'main-frame',
@@ -105,10 +108,10 @@ class MainFrameView {
panel (constraints : JSplitPane.BOTTOM) { panel (constraints : JSplitPane.BOTTOM) {
borderLayout() borderLayout()
scrollPane (constraints : BorderLayout.CENTER) { scrollPane (constraints : BorderLayout.CENTER) {
table(id : "downloads-table") { downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
tableModel(list: model.downloads) { tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()}) closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState()}) closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
closureColumn(header: "Progress", preferredWidth: 20, 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()
@@ -136,7 +139,7 @@ class MainFrameView {
button(text : "Click here to share 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", autoCreateRowSorter: true) {
tableModel(list : model.shared) { tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()}) closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : String, closureColumn(header : "Size", preferredWidth : 50, type : String,
@@ -213,7 +216,7 @@ class MainFrameView {
panel (border : etchedBorder()){ panel (border : etchedBorder()){
borderLayout() borderLayout()
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") { table(id : "trusted-table", autoCreateRowSorter : true) {
tableModel(list : model.trusted) { tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } ) closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
} }
@@ -228,7 +231,7 @@ class MainFrameView {
panel (border : etchedBorder()){ panel (border : etchedBorder()){
borderLayout() borderLayout()
scrollPane(constraints : BorderLayout.CENTER) { scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") { table(id : "distrusted-table", autoCreateRowSorter : true) {
tableModel(list : model.distrusted) { tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } ) closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
} }
@@ -260,7 +263,7 @@ class MainFrameView {
def selectionModel = downloadsTable.getSelectionModel() def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({ selectionModel.addListSelectionListener({
int selectedRow = downloadsTable.getSelectedRow() int selectedRow = selectedDownloaderRow()
def downloader = model.downloads[selectedRow].downloader def downloader = model.downloads[selectedRow].downloader
switch(downloader.getCurrentState()) { switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING : case Downloader.DownloadState.CONNECTING :
@@ -280,9 +283,18 @@ class MainFrameView {
def centerRenderer = new DefaultTableCellRenderer() def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER) centerRenderer.setHorizontalAlignment(JLabel.CENTER)
builder.getVariable("downloads-table").setDefaultRenderer(Integer.class, centerRenderer) downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
} }
int selectedDownloaderRow() {
int selected = builder.getVariable("downloads-table").getSelectedRow()
if (lastDownloadSortEvent != null)
selected = lastDownloadSortEvent.convertPreviousRowIndexToModel(selected)
selected
}
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")

View File

@@ -53,7 +53,7 @@ class OptionsView {
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, 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)) label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
label(text : "Only allow trusted connections", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 2))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 2)) allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3)) label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))

View File

@@ -26,19 +26,20 @@ class SearchTabView {
def parent def parent
def searchTerms def searchTerms
def resultsTable def resultsTable
def lastSortEvent
void initUI() { void initUI() {
builder.with { builder.with {
def resultsTable def resultsTable
def pane = scrollPane { def pane = scrollPane {
resultsTable = table(id : "results-table") { resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) { tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')}) 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: "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: "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: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row -> closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination) model.core.trustService.getLevel(row.sender.destination).toString()
}) })
} }
} }
@@ -84,6 +85,8 @@ class SearchTabView {
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer) resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer) resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer) resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
} }
def closeTab = { def closeTab = {