Compare commits
47 Commits
muwire-0.0
...
muwire-0.1
Author | SHA1 | Date | |
---|---|---|---|
bd21cf65ea | |||
dea592eb27 | |||
c81f963e0a | |||
dc6b1199f3 | |||
42621a2dfb | |||
a7125963a7 | |||
f39d7f4fa8 | |||
b88334f19a | |||
81e186ad1f | |||
33a45c3835 | |||
32b7867e44 | |||
5b313276f4 | |||
abba4cc6fa | |||
15b4804968 | |||
942a01a501 | |||
502a8d91da | |||
5414e8679b | |||
14e42dd7c2 | |||
1299fb2512 | |||
9bafdfe0b1 | |||
36eb632756 | |||
83ee620402 | |||
3fe40d317d | |||
e9703a2652 | |||
a3fe89851f | |||
b9ea0128cd | |||
53c6db4ec8 | |||
60776829b9 | |||
b5cb31c23d | |||
5052c0c993 | |||
06de007866 | |||
7c8a0c9ad9 | |||
cda81a89a2 | |||
483773422c | |||
1e1e6d0bb0 | |||
668d6e087d | |||
49af412b96 | |||
d5513021ed | |||
c3154cf717 | |||
114940c4c1 | |||
d4336e9b5d | |||
2c1d5508ed | |||
1cebf6c7bd | |||
e12924a207 | |||
f3b11895e4 | |||
1e084820fb | |||
2198b4846d |
11
README.md
11
README.md
@ -1,10 +1,10 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
@ -23,15 +23,12 @@ Some of the UI tests will fail because they haven't been written yet :-/
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
### Known bugs and limitations
|
||||
|
||||
* Many UI features you would expect are not there yet
|
||||
|
||||
|
||||
|
@ -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'
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.0.13")
|
||||
core = new Core(props, home, "0.1.5")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
166
cli/src/main/groovy/com/muwire/cli/CliDownloader.groovy
Normal file
166
cli/src/main/groovy/com/muwire/cli/CliDownloader.groovy
Normal 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.5")
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.connection.LeafConnectionManager
|
||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
@ -90,8 +91,13 @@ public class Core {
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
if (!i2pOptions.hasProperty("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.hasProperty("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
} else {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.length"] = "3"
|
||||
i2pOptions["inbound.quantity"] = "2"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
@ -186,8 +192,11 @@ public class Core {
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
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(UILoadedEvent.class, downloadManager)
|
||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||
@ -248,7 +257,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.0.13")
|
||||
Core core = new Core(props, home, "0.1.5")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -0,0 +1,4 @@
|
||||
package com.muwire.core
|
||||
|
||||
class UILoadedEvent extends Event {
|
||||
}
|
@ -127,6 +127,7 @@ abstract class Connection implements Closeable {
|
||||
query.uuid = e.searchEvent.getUuid()
|
||||
query.firstHop = e.firstHop
|
||||
query.keywords = e.searchEvent.getSearchTerms()
|
||||
query.oobInfohash = e.searchEvent.oobInfohash
|
||||
if (e.searchEvent.searchHash != null)
|
||||
query.infohash = Base64.encode(e.searchEvent.searchHash)
|
||||
query.replyTo = e.replyTo.toBase64()
|
||||
@ -183,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,
|
||||
searchHash : infohash,
|
||||
uuid : uuid)
|
||||
uuid : uuid,
|
||||
oobInfohash : oob)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
|
@ -144,7 +144,7 @@ class ConnectionAcceptor {
|
||||
|
||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||
boolean accept = !manager.isConnected(e.destination) &&
|
||||
!establisher.inProgress.contains(e.destination) &&
|
||||
!establisher.isInProgress(e.destination) &&
|
||||
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||
if (accept) {
|
||||
log.info("accepting connection, leaf:$leaf")
|
||||
|
@ -35,6 +35,8 @@ class ConnectionEstablisher {
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
ConnectionEstablisher(){}
|
||||
|
||||
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
||||
ConnectionManager connectionManager, HostCache hostCache) {
|
||||
this.eventBus = eventBus
|
||||
@ -176,4 +178,8 @@ class ConnectionEstablisher {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInProgress(Destination d) {
|
||||
inProgress.contains(d)
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,21 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
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.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
@ -16,13 +25,16 @@ public class DownloadManager {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes
|
||||
private final File incompletes, home
|
||||
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.connector = connector
|
||||
this.incompletes = incompletes
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
this.me = me
|
||||
|
||||
incompletes.mkdir()
|
||||
@ -50,11 +62,67 @@ public class DownloadManager {
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes)
|
||||
downloaders.add(downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||
downloaders.remove(e.downloader)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
void resume(Downloader downloader) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ public class Downloader {
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File piecesFile
|
||||
final int pieceSizePow2
|
||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||
|
||||
|
||||
@ -61,6 +62,7 @@ public class Downloader {
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.pieceSizePow2 = pieceSizePow2
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
|
||||
int nPieces
|
||||
@ -88,8 +90,8 @@ public class Downloader {
|
||||
void readPieces() {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.withReader {
|
||||
int piece = Integer.parseInt(it.readLine())
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
downloaded.markDownloaded(piece)
|
||||
}
|
||||
}
|
||||
@ -163,11 +165,18 @@ public class Downloader {
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
activeWorkers.each { destination, worker ->
|
||||
if (worker.currentState == WorkerState.FINISHED) {
|
||||
def newWorker = new DownloadWorker(destination)
|
||||
activeWorkers.put(destination, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
destinations.each { destination ->
|
||||
def worker = activeWorkers.get(destination)
|
||||
if (worker != null) {
|
||||
if (worker.currentState == WorkerState.FINISHED) {
|
||||
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) {
|
||||
piecesFile.delete()
|
||||
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()
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadCancelledEvent extends Event {
|
||||
Downloader downloader
|
||||
}
|
@ -2,10 +2,11 @@ package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class FileDownloadedEvent extends Event {
|
||||
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import java.nio.MappedByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileChannel.MapMode
|
||||
@ -17,12 +20,12 @@ class FileHasher {
|
||||
* @return the size of each piece in power of 2
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 25)
|
||||
return 18
|
||||
if (size <= 0x1 << 27)
|
||||
return 17
|
||||
|
||||
for (int i = 26; i <= 37; i++) {
|
||||
for (int i = 28; i <= 37; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-7
|
||||
return i-10
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,4 +70,18 @@ class FileHasher {
|
||||
byte [] hashList = output.toByteArray()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchIndex
|
||||
@ -26,20 +27,20 @@ class FileManager {
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (settings.shareDownloadedFiles) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
}
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
addToIndex(e.loadedFile)
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
addToIndex(e.downloadedFile)
|
||||
}
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (settings.shareDownloadedFiles) {
|
||||
addToIndex(e.downloadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
@ -105,6 +106,7 @@ class FileManager {
|
||||
if (e.searchHash != null) {
|
||||
Set<SharedFile> found
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
|
||||
} else {
|
||||
@ -113,6 +115,7 @@ class FileManager {
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
|
||||
|
||||
@ -121,4 +124,15 @@ class FileManager {
|
||||
if (re != null)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class HasherService {
|
||||
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
||||
} else {
|
||||
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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,14 +98,19 @@ class PersisterService extends Service {
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih)
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
@ -132,6 +137,7 @@ class PersisterService extends Service {
|
||||
json.length = f.length()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = Base64.encode ih.getRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
byte [] tmp = new byte [32]
|
||||
json.hashList = []
|
||||
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
||||
|
@ -51,11 +51,14 @@ class ResultsSender {
|
||||
if (target.equals(me.destination)) {
|
||||
results.each {
|
||||
long length = it.getFile().length()
|
||||
int pieceSize = it.getPieceSize()
|
||||
if (pieceSize == 0)
|
||||
pieceSize = FileHasher.getPieceSize(length)
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : FileHasher.getPieceSize(length),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid
|
||||
)
|
||||
eventBus.publish(uiResultEvent)
|
||||
@ -95,7 +98,7 @@ class ResultsSender {
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = FileHasher.getPieceSize(it.getFile().length())
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
byte [] hashList = it.getInfoHash().getHashList()
|
||||
def hashListB64 = []
|
||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||
|
@ -8,11 +8,12 @@ class SearchEvent extends Event {
|
||||
List<String> searchTerms
|
||||
byte [] searchHash
|
||||
UUID uuid
|
||||
boolean oobInfohash
|
||||
|
||||
String toString() {
|
||||
def infoHash = null
|
||||
if (searchHash != null)
|
||||
infoHash = new InfoHash(searchHash)
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid"
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class SearchIndex {
|
||||
terms.each {
|
||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||
if (rv == null) {
|
||||
rv = forWord
|
||||
rv = new HashSet<>(forWord)
|
||||
} else {
|
||||
rv.retainAll(forWord)
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ public class DownloadedFile extends SharedFile {
|
||||
|
||||
private final Set<Destination> sources;
|
||||
|
||||
public DownloadedFile(File file, InfoHash infoHash, Set<Destination> sources) {
|
||||
super(file, infoHash);
|
||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
|
||||
super(file, infoHash, pieceSize);
|
||||
this.sources = sources;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.data.Base32;
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class InfoHash {
|
||||
|
||||
@ -76,13 +77,13 @@ public class InfoHash {
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:";
|
||||
String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
|
||||
List<String> b64HashList = new ArrayList<>();
|
||||
if (hashList != null) {
|
||||
byte [] tmp = new byte[SIZE];
|
||||
for (int i = 0; i < hashList.length / SIZE; i++) {
|
||||
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
|
||||
b64HashList.add(Base32.encode(tmp));
|
||||
b64HashList.add(Base64.encode(tmp));
|
||||
}
|
||||
}
|
||||
rv += b64HashList.toString();
|
||||
|
@ -6,10 +6,12 @@ public class SharedFile {
|
||||
|
||||
private final File file;
|
||||
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.infoHash = infoHash;
|
||||
this.pieceSize = pieceSize;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
@ -20,4 +22,7 @@ public class SharedFile {
|
||||
return infoHash;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
return pieceSize;
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,9 @@ class ConnectionAcceptorTest {
|
||||
|
||||
def uploadManagerMock
|
||||
UploadManager uploadManager
|
||||
|
||||
def connectionEstablisherMock
|
||||
ConnectionEstablisher connectionEstablisher
|
||||
|
||||
ConnectionAcceptor acceptor
|
||||
List<ConnectionEvent> connectionEvents
|
||||
@ -57,6 +60,7 @@ class ConnectionAcceptorTest {
|
||||
trustServiceMock = new MockFor(TrustService.class)
|
||||
searchManagerMock = new MockFor(SearchManager.class)
|
||||
uploadManagerMock = new MockFor(UploadManager.class)
|
||||
connectionEstablisherMock = new MockFor(ConnectionEstablisher.class)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -68,6 +72,7 @@ class ConnectionAcceptorTest {
|
||||
trustServiceMock.verify trustService
|
||||
searchManagerMock.verify searchManager
|
||||
uploadManagerMock.verify uploadManager
|
||||
connectionEstablisherMock.verify connectionEstablisher
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
@ -87,8 +92,10 @@ class ConnectionAcceptorTest {
|
||||
trustService = trustServiceMock.proxyInstance()
|
||||
searchManager = searchManagerMock.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()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
@ -108,6 +115,7 @@ class ConnectionAcceptorTest {
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
@ -150,6 +158,7 @@ class ConnectionAcceptorTest {
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
@ -264,6 +273,7 @@ class ConnectionAcceptorTest {
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
@ -310,6 +320,7 @@ class ConnectionAcceptorTest {
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
@ -356,6 +367,7 @@ class ConnectionAcceptorTest {
|
||||
new Endpoint(destinations.dest1, is, os, null)
|
||||
}
|
||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||
connectionManagerMock.demand.isConnected { dest ->
|
||||
assert dest == destinations.dest1
|
||||
false
|
||||
|
@ -24,9 +24,9 @@ class FileHasherTest extends GroovyTestCase {
|
||||
|
||||
@Test
|
||||
void testPieceSize() {
|
||||
assert 18 == FileHasher.getPieceSize(1000000)
|
||||
assert 20 == FileHasher.getPieceSize(100000000)
|
||||
assert 30 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||
assert 17 == FileHasher.getPieceSize(1000000)
|
||||
assert 17 == FileHasher.getPieceSize(100000000)
|
||||
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||
shouldFail IllegalArgumentException, {
|
||||
FileHasher.getPieceSize(Long.MAX_VALUE)
|
||||
}
|
||||
@ -48,7 +48,7 @@ class FileHasherTest extends GroovyTestCase {
|
||||
fos.write b
|
||||
fos.close()
|
||||
def ih = hasher.hashFile tmp
|
||||
assert ih.getHashList().length == 32
|
||||
assert ih.getHashList().length == 64
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -58,7 +58,7 @@ class FileHasherTest extends GroovyTestCase {
|
||||
fos.write b
|
||||
fos.close()
|
||||
def ih = hasher.hashFile tmp
|
||||
assert ih.getHashList().length == 64
|
||||
assert ih.getHashList().length == 96
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -68,7 +68,7 @@ class FileHasherTest extends GroovyTestCase {
|
||||
fos.write b
|
||||
fos.close()
|
||||
def ih = hasher.hashFile tmp
|
||||
assert ih.getHashList().length == 64
|
||||
assert ih.getHashList().length == 128
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -78,6 +78,6 @@ class FileHasherTest extends GroovyTestCase {
|
||||
fos.write b
|
||||
fos.close()
|
||||
def ih = hasher.hashFile tmp
|
||||
assert ih.getHashList().length == 32 * 3
|
||||
assert ih.getHashList().length == 160
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
@ -26,7 +27,7 @@ class FileManagerTest {
|
||||
void before() {
|
||||
eventBus = new EventBus()
|
||||
eventBus.register(ResultsEvent.class, listener)
|
||||
manager = new FileManager(eventBus)
|
||||
manager = new FileManager(eventBus, new MuWireSettings())
|
||||
results = null
|
||||
}
|
||||
|
||||
@ -34,7 +35,7 @@ class FileManagerTest {
|
||||
void testHash1Result() {
|
||||
File f = new File("a b.c")
|
||||
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)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@ -53,8 +54,8 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testHash2Results() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
@ -75,7 +76,7 @@ class FileManagerTest {
|
||||
void testHash0Results() {
|
||||
File f = new File("a b.c")
|
||||
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)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@ -89,7 +90,7 @@ class FileManagerTest {
|
||||
void testKeyword1Result() {
|
||||
File f = new File("a b.c")
|
||||
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)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@ -107,12 +108,12 @@ class FileManagerTest {
|
||||
void testKeyword2Results() {
|
||||
File f1 = new File("a b.c")
|
||||
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)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
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)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
@ -130,7 +131,7 @@ class FileManagerTest {
|
||||
void testKeyword0Results() {
|
||||
File f = new File("a b.c")
|
||||
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)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@ -143,8 +144,8 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testRemoveFileExistingHash() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
@ -161,12 +162,12 @@ class FileManagerTest {
|
||||
void testRemoveFile() {
|
||||
File f1 = new File("a b.c")
|
||||
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)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
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.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
|
||||
|
@ -8,6 +8,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class HasherServiceTest {
|
||||
|
||||
@ -24,7 +25,7 @@ class HasherServiceTest {
|
||||
void before() {
|
||||
eventBus = new EventBus()
|
||||
hasher = new FileHasher()
|
||||
service = new HasherService(hasher, eventBus)
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||
eventBus.register(FileHashedEvent.class, listener)
|
||||
service.start()
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class PersisterServiceLoadingTest {
|
||||
FileHasher fh = new FileHasher()
|
||||
InfoHash ih1 = fh.hashFile(sharedFile1)
|
||||
|
||||
assert ih1.getHashList().length == 2 * 32
|
||||
assert ih1.getHashList().length == 96
|
||||
|
||||
def json = [:]
|
||||
json.file = getSharedFileJsonName(sharedFile1)
|
||||
@ -111,7 +111,9 @@ class PersisterServiceLoadingTest {
|
||||
String hash1 = Base64.encode(tmp)
|
||||
System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32)
|
||||
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)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import com.muwire.core.Destinations
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ -31,7 +32,7 @@ class PersisterServiceSavingTest {
|
||||
f = new File("build.gradle")
|
||||
f = f.getCanonicalFile()
|
||||
ih = fh.hashFile(f)
|
||||
fileSource = new FileManager(eventBus) {
|
||||
fileSource = new FileManager(eventBus, new MuWireSettings()) {
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
Map<File, SharedFile> rv = new HashMap<>()
|
||||
rv.put(f, sf)
|
||||
@ -54,7 +55,7 @@ class PersisterServiceSavingTest {
|
||||
|
||||
@Test
|
||||
void testSavingSharedFile() {
|
||||
sf = new SharedFile(f, ih)
|
||||
sf = new SharedFile(f, ih, 0)
|
||||
|
||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||
ps.start()
|
||||
@ -73,7 +74,7 @@ class PersisterServiceSavingTest {
|
||||
@Test
|
||||
void testSavingDownloadedFile() {
|
||||
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.start()
|
||||
|
@ -30,7 +30,18 @@ class SearchIndexTest {
|
||||
assert found.size() == 2
|
||||
assert found.contains("a b.c")
|
||||
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
|
||||
void testDrillDown() {
|
||||
|
37
doc/infohash-upgrade.md
Normal file
37
doc/infohash-upgrade.md
Normal 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).
|
@ -131,12 +131,18 @@ Sent by a leaf or ultrapeer when performing a search. Contains the reply-to per
|
||||
firstHop: false,
|
||||
keywords : ["keyword1","keyword2"...]
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
The "Search" message is also sent from an ultrapeer to a leaf.
|
||||
|
@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.0.13
|
||||
version = 0.1.5
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@ -15,6 +15,7 @@ import javax.inject.Inject
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
@ -50,8 +51,9 @@ class MainFrameController {
|
||||
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
|
||||
} else {
|
||||
// this can be improved a lot
|
||||
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
|
||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
|
||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: false)
|
||||
}
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
@ -81,12 +83,19 @@ class MainFrameController {
|
||||
int row = table.getSelectedRow()
|
||||
if (row == -1)
|
||||
return
|
||||
def sortEvt = group.view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
row = sortEvt.convertPreviousRowIndexToModel(row)
|
||||
}
|
||||
group.model.results[row]
|
||||
}
|
||||
|
||||
private def selectedDownload() {
|
||||
private int selectedDownload() {
|
||||
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
|
||||
@ -123,13 +132,14 @@ class MainFrameController {
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
def downloader = selectedDownload()
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
downloader.cancel()
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void resume() {
|
||||
def downloader = selectedDownload()
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
downloader.resume()
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||
|
@ -1,11 +1,13 @@
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.env.Metadata
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
|
||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
@ -34,13 +36,13 @@ class Ready extends AbstractLifecycleHandler {
|
||||
log.info "starting core services"
|
||||
def portableHome = System.getProperty("portable.home")
|
||||
def home = portableHome == null ?
|
||||
System.getProperty("user.home") + File.separator + ".MuWire" :
|
||||
selectHome() :
|
||||
portableHome
|
||||
|
||||
home = new File(home)
|
||||
if (!home.exists()) {
|
||||
log.info("creating home dir")
|
||||
home.mkdir()
|
||||
log.info("creating home dir $home")
|
||||
home.mkdirs()
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,9 @@ class MainFrameView {
|
||||
@MVCMember @Nonnull
|
||||
MainFrameModel model
|
||||
|
||||
def downloadsTable
|
||||
def lastDownloadSortEvent
|
||||
|
||||
void initUI() {
|
||||
builder.with {
|
||||
application(size : [1024,768], id: 'main-frame',
|
||||
@ -105,10 +108,10 @@ class MainFrameView {
|
||||
panel (constraints : JSplitPane.BOTTOM) {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
table(id : "downloads-table") {
|
||||
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
|
||||
tableModel(list: model.downloads) {
|
||||
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 ->
|
||||
int pieces = row.downloader.nPieces
|
||||
int done = row.downloader.donePieces()
|
||||
@ -136,7 +139,7 @@ class MainFrameView {
|
||||
button(text : "Click here to share files", actionPerformed : shareFiles)
|
||||
}
|
||||
scrollPane ( constraints : BorderLayout.CENTER) {
|
||||
table(id : "shared-files-table") {
|
||||
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
||||
tableModel(list : model.shared) {
|
||||
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
|
||||
closureColumn(header : "Size", preferredWidth : 50, type : String,
|
||||
@ -213,7 +216,7 @@ class MainFrameView {
|
||||
panel (border : etchedBorder()){
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "trusted-table") {
|
||||
table(id : "trusted-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
}
|
||||
@ -228,7 +231,7 @@ class MainFrameView {
|
||||
panel (border : etchedBorder()){
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "distrusted-table") {
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
}
|
||||
@ -260,7 +263,7 @@ class MainFrameView {
|
||||
def selectionModel = downloadsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = downloadsTable.getSelectedRow()
|
||||
int selectedRow = selectedDownloaderRow()
|
||||
def downloader = model.downloads[selectedRow].downloader
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
@ -280,9 +283,18 @@ class MainFrameView {
|
||||
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
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 cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||
|
@ -53,7 +53,7 @@ class OptionsView {
|
||||
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 : "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))
|
||||
|
||||
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
|
||||
|
@ -26,19 +26,20 @@ class SearchTabView {
|
||||
def parent
|
||||
def searchTerms
|
||||
def resultsTable
|
||||
def lastSortEvent
|
||||
|
||||
void initUI() {
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def pane = scrollPane {
|
||||
resultsTable = table(id : "results-table") {
|
||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
|
||||
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)
|
||||
model.core.trustService.getLevel(row.sender.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -84,6 +85,8 @@ class SearchTabView {
|
||||
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
|
||||
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
|
||||
|
||||
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
|
Reference in New Issue
Block a user