Compare commits
147 Commits
muwire-0.0
...
muwire-0.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
040248560a | ||
![]() |
77caaf83de | ||
![]() |
cc5ece5103 | ||
![]() |
db7e21e343 | ||
![]() |
a388eaec1d | ||
![]() |
8ff39072c7 | ||
![]() |
55d2ac9b24 | ||
![]() |
6ebe492fd8 | ||
![]() |
165cd542ec | ||
![]() |
5ca0c8b00d | ||
![]() |
b6a38e3f23 | ||
![]() |
34d9165bd5 | ||
![]() |
2e52dd5c49 | ||
![]() |
2a315dd734 | ||
![]() |
6b661b99c5 | ||
![]() |
5dacd60bbb | ||
![]() |
f8f7cfe836 | ||
![]() |
0b4f261bc1 | ||
![]() |
042d67d784 | ||
![]() |
800df88f14 | ||
![]() |
4d1eac50a0 | ||
![]() |
c48df7f14b | ||
![]() |
9d04148001 | ||
![]() |
bb4d522572 | ||
![]() |
8052501e52 | ||
![]() |
66cc6d8ab7 | ||
![]() |
a45e57f5ec | ||
![]() |
7d8ca55d87 | ||
![]() |
de22f3c6b9 | ||
![]() |
3b0eb5678d | ||
![]() |
5a1f32e40b | ||
![]() |
ca3f2513e1 | ||
![]() |
658d9cf5a8 | ||
![]() |
e389090b7e | ||
![]() |
04ceaba514 | ||
![]() |
6a01d97a8d | ||
![]() |
747663e1dc | ||
![]() |
e426b3ccbd | ||
![]() |
5172e19627 | ||
![]() |
e826cfd8d5 | ||
![]() |
51004f6fe9 | ||
![]() |
08bb2b614d | ||
![]() |
d0e5d0ce8a | ||
![]() |
9e05802d1b | ||
![]() |
fb4f56eec9 | ||
![]() |
be2083d430 | ||
![]() |
af6275d0a3 | ||
![]() |
5269815329 | ||
![]() |
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 | ||
![]() |
a5d442d320 | ||
![]() |
3f9ee887d6 | ||
![]() |
4a9e6d3b6b | ||
![]() |
80f2cc5f99 | ||
![]() |
12283dba9d | ||
![]() |
5c959bc8b7 | ||
![]() |
f3712fe7af | ||
![]() |
3e49b0ec66 | ||
![]() |
f90beb8e3d | ||
![]() |
fbad7b6c7e | ||
![]() |
ec2d89c18c | ||
![]() |
c27fc0a515 | ||
![]() |
14681c2060 | ||
![]() |
1aeb230ea8 | ||
![]() |
d1dfc73f5a | ||
![]() |
0cebe4119c | ||
![]() |
9f21120ec8 | ||
![]() |
7eea8be67d | ||
![]() |
f114302bdb | ||
![]() |
05b9b37488 | ||
![]() |
52f317a5b7 | ||
![]() |
fb8227a1f3 | ||
![]() |
5677d9f46a | ||
![]() |
c5192e3845 | ||
![]() |
43c2a55cb8 | ||
![]() |
94f6de6bea | ||
![]() |
6782849a12 | ||
![]() |
c07d351c5d | ||
![]() |
dc2f675dd3 | ||
![]() |
a8e795ec51 | ||
![]() |
33c5b3b18e | ||
![]() |
581fce4643 | ||
![]() |
7fe78a0719 | ||
![]() |
cdb6e22522 | ||
![]() |
2edeb046be | ||
![]() |
4021f3c244 | ||
![]() |
9008fac24d | ||
![]() |
e2f92c5c5e | ||
![]() |
7b33a16fd8 | ||
![]() |
9a2531b264 | ||
![]() |
9a8dadff57 | ||
![]() |
4a274010f9 | ||
![]() |
1eb930435b | ||
![]() |
9df28552ad | ||
![]() |
ac0204dffc | ||
![]() |
e5c402a400 | ||
![]() |
7704c73b68 | ||
![]() |
a9aa8dd840 | ||
![]() |
de682a802a | ||
![]() |
5435518212 | ||
![]() |
bd01f983c9 | ||
![]() |
8b63864b90 |
17
README.md
17
README.md
@@ -1,14 +1,14 @@
|
|||||||
# 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 current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||||
|
|
||||||
```
|
```
|
||||||
./gradlew assemble
|
./gradlew assemble
|
||||||
@@ -23,17 +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
|
||||||
|
|
||||||
* Any shared files get re-hashed on startup
|
|
||||||
* Sometimes the list of shared files gets lost
|
|
||||||
* Many UI features you would expect are not there yet
|
* Many UI features you would expect are not there yet
|
||||||
|
* Sorting the results table sometimes causes the wrong result to be downloaded
|
||||||
|
|
||||||
|
@@ -1,7 +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']
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(":core")
|
compile project(":core")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,18 @@
|
|||||||
package com.muwire.cli
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||||
|
import com.muwire.core.connection.ConnectionEvent
|
||||||
|
import com.muwire.core.connection.DisconnectionEvent
|
||||||
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
|
import com.muwire.core.files.FileHashedEvent
|
||||||
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
import com.muwire.core.upload.UploadEvent
|
||||||
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
|
||||||
class Cli {
|
class Cli {
|
||||||
|
|
||||||
@@ -23,20 +34,109 @@ class Cli {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.0.8")
|
core = new Core(props, home, "0.1.11")
|
||||||
} 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"
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def filesList
|
||||||
|
if (args.length == 0) {
|
||||||
|
println "Enter a file containing list of files to share"
|
||||||
|
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||||
|
filesList = reader.readLine()
|
||||||
|
} else
|
||||||
|
filesList = args[0]
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
println "loading shared files from $filesList"
|
||||||
|
|
||||||
|
// listener for shared files
|
||||||
|
def sharedListener = new SharedListener()
|
||||||
|
core.eventBus.register(FileHashedEvent.class, sharedListener)
|
||||||
|
core.eventBus.register(FileLoadedEvent.class, sharedListener)
|
||||||
|
|
||||||
|
// for connections
|
||||||
|
def connectionsListener = new ConnectionListener()
|
||||||
|
core.eventBus.register(ConnectionEvent.class, connectionsListener)
|
||||||
|
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
|
||||||
|
|
||||||
|
// for uploads
|
||||||
|
def uploadsListener = new UploadsListener()
|
||||||
|
core.eventBus.register(UploadEvent.class, uploadsListener)
|
||||||
|
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
|
||||||
|
|
||||||
|
Timer timer = new Timer("status-printer", true)
|
||||||
|
timer.schedule({
|
||||||
|
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
|
||||||
|
} as TimerTask, 60000, 60000)
|
||||||
|
|
||||||
|
def latch = new CountDownLatch(1)
|
||||||
|
def fileLoader = new Object() {
|
||||||
|
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
|
println "waiting for files to load"
|
||||||
|
latch.await()
|
||||||
// now we begin
|
// now we begin
|
||||||
println "MuWire is ready"
|
println "MuWire is ready"
|
||||||
println "Enter a file containing list of files to share"
|
|
||||||
def reader = new BufferedReader(new InputStreamReader(System.in))
|
filesList = new File(filesList)
|
||||||
def filesList = reader.readLine()
|
filesList.withReader {
|
||||||
|
def toShare = it.readLine()
|
||||||
|
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
||||||
|
}
|
||||||
|
Runtime.getRuntime().addShutdownHook({
|
||||||
|
println "shutting down.."
|
||||||
|
core.shutdown()
|
||||||
|
println "shutdown."
|
||||||
|
})
|
||||||
Thread.sleep(Integer.MAX_VALUE)
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
static class ConnectionListener {
|
||||||
|
volatile int connections
|
||||||
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
connections++
|
||||||
|
}
|
||||||
|
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||||
|
connections--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class UploadsListener {
|
||||||
|
volatile int uploads
|
||||||
|
public void onUploadEvent(UploadEvent e) {
|
||||||
|
uploads++
|
||||||
|
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
|
}
|
||||||
|
public void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||||
|
uploads--
|
||||||
|
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SharedListener {
|
||||||
|
volatile int shared
|
||||||
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
|
if (e.error != null)
|
||||||
|
println "ERROR $e.error"
|
||||||
|
else {
|
||||||
|
println "Shared file : $e.sharedFile.file"
|
||||||
|
shared++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||||
|
shared++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.11")
|
||||||
|
} 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.LeafConnectionManager
|
||||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||||
import com.muwire.core.download.DownloadManager
|
import com.muwire.core.download.DownloadManager
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
@@ -56,6 +57,8 @@ public class Core {
|
|||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Persona me
|
final Persona me
|
||||||
final File home
|
final File home
|
||||||
|
final Properties i2pOptions
|
||||||
|
final MuWireSettings muOptions
|
||||||
|
|
||||||
private final TrustService trustService
|
private final TrustService trustService
|
||||||
private final PersisterService persisterService
|
private final PersisterService persisterService
|
||||||
@@ -66,9 +69,11 @@ public class Core {
|
|||||||
private final ConnectionAcceptor connectionAcceptor
|
private final ConnectionAcceptor connectionAcceptor
|
||||||
private final ConnectionEstablisher connectionEstablisher
|
private final ConnectionEstablisher connectionEstablisher
|
||||||
private final HasherService hasherService
|
private final HasherService hasherService
|
||||||
|
private final DownloadManager downloadManager
|
||||||
|
|
||||||
public Core(MuWireSettings props, File home, String myVersion) {
|
public Core(MuWireSettings props, File home, String myVersion) {
|
||||||
this.home = home
|
this.home = home
|
||||||
|
this.muOptions = props
|
||||||
log.info "Initializing I2P context"
|
log.info "Initializing I2P context"
|
||||||
I2PAppContext.getGlobalContext().logManager()
|
I2PAppContext.getGlobalContext().logManager()
|
||||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||||
@@ -83,12 +88,31 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def sysProps = System.getProperties().clone()
|
i2pOptions = new Properties()
|
||||||
sysProps["inbound.nickname"] = "MuWire"
|
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||||
|
if (i2pOptionsFile.exists()) {
|
||||||
|
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||||
|
|
||||||
|
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||||
|
i2pOptions["inbound.nickname"] = "MuWire"
|
||||||
|
if (!i2pOptions.containsKey("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"
|
||||||
|
i2pOptions["outbound.quantity"] = "2"
|
||||||
|
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||||
|
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||||
|
}
|
||||||
|
|
||||||
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
I2PSession i2pSession
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, sysProps)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||||
}
|
}
|
||||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||||
@@ -129,7 +153,7 @@ public class Core {
|
|||||||
|
|
||||||
|
|
||||||
log.info "initializing file manager"
|
log.info "initializing file manager"
|
||||||
FileManager fileManager = new FileManager(eventBus)
|
FileManager fileManager = new FileManager(eventBus, props)
|
||||||
eventBus.register(FileHashedEvent.class, fileManager)
|
eventBus.register(FileHashedEvent.class, fileManager)
|
||||||
eventBus.register(FileLoadedEvent.class, fileManager)
|
eventBus.register(FileLoadedEvent.class, fileManager)
|
||||||
eventBus.register(FileDownloadedEvent.class, fileManager)
|
eventBus.register(FileDownloadedEvent.class, fileManager)
|
||||||
@@ -137,7 +161,7 @@ public class Core {
|
|||||||
eventBus.register(SearchEvent.class, fileManager)
|
eventBus.register(SearchEvent.class, fileManager)
|
||||||
|
|
||||||
log.info "initializing persistence service"
|
log.info "initializing persistence service"
|
||||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
|
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||||
|
|
||||||
log.info("initializing host cache")
|
log.info("initializing host cache")
|
||||||
File hostStorage = new File(home, "hosts.json")
|
File hostStorage = new File(home, "hosts.json")
|
||||||
@@ -147,7 +171,8 @@ public class Core {
|
|||||||
|
|
||||||
log.info("initializing connection manager")
|
log.info("initializing connection manager")
|
||||||
connectionManager = props.isLeaf() ?
|
connectionManager = props.isLeaf() ?
|
||||||
new LeafConnectionManager(eventBus, me, 3, hostCache) : new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService)
|
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||||
|
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||||
eventBus.register(TrustEvent.class, connectionManager)
|
eventBus.register(TrustEvent.class, connectionManager)
|
||||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||||
@@ -171,23 +196,26 @@ 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 = 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)
|
||||||
|
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
log.info("initializing upload manager")
|
||||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||||
|
|
||||||
|
log.info("initializing connection establisher")
|
||||||
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
|
|
||||||
log.info("initializing acceptor")
|
log.info("initializing acceptor")
|
||||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager)
|
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||||
|
|
||||||
|
|
||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
|
||||||
|
|
||||||
log.info("initializing hasher service")
|
log.info("initializing hasher service")
|
||||||
hasherService = new HasherService(new FileHasher(), eventBus)
|
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||||
eventBus.register(FileSharedEvent.class, hasherService)
|
eventBus.register(FileSharedEvent.class, hasherService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +234,14 @@ public class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
log.info("shutting down connection manager")
|
||||||
connectionManager.shutdown()
|
connectionManager.shutdown()
|
||||||
|
log.info("shutting down download manageer")
|
||||||
|
downloadManager.shutdown()
|
||||||
|
log.info("shutting down connection acceeptor")
|
||||||
|
connectionAcceptor.stop()
|
||||||
|
log.info("shutting down connection establisher")
|
||||||
|
connectionEstablisher.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
static main(args) {
|
static main(args) {
|
||||||
@@ -233,7 +268,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.0.8")
|
Core core = new Core(props, home, "0.1.11")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -3,6 +3,7 @@ package com.muwire.core
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
|
||||||
@@ -23,14 +24,18 @@ 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) {
|
||||||
currentHandlers = handlers.getOrDefault(clazz, [])
|
currentHandlers = handlers.getOrDefault(clazz, [])
|
||||||
}
|
}
|
||||||
currentHandlers.each {
|
currentHandlers.each {
|
||||||
it."on${clazz.getSimpleName()}"(e)
|
try {
|
||||||
|
it."on${clazz.getSimpleName()}"(e)
|
||||||
|
} catch (Exception bad) {
|
||||||
|
log.log(Level.SEVERE, "exception dispatching event",bad)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ class MuWireSettings {
|
|||||||
File downloadLocation
|
File downloadLocation
|
||||||
String sharedFiles
|
String sharedFiles
|
||||||
CrawlerResponse crawlerResponse
|
CrawlerResponse crawlerResponse
|
||||||
|
boolean shareDownloadedFiles
|
||||||
|
|
||||||
MuWireSettings() {
|
MuWireSettings() {
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
@@ -27,6 +28,7 @@ class MuWireSettings {
|
|||||||
sharedFiles = props.getProperty("sharedFiles")
|
sharedFiles = props.getProperty("sharedFiles")
|
||||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
||||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
||||||
|
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -38,6 +40,7 @@ class MuWireSettings {
|
|||||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||||
|
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||||
if (sharedFiles != null)
|
if (sharedFiles != null)
|
||||||
props.setProperty("sharedFiles", sharedFiles)
|
props.setProperty("sharedFiles", sharedFiles)
|
||||||
props.store(out, "")
|
props.store(out, "")
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core
|
||||||
|
|
||||||
|
class UILoadedEvent extends Event {
|
||||||
|
}
|
@@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||||
@@ -26,7 +27,8 @@ abstract class Connection implements Closeable {
|
|||||||
final boolean incoming
|
final boolean incoming
|
||||||
final HostCache hostCache
|
final HostCache hostCache
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
|
final MuWireSettings settings
|
||||||
|
|
||||||
private final AtomicBoolean running = new AtomicBoolean()
|
private final AtomicBoolean running = new AtomicBoolean()
|
||||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||||
private final Thread reader, writer
|
private final Thread reader, writer
|
||||||
@@ -35,12 +37,14 @@ abstract class Connection implements Closeable {
|
|||||||
|
|
||||||
long lastPingSentTime, lastPongReceivedTime
|
long lastPingSentTime, lastPongReceivedTime
|
||||||
|
|
||||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming, HostCache hostCache, TrustService trustService) {
|
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||||
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.incoming = incoming
|
this.incoming = incoming
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
|
this.settings = settings
|
||||||
|
|
||||||
this.name = endpoint.destination.toBase32().substring(0,8)
|
this.name = endpoint.destination.toBase32().substring(0,8)
|
||||||
|
|
||||||
@@ -72,9 +76,9 @@ abstract class Connection implements Closeable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.info("closing $name")
|
log.info("closing $name")
|
||||||
endpoint.close()
|
|
||||||
reader.interrupt()
|
reader.interrupt()
|
||||||
writer.interrupt()
|
writer.interrupt()
|
||||||
|
endpoint.close()
|
||||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +127,9 @@ 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)
|
||||||
|
query.infohash = Base64.encode(e.searchEvent.searchHash)
|
||||||
query.replyTo = e.replyTo.toBase64()
|
query.replyTo = e.replyTo.toBase64()
|
||||||
if (e.originator != null)
|
if (e.originator != null)
|
||||||
query.originator = e.originator.toBase64()
|
query.originator = e.originator.toBase64()
|
||||||
@@ -151,15 +158,22 @@ 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)
|
||||||
if (trustService.getLevel(replyTo) == TrustLevel.DISTRUSTED) {
|
TrustLevel trustLevel = trustService.getLevel(replyTo)
|
||||||
|
if (trustLevel == TrustLevel.DISTRUSTED) {
|
||||||
log.info "dropping search from distrusted peer"
|
log.info "dropping search from distrusted peer"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: add option to respond only to trusted peers
|
if (trustLevel == TrustLevel.NEUTRAL && !settings.allowUntrusted()) {
|
||||||
|
log.info("dropping search from neutral peer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Persona originator = null
|
Persona originator = null
|
||||||
if (search.originator != null) {
|
if (search.originator != null) {
|
||||||
@@ -170,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 : 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,
|
||||||
|
@@ -34,13 +34,17 @@ class ConnectionAcceptor {
|
|||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
final SearchManager searchManager
|
final SearchManager searchManager
|
||||||
final UploadManager uploadManager
|
final UploadManager uploadManager
|
||||||
|
final ConnectionEstablisher establisher
|
||||||
|
|
||||||
final ExecutorService acceptorThread
|
final ExecutorService acceptorThread
|
||||||
final ExecutorService handshakerThreads
|
final ExecutorService handshakerThreads
|
||||||
|
|
||||||
|
private volatile shutdown
|
||||||
|
|
||||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager) {
|
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||||
|
ConnectionEstablisher establisher) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.manager = manager
|
this.manager = manager
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
@@ -49,7 +53,8 @@ class ConnectionAcceptor {
|
|||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
this.searchManager = searchManager
|
this.searchManager = searchManager
|
||||||
this.uploadManager = uploadManager
|
this.uploadManager = uploadManager
|
||||||
|
this.establisher = establisher
|
||||||
|
|
||||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||||
def rv = new Thread(r)
|
def rv = new Thread(r)
|
||||||
rv.setDaemon(true)
|
rv.setDaemon(true)
|
||||||
@@ -70,11 +75,13 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
|
shutdown = true
|
||||||
acceptorThread.shutdownNow()
|
acceptorThread.shutdownNow()
|
||||||
handshakerThreads.shutdownNow()
|
handshakerThreads.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acceptLoop() {
|
private void acceptLoop() {
|
||||||
|
try {
|
||||||
while(true) {
|
while(true) {
|
||||||
def incoming = acceptor.accept()
|
def incoming = acceptor.accept()
|
||||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||||
@@ -90,6 +97,11 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "exception in accept loop",e)
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processIncoming(Endpoint e) {
|
private void processIncoming(Endpoint e) {
|
||||||
@@ -105,6 +117,9 @@ class ConnectionAcceptor {
|
|||||||
case (byte)'G':
|
case (byte)'G':
|
||||||
processGET(e)
|
processGET(e)
|
||||||
break
|
break
|
||||||
|
case (byte)'H':
|
||||||
|
processHashList(e)
|
||||||
|
break
|
||||||
case (byte)'P':
|
case (byte)'P':
|
||||||
processPOST(e)
|
processPOST(e)
|
||||||
break
|
break
|
||||||
@@ -140,7 +155,9 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||||
boolean accept = !manager.isConnected(e.destination) && (leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
boolean accept = !manager.isConnected(e.destination) &&
|
||||||
|
!establisher.isInProgress(e.destination) &&
|
||||||
|
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||||
if (accept) {
|
if (accept) {
|
||||||
log.info("accepting connection, leaf:$leaf")
|
log.info("accepting connection, leaf:$leaf")
|
||||||
e.outputStream.write("OK".bytes)
|
e.outputStream.write("OK".bytes)
|
||||||
@@ -173,9 +190,18 @@ class ConnectionAcceptor {
|
|||||||
dis.readFully(et)
|
dis.readFully(et)
|
||||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||||
throw new IOException("Invalid GET connection")
|
throw new IOException("Invalid GET connection")
|
||||||
uploadManager.processEndpoint(e)
|
uploadManager.processGET(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processHashList(Endpoint e) {
|
||||||
|
byte[] ashList = new byte[8]
|
||||||
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(ashList)
|
||||||
|
if (ashList != "ASHLIST ".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
throw new IOException("Invalid HASHLIST connection")
|
||||||
|
uploadManager.processHashList(e)
|
||||||
|
}
|
||||||
|
|
||||||
private void processPOST(final Endpoint e) throws IOException {
|
private void processPOST(final Endpoint e) throws IOException {
|
||||||
byte [] ost = new byte[4]
|
byte [] ost = new byte[4]
|
||||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -19,13 +20,15 @@ abstract class ConnectionManager {
|
|||||||
|
|
||||||
protected final HostCache hostCache
|
protected final HostCache hostCache
|
||||||
protected final Persona me
|
protected final Persona me
|
||||||
|
protected final MuWireSettings settings
|
||||||
|
|
||||||
ConnectionManager() {}
|
ConnectionManager() {}
|
||||||
|
|
||||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache) {
|
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
|
this.settings = settings
|
||||||
this.timer = new Timer("connections-pinger",true)
|
this.timer = new Timer("connections-pinger",true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ class Endpoint implements Closeable {
|
|||||||
try {outputStream.close()} catch (Exception ignore) {}
|
try {outputStream.close()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
if (toClose != null) {
|
if (toClose != null) {
|
||||||
try {toClose.close()} catch (Exception ignore) {}
|
try {toClose.reset()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
|
||||||
@@ -16,8 +17,9 @@ import net.i2p.data.Destination
|
|||||||
*/
|
*/
|
||||||
class LeafConnection extends Connection {
|
class LeafConnection extends Connection {
|
||||||
|
|
||||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||||
super(eventBus, endpoint, true, hostCache, trustService);
|
TrustService trustService, MuWireSettings settings) {
|
||||||
|
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.connection
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -17,8 +18,9 @@ class LeafConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
||||||
|
|
||||||
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections, HostCache hostCache) {
|
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
||||||
super(eventBus, me, hostCache)
|
HostCache hostCache, MuWireSettings settings) {
|
||||||
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxConnections = maxConnections
|
this.maxConnections = maxConnections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
@@ -29,8 +30,9 @@ class PeerConnection extends Connection {
|
|||||||
private final JsonSlurper slurper = new JsonSlurper()
|
private final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||||
boolean incoming, HostCache hostCache, TrustService trustService) {
|
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||||
super(eventBus, endpoint, incoming, hostCache, trustService)
|
MuWireSettings settings) {
|
||||||
|
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||||
this.dis = new DataInputStream(endpoint.inputStream)
|
this.dis = new DataInputStream(endpoint.inputStream)
|
||||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import java.util.Collection
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -17,15 +18,15 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
final int maxPeers, maxLeafs
|
final int maxPeers, maxLeafs
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
|
|
||||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||||
|
|
||||||
UltrapeerConnectionManager() {}
|
UltrapeerConnectionManager() {}
|
||||||
|
|
||||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||||
HostCache hostCache, TrustService trustService) {
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
super(eventBus, me, hostCache)
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxPeers = maxPeers
|
this.maxPeers = maxPeers
|
||||||
this.maxLeafs = maxLeafs
|
this.maxLeafs = maxLeafs
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
@@ -85,8 +86,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
return
|
return
|
||||||
|
|
||||||
Connection c = e.leaf ?
|
Connection c = e.leaf ?
|
||||||
new LeafConnection(eventBus, e.endpoint, hostCache, trustService) :
|
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
||||||
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService)
|
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
||||||
def map = e.leaf ? leafConnections : peerConnections
|
def map = e.leaf ? leafConnections : peerConnections
|
||||||
map.put(e.endpoint.destination, c)
|
map.put(e.endpoint.destination, c)
|
||||||
c.start()
|
c.start()
|
||||||
@@ -103,8 +104,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
peerConnections.each {k,v -> v.close() }
|
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
leafConnections.each {k,v -> v.close() }
|
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
peerConnections.clear()
|
peerConnections.clear()
|
||||||
leafConnections.clear()
|
leafConnections.clear()
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
@@ -47,14 +59,85 @@ public class DownloadManager {
|
|||||||
destinations.add(it.sender.destination)
|
destinations.add(it.sender.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
def downloader = new Downloader(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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||||
|
downloaders.remove(e.downloader)
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
InfoHash infoHash
|
||||||
|
if (json.hashList != null) {
|
||||||
|
byte[] hashList = Base64.decode(json.hashList)
|
||||||
|
infoHash = InfoHash.fromHashList(hashList)
|
||||||
|
} else {
|
||||||
|
byte [] root = Base64.decode(json.hashRoot)
|
||||||
|
infoHash = new InfoHash(root)
|
||||||
|
}
|
||||||
|
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||||
|
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
||||||
|
downloaders.add(downloader)
|
||||||
|
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
|
||||||
|
|
||||||
|
InfoHash infoHash = downloader.getInfoHash()
|
||||||
|
if (infoHash.hashList != null)
|
||||||
|
json.hashList = Base64.encode(infoHash.hashList)
|
||||||
|
else
|
||||||
|
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||||
|
writer.println(JsonOutput.toJson(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
downloaders.each { it.stop() }
|
||||||
|
Downloader.executorService.shutdownNow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,8 +31,8 @@ class DownloadSession {
|
|||||||
private final long fileLength
|
private final long fileLength
|
||||||
private final MessageDigest digest
|
private final MessageDigest digest
|
||||||
|
|
||||||
private final ArrayDeque<Long> timestamps = new ArrayDeque<>(SAMPLES)
|
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||||
private final ArrayDeque<Integer> reads = new ArrayDeque<>(SAMPLES)
|
private final LinkedList<Integer> reads = new LinkedList<>()
|
||||||
|
|
||||||
private ByteBuffer mapped
|
private ByteBuffer mapped
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ class DownloadSession {
|
|||||||
if (!code.startsWith("200 ")) {
|
if (!code.startsWith("200 ")) {
|
||||||
log.warning("unknown code $code")
|
log.warning("unknown code $code")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse all headers
|
// parse all headers
|
||||||
@@ -131,7 +131,7 @@ class DownloadSession {
|
|||||||
if (receivedStart != start || receivedEnd != end) {
|
if (receivedStart != start || receivedEnd != end) {
|
||||||
log.warning("We don't support mismatching ranges yet")
|
log.warning("We don't support mismatching ranges yet")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the download
|
// start the download
|
||||||
@@ -183,9 +183,22 @@ class DownloadSession {
|
|||||||
synchronized int speed() {
|
synchronized int speed() {
|
||||||
if (timestamps.size() < SAMPLES)
|
if (timestamps.size() < SAMPLES)
|
||||||
return 0
|
return 0
|
||||||
long interval = timestamps.last - timestamps.first
|
|
||||||
int totalRead = 0
|
int totalRead = 0
|
||||||
reads.each { totalRead += it }
|
int idx = 0
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
|
||||||
|
idx++
|
||||||
|
if (idx == SAMPLES)
|
||||||
|
return 0
|
||||||
|
if (idx == SAMPLES - 1)
|
||||||
|
return reads[idx]
|
||||||
|
|
||||||
|
long interval = timestamps.last - timestamps[idx]
|
||||||
|
if (interval == 0)
|
||||||
|
interval = 1
|
||||||
|
for (int i = idx; i < SAMPLES; i++)
|
||||||
|
totalRead += reads[idx]
|
||||||
(int)(totalRead * 1000.0 / interval)
|
(int)(totalRead * 1000.0 / interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,15 +10,18 @@ import java.util.concurrent.Executors
|
|||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.DownloadedFile
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.connection.I2PConnector
|
import com.muwire.core.connection.I2PConnector
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Downloader {
|
public class Downloader {
|
||||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||||
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
|
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||||
|
|
||||||
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||||
Thread rv = new Thread(r)
|
Thread rv = new Thread(r)
|
||||||
@@ -27,25 +30,30 @@ public class Downloader {
|
|||||||
rv
|
rv
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
private final DownloadManager downloadManager
|
private final DownloadManager downloadManager
|
||||||
private final Persona me
|
private final Persona me
|
||||||
private final File file
|
private final File file
|
||||||
private final Pieces downloaded, claimed
|
private final Pieces downloaded, claimed
|
||||||
private final long length
|
private final long length
|
||||||
private final InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
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<>()
|
||||||
|
|
||||||
|
|
||||||
private volatile boolean cancelled
|
private volatile boolean cancelled
|
||||||
|
private volatile boolean eventFired
|
||||||
|
|
||||||
public Downloader(DownloadManager downloadManager, Persona me, File file, long length, InfoHash infoHash,
|
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||||
|
Persona me, File file, long length, InfoHash infoHash,
|
||||||
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||||
File incompletes) {
|
File incompletes) {
|
||||||
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
this.downloadManager = downloadManager
|
this.downloadManager = downloadManager
|
||||||
this.file = file
|
this.file = file
|
||||||
@@ -54,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
|
||||||
@@ -67,6 +76,14 @@ public class Downloader {
|
|||||||
claimed = new Pieces(nPieces)
|
claimed = new Pieces(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized InfoHash getInfoHash() {
|
||||||
|
infoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
|
this.infoHash = infoHash
|
||||||
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
readPieces()
|
readPieces()
|
||||||
destinations.each {
|
destinations.each {
|
||||||
@@ -81,8 +98,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +121,8 @@ public class Downloader {
|
|||||||
int total = 0
|
int total = 0
|
||||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
total += it.speed()
|
if (it.currentState == WorkerState.DOWNLOADING)
|
||||||
|
total += it.speed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
total
|
total
|
||||||
@@ -135,11 +153,28 @@ public class Downloader {
|
|||||||
if (oneDownloading)
|
if (oneDownloading)
|
||||||
return DownloadState.DOWNLOADING
|
return DownloadState.DOWNLOADING
|
||||||
|
|
||||||
|
// at least one is requesting hashlist
|
||||||
|
boolean oneHashlist = false
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.HASHLIST) {
|
||||||
|
oneHashlist = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oneHashlist)
|
||||||
|
return DownloadState.HASHLIST
|
||||||
|
|
||||||
return DownloadState.CONNECTING
|
return DownloadState.CONNECTING
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
stop()
|
||||||
|
file.delete()
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
@@ -155,7 +190,20 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
downloadManager.resume(this)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadWorker implements Runnable {
|
class DownloadWorker implements Runnable {
|
||||||
@@ -175,10 +223,16 @@ public class Downloader {
|
|||||||
Endpoint endpoint = null
|
Endpoint endpoint = null
|
||||||
try {
|
try {
|
||||||
endpoint = connector.connect(destination)
|
endpoint = connector.connect(destination)
|
||||||
|
while(getInfoHash().hashList == null) {
|
||||||
|
currentState = WorkerState.HASHLIST
|
||||||
|
HashListSession session = new HashListSession(me.toBase64(), infoHash, endpoint)
|
||||||
|
InfoHash received = session.request()
|
||||||
|
setInfoHash(received)
|
||||||
|
}
|
||||||
currentState = WorkerState.DOWNLOADING
|
currentState = WorkerState.DOWNLOADING
|
||||||
boolean requestPerformed
|
boolean requestPerformed
|
||||||
while(!downloaded.isComplete()) {
|
while(!downloaded.isComplete()) {
|
||||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
|
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
|
||||||
requestPerformed = currentSession.request()
|
requestPerformed = currentSession.request()
|
||||||
if (!requestPerformed)
|
if (!requestPerformed)
|
||||||
break
|
break
|
||||||
@@ -188,6 +242,15 @@ public class Downloader {
|
|||||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||||
} finally {
|
} finally {
|
||||||
currentState = WorkerState.FINISHED
|
currentState = WorkerState.FINISHED
|
||||||
|
if (downloaded.isComplete() && !eventFired) {
|
||||||
|
piecesFile.delete()
|
||||||
|
eventFired = true
|
||||||
|
eventBus.publish(
|
||||||
|
new FileDownloadedEvent(
|
||||||
|
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
||||||
|
downloader : Downloader.this))
|
||||||
|
|
||||||
|
}
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class HashListSession {
|
||||||
|
private final String meB64
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final Endpoint endpoint
|
||||||
|
|
||||||
|
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||||
|
this.meB64 = meB64
|
||||||
|
this.infoHash = infoHash
|
||||||
|
this.endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash request() throws IOException {
|
||||||
|
InputStream is = endpoint.getInputStream()
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
|
||||||
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
|
||||||
|
String code = readTillRN(is)
|
||||||
|
if (!code.startsWith("200"))
|
||||||
|
throw new IOException("unknown code $code")
|
||||||
|
|
||||||
|
// parse all headers
|
||||||
|
Set<String> headers = new HashSet<>()
|
||||||
|
String header
|
||||||
|
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||||
|
headers.add(header)
|
||||||
|
|
||||||
|
long receivedStart = -1
|
||||||
|
long receivedEnd = -1
|
||||||
|
for (String receivedHeader : headers) {
|
||||||
|
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
||||||
|
if (group.size() != 1) {
|
||||||
|
log.info("ignoring header $receivedHeader")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedStart = Long.parseLong(group[0][1])
|
||||||
|
receivedEnd = Long.parseLong(group[0][2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivedStart != 0)
|
||||||
|
throw new IOException("hashlist started at $receivedStart")
|
||||||
|
|
||||||
|
byte[] hashList = new byte[receivedEnd]
|
||||||
|
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||||
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
|
while(hashListBuf.hasRemaining()) {
|
||||||
|
if (hashListBuf.remaining() > tmp.length)
|
||||||
|
tmp = new byte[hashListBuf.remaining()]
|
||||||
|
int read = is.read(tmp)
|
||||||
|
if (read == -1)
|
||||||
|
throw new IOException()
|
||||||
|
hashListBuf.put(tmp, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash received = InfoHash.fromHashList(hashList)
|
||||||
|
if (received.getRoot() != infoHash.getRoot())
|
||||||
|
throw new IOException("fetched list doesn't match root")
|
||||||
|
received
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadCancelledEvent extends Event {
|
||||||
|
Downloader downloader
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class AllFilesLoadedEvent extends Event {
|
||||||
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 << 30)
|
||||||
return 18
|
return 17
|
||||||
|
|
||||||
for (int i = 26; i <= 37; i++) {
|
for (int i = 31; i <= 37; i++) {
|
||||||
if (size <= 0x1L << i) {
|
if (size <= 0x1L << i) {
|
||||||
return i-7
|
return i-13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +55,11 @@ class FileHasher {
|
|||||||
try {
|
try {
|
||||||
MappedByteBuffer buf
|
MappedByteBuffer buf
|
||||||
for (int i = 0; i < numPieces - 1; i++) {
|
for (int i = 0; i < numPieces - 1; i++) {
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
}
|
}
|
||||||
def lastPieceLength = length - (numPieces - 1) * size
|
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,9 @@ package com.muwire.core.files
|
|||||||
|
|
||||||
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.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
|
||||||
@@ -14,27 +16,31 @@ class FileManager {
|
|||||||
|
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
|
final MuWireSettings settings
|
||||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||||
final SearchIndex index = new SearchIndex()
|
final SearchIndex index = new SearchIndex()
|
||||||
|
|
||||||
FileManager(EventBus eventBus) {
|
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||||
|
this.settings = settings
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFileHashedEvent(FileHashedEvent e) {
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
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())
|
||||||
@@ -100,20 +106,33 @@ 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, searchEvent: e)
|
||||||
} else {
|
} else {
|
||||||
def names = index.search e.searchTerms
|
def names = index.search e.searchTerms
|
||||||
Set<File> files = new HashSet<>()
|
Set<File> files = new HashSet<>()
|
||||||
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, searchEvent: e)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,13 @@ class HasherService {
|
|||||||
|
|
||||||
final FileHasher hasher
|
final FileHasher hasher
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
|
final FileManager fileManager
|
||||||
Executor executor
|
Executor executor
|
||||||
|
|
||||||
HasherService(FileHasher hasher, EventBus eventBus) {
|
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||||
this.hasher = hasher
|
this.hasher = hasher
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
this.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
@@ -22,6 +24,8 @@ class HasherService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileSharedEvent(FileSharedEvent evt) {
|
void onFileSharedEvent(FileSharedEvent evt) {
|
||||||
|
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||||
|
return
|
||||||
executor.execute( { -> process(evt.file) } as Runnable)
|
executor.execute( { -> process(evt.file) } as Runnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,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())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import java.nio.file.CopyOption
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
@@ -34,7 +37,7 @@ class PersisterService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
timer.schedule({load()} as TimerTask, 1000)
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
@@ -55,10 +58,13 @@ class PersisterService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||||
log.log(Level.WARNING, "couldn't load files",e)
|
log.log(Level.WARNING, "couldn't load files",e)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
@@ -94,28 +100,37 @@ 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistFiles() {
|
private void persistFiles() {
|
||||||
location.delete()
|
|
||||||
def sharedFiles = fileManager.getSharedFiles()
|
def sharedFiles = fileManager.getSharedFiles()
|
||||||
location.withPrintWriter { writer ->
|
|
||||||
|
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||||
|
tmp.deleteOnExit()
|
||||||
|
tmp.withPrintWriter { writer ->
|
||||||
sharedFiles.each { k, v ->
|
sharedFiles.each { k, v ->
|
||||||
def json = toJson(k,v)
|
def json = toJson(k,v)
|
||||||
json = JsonOutput.toJson(json)
|
json = JsonOutput.toJson(json)
|
||||||
writer.println json
|
writer.println json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
tmp.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
private def toJson(File f, SharedFile sf) {
|
private def toJson(File f, SharedFile sf) {
|
||||||
@@ -124,6 +139,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++) {
|
||||||
|
@@ -140,7 +140,7 @@ class CacheClient {
|
|||||||
pong.pongs.asList().each {
|
pong.pongs.asList().each {
|
||||||
Destination dest = new Destination(it)
|
Destination dest = new Destination(it)
|
||||||
if (!session.getMyDestination().equals(dest))
|
if (!session.getMyDestination().equals(dest))
|
||||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -30,4 +30,8 @@ class Host {
|
|||||||
synchronized boolean hasSucceeded() {
|
synchronized boolean hasSucceeded() {
|
||||||
successes > 0
|
successes > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void clearFailures() {
|
||||||
|
failures = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,8 +46,12 @@ class HostCache extends Service {
|
|||||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||||
if (myself == e.destination)
|
if (myself == e.destination)
|
||||||
return
|
return
|
||||||
if (hosts.containsKey(e.destination))
|
if (hosts.containsKey(e.destination)) {
|
||||||
return
|
if (!e.fromHostcache)
|
||||||
|
return
|
||||||
|
hosts.get(e.destination).clearFailures()
|
||||||
|
return
|
||||||
|
}
|
||||||
Host host = new Host(e.destination)
|
Host host = new Host(e.destination)
|
||||||
if (allowHost(host)) {
|
if (allowHost(host)) {
|
||||||
hosts.put(e.destination, host)
|
hosts.put(e.destination, host)
|
||||||
|
@@ -7,9 +7,10 @@ import net.i2p.data.Destination
|
|||||||
class HostDiscoveredEvent extends Event {
|
class HostDiscoveredEvent extends Event {
|
||||||
|
|
||||||
Destination destination
|
Destination destination
|
||||||
|
boolean fromHostcache
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()}"
|
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class ResultsEvent extends Event {
|
class ResultsEvent extends Event {
|
||||||
|
|
||||||
|
SearchEvent searchEvent
|
||||||
SharedFile[] results
|
SharedFile[] results
|
||||||
UUID uuid
|
UUID uuid
|
||||||
}
|
}
|
||||||
|
@@ -12,8 +12,19 @@ class ResultsParser {
|
|||||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||||
if (json.type != "Result")
|
if (json.type != "Result")
|
||||||
throw new InvalidSearchResultException("not a result json")
|
throw new InvalidSearchResultException("not a result json")
|
||||||
if (json.version != 1)
|
switch(json.version) {
|
||||||
throw new InvalidSearchResultException("unknown version $json.version")
|
case 1:
|
||||||
|
return parseV1(p, uuid, json)
|
||||||
|
case 2:
|
||||||
|
return parseV2(p, uuid, json)
|
||||||
|
default:
|
||||||
|
throw new InvalidSearchResultException("unknown version $json.version")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseV1(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
if (json.size == null)
|
if (json.size == null)
|
||||||
@@ -52,4 +63,33 @@ class ResultsParser {
|
|||||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||||
|
if (json.name == null)
|
||||||
|
throw new InvalidSearchResultException("name missing")
|
||||||
|
if (json.size == null)
|
||||||
|
throw new InvalidSearchResultException("length missing")
|
||||||
|
if (json.infohash == null)
|
||||||
|
throw new InvalidSearchResultException("infohash missing")
|
||||||
|
if (json.pieceSize == null)
|
||||||
|
throw new InvalidSearchResultException("pieceSize missing")
|
||||||
|
if (json.hashList != null)
|
||||||
|
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||||
|
try {
|
||||||
|
String name = DataUtil.readi18nString(Base64.decode(json.name))
|
||||||
|
long size = json.size
|
||||||
|
byte [] infoHash = Base64.decode(json.infohash)
|
||||||
|
if (infoHash.length != InfoHash.SIZE)
|
||||||
|
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
|
||||||
|
int pieceSize = json.pieceSize
|
||||||
|
return new UIResultEvent( sender : p,
|
||||||
|
name : name,
|
||||||
|
size : size,
|
||||||
|
infohash : new InfoHash(infoHash),
|
||||||
|
pieceSize : pieceSize,
|
||||||
|
uuid: uuid)
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,22 +46,26 @@ class ResultsSender {
|
|||||||
this.me = me
|
this.me = me
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendResults(UUID uuid, SharedFile[] results, Destination target) {
|
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()}")
|
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
|
||||||
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)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executor.execute(new ResultSendJob(uuid : uuid, results : results, target: target))
|
executor.execute(new ResultSendJob(uuid : uuid, results : results,
|
||||||
|
target: target, oobInfohash : oobInfohash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ class ResultsSender {
|
|||||||
UUID uuid
|
UUID uuid
|
||||||
SharedFile [] results
|
SharedFile [] results
|
||||||
Destination target
|
Destination target
|
||||||
|
boolean oobInfohash
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -91,19 +96,20 @@ class ResultsSender {
|
|||||||
String encodedName = Base64.encode(baos.toByteArray())
|
String encodedName = Base64.encode(baos.toByteArray())
|
||||||
def obj = [:]
|
def obj = [:]
|
||||||
obj.type = "Result"
|
obj.type = "Result"
|
||||||
obj.version = 1
|
obj.version = oobInfohash ? 2 : 1
|
||||||
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()
|
if (!oobInfohash) {
|
||||||
def hashListB64 = []
|
byte [] hashList = it.getInfoHash().getHashList()
|
||||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
def hashListB64 = []
|
||||||
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||||
hashListB64 << Base64.encode(tmp)
|
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||||
|
hashListB64 << Base64.encode(tmp)
|
||||||
|
}
|
||||||
|
obj.hashList = hashListB64
|
||||||
}
|
}
|
||||||
obj.hashList = hashListB64
|
|
||||||
|
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
os.writeShort((short)json.length())
|
os.writeShort((short)json.length())
|
||||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||||
|
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ public class SearchManager {
|
|||||||
log.info("No results for search uuid $event.uuid")
|
log.info("No results for search uuid $event.uuid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultsSender.sendResults(event.uuid, event.results, target)
|
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasLocalSearch(UUID uuid) {
|
boolean hasLocalSearch(UUID uuid) {
|
||||||
|
@@ -11,4 +11,9 @@ class UIResultEvent extends Event {
|
|||||||
long size
|
long size
|
||||||
InfoHash infohash
|
InfoHash infohash
|
||||||
int pieceSize
|
int pieceSize
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
package com.muwire.core.update
|
package com.muwire.core.update
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
|
||||||
class UpdateAvailableEvent extends Event {
|
class UpdateAvailableEvent extends Event {
|
||||||
String version
|
String version
|
||||||
String signer
|
String signer
|
||||||
|
String infoHash
|
||||||
}
|
}
|
||||||
|
@@ -36,7 +36,7 @@ class UpdateClient {
|
|||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||||
timer.schedule({checkUpdate()} as TimerTask, 30000, 60 * 60 * 1000)
|
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
@@ -107,7 +107,7 @@ class UpdateClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info("new version $payload.version available, publishing event")
|
log.info("new version $payload.version available, publishing event")
|
||||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer))
|
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"Invalid datagram",e)
|
log.log(Level.WARNING,"Invalid datagram",e)
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class ContentRequest extends Request {
|
||||||
|
Range range
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
class ContentUploader extends Uploader {
|
||||||
|
|
||||||
|
private final File file
|
||||||
|
private final ContentRequest request
|
||||||
|
|
||||||
|
ContentUploader(File file, ContentRequest request, Endpoint endpoint) {
|
||||||
|
super(endpoint)
|
||||||
|
this.file = file
|
||||||
|
this.request = request
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
Range range = request.getRange()
|
||||||
|
if (range.start >= file.length() || range.end >= file.length()) {
|
||||||
|
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
FileChannel channel
|
||||||
|
try {
|
||||||
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||||
|
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||||
|
byte [] tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {channel?.close() } catch (IOException ignored) {}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class HashListRequest extends Request {
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
class HashListUploader extends Uploader {
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final HashListRequest request
|
||||||
|
|
||||||
|
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
||||||
|
super(endpoint)
|
||||||
|
this.infoHash = infoHash
|
||||||
|
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||||
|
}
|
||||||
|
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
byte[]tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
}
|
||||||
|
}
|
@@ -16,57 +16,12 @@ class Request {
|
|||||||
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
||||||
|
|
||||||
InfoHash infoHash
|
InfoHash infoHash
|
||||||
Range range
|
|
||||||
Persona downloader
|
Persona downloader
|
||||||
Map<String, String> headers
|
Map<String, String> headers
|
||||||
|
|
||||||
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
|
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
Map<String,String> headers = new HashMap<>()
|
|
||||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
Map<String, String> headers = parseHeaders(is)
|
||||||
while(headers.size() < Constants.MAX_HEADERS) {
|
|
||||||
boolean r = false
|
|
||||||
boolean n = false
|
|
||||||
int idx = 0
|
|
||||||
while (true) {
|
|
||||||
byte read = is.read()
|
|
||||||
if (read == -1)
|
|
||||||
throw new IOException("Stream closed")
|
|
||||||
|
|
||||||
if (!r && read == N)
|
|
||||||
throw new IOException("Received N before R")
|
|
||||||
if (read == R) {
|
|
||||||
if (r)
|
|
||||||
throw new IOException("double R")
|
|
||||||
r = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r && !n) {
|
|
||||||
if (read != N)
|
|
||||||
throw new IOException("R not followed by N")
|
|
||||||
n = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (idx == 0x1 << 14)
|
|
||||||
throw new IOException("Header too long")
|
|
||||||
tmp[idx++] = read
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idx == 0)
|
|
||||||
break
|
|
||||||
|
|
||||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
|
||||||
log.fine("Read header $header")
|
|
||||||
|
|
||||||
int keyIdx = header.indexOf(":")
|
|
||||||
if (keyIdx < 1)
|
|
||||||
throw new IOException("Header key not found")
|
|
||||||
if (keyIdx == header.length())
|
|
||||||
throw new IOException("Header value not found")
|
|
||||||
String key = header.substring(0, keyIdx)
|
|
||||||
String value = header.substring(keyIdx + 1)
|
|
||||||
headers.put(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!headers.containsKey("Range"))
|
if (!headers.containsKey("Range"))
|
||||||
throw new IOException("Range header not found")
|
throw new IOException("Range header not found")
|
||||||
@@ -93,7 +48,69 @@ class Request {
|
|||||||
def decoded = Base64.decode(encoded)
|
def decoded = Base64.decode(encoded)
|
||||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
}
|
}
|
||||||
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
|
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||||
|
headers : headers, downloader : downloader)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
|
Map<String,String> headers = parseHeaders(is)
|
||||||
|
Persona downloader = null
|
||||||
|
if (headers.containsKey("X-Persona")) {
|
||||||
|
def encoded = headers["X-Persona"].trim()
|
||||||
|
def decoded = Base64.decode(encoded)
|
||||||
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
|
}
|
||||||
|
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseHeaders(InputStream is) {
|
||||||
|
Map<String,String> headers = new HashMap<>()
|
||||||
|
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||||
|
while(headers.size() < Constants.MAX_HEADERS) {
|
||||||
|
boolean r = false
|
||||||
|
boolean n = false
|
||||||
|
int idx = 0
|
||||||
|
while (true) {
|
||||||
|
byte read = is.read()
|
||||||
|
if (read == -1)
|
||||||
|
throw new IOException("Stream closed")
|
||||||
|
|
||||||
|
if (!r && read == N)
|
||||||
|
throw new IOException("Received N before R")
|
||||||
|
if (read == R) {
|
||||||
|
if (r)
|
||||||
|
throw new IOException("double R")
|
||||||
|
r = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r && !n) {
|
||||||
|
if (read != N)
|
||||||
|
throw new IOException("R not followed by N")
|
||||||
|
n = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (idx == 0x1 << 14)
|
||||||
|
throw new IOException("Header too long")
|
||||||
|
tmp[idx++] = read
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx == 0)
|
||||||
|
break
|
||||||
|
|
||||||
|
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||||
|
log.fine("Read header $header")
|
||||||
|
|
||||||
|
int keyIdx = header.indexOf(":")
|
||||||
|
if (keyIdx < 1)
|
||||||
|
throw new IOException("Header key not found")
|
||||||
|
if (keyIdx == header.length())
|
||||||
|
throw new IOException("Header value not found")
|
||||||
|
String key = header.substring(0, keyIdx)
|
||||||
|
String value = header.substring(keyIdx + 1)
|
||||||
|
headers.put(key, value)
|
||||||
|
}
|
||||||
|
headers
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -23,9 +23,9 @@ public class UploadManager {
|
|||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processEndpoint(Endpoint e) throws IOException {
|
public void processGET(Endpoint e) throws IOException {
|
||||||
byte [] infoHashStringBytes = new byte[44]
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
boolean first = true
|
boolean first = true
|
||||||
while(true) {
|
while(true) {
|
||||||
if (first)
|
if (first)
|
||||||
@@ -61,13 +61,13 @@ public class UploadManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
|
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
log.info("Downloader persona doesn't match their destination")
|
log.info("Downloader persona doesn't match their destination")
|
||||||
e.close()
|
e.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
|
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
@@ -75,7 +75,92 @@ public class UploadManager {
|
|||||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processHashList(Endpoint e) {
|
||||||
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to hashlist request for root $infoHashString")
|
||||||
|
|
||||||
|
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
byte [] rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed HASHLIST header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
|
||||||
|
// proceed with content
|
||||||
|
while(true) {
|
||||||
|
byte[] get = new byte[4]
|
||||||
|
dis.readFully(get)
|
||||||
|
if (get != "GET ".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("received a method other than GET on subsequent call")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to upload request for root $infoHashString")
|
||||||
|
|
||||||
|
infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed GET header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,49 +8,16 @@ import java.nio.file.StandardOpenOption
|
|||||||
|
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
class Uploader {
|
abstract class Uploader {
|
||||||
private final File file
|
protected final Endpoint endpoint
|
||||||
private final Request request
|
protected ByteBuffer mapped
|
||||||
private final Endpoint endpoint
|
|
||||||
private ByteBuffer mapped
|
|
||||||
|
|
||||||
Uploader(File file, Request request, Endpoint endpoint) {
|
Uploader(Endpoint endpoint) {
|
||||||
this.file = file
|
|
||||||
this.request = request
|
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
void respond() {
|
abstract void respond()
|
||||||
OutputStream os = endpoint.getOutputStream()
|
|
||||||
Range range = request.getRange()
|
|
||||||
if (range.start >= file.length() || range.end >= file.length()) {
|
|
||||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.flush()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
|
||||||
byte [] tmp = new byte[0x1 << 13]
|
|
||||||
while(mapped.hasRemaining()) {
|
|
||||||
int start = mapped.position()
|
|
||||||
synchronized(this) {
|
|
||||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
|
||||||
}
|
|
||||||
int read = mapped.position() - start
|
|
||||||
endpoint.getOutputStream().write(tmp, 0, read)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {channel?.close() } catch (IOException ignored) {}
|
|
||||||
endpoint.getOutputStream().flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized int getPosition() {
|
public synchronized int getPosition() {
|
||||||
if (mapped == null)
|
if (mapped == null)
|
||||||
return -1
|
return -1
|
||||||
|
@@ -55,21 +55,21 @@ class JULLog extends Log {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldDebug() {
|
public boolean shouldDebug() {
|
||||||
level.intValue().intValue() >= Level.FINE.intValue()
|
level.intValue().intValue() <= Level.FINE.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldInfo() {
|
public boolean shouldInfo() {
|
||||||
level.intValue().intValue() >= Level.INFO.intValue()
|
level.intValue().intValue() <= Level.INFO.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWarn() {
|
public boolean shouldWarn() {
|
||||||
level.intValue().intValue() >= Level.WARNING.intValue()
|
level.intValue().intValue() <= Level.WARNING.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldError() {
|
public boolean shouldError() {
|
||||||
level.intValue().intValue() >= Level.SEVERE.intValue()
|
level.intValue().intValue() <= Level.SEVERE.intValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
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,
|
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.
|
||||||
@@ -175,6 +181,8 @@ Search results are sent through and HTTP POST method from the responder to the o
|
|||||||
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
|
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
|
||||||
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
|
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
|
||||||
|
|
||||||
|
Results version 1 contain the full hashlist, version 2 does not contain that list. See the "infohash-upgrade" document for more information.
|
||||||
|
|
||||||
### "Who do you trust" query - any node to any node
|
### "Who do you trust" query - any node to any node
|
||||||
(See the "web-of-trust" document for more info on this query)
|
(See the "web-of-trust" document for more info on this query)
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.0.8
|
version = 0.1.11
|
||||||
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
|
||||||
|
@@ -7,12 +7,15 @@ import griffon.core.mvc.MVCGroup
|
|||||||
import griffon.core.mvc.MVCGroupConfiguration
|
import griffon.core.mvc.MVCGroupConfiguration
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.download.DownloadStartedEvent
|
import com.muwire.core.download.DownloadStartedEvent
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
@@ -42,15 +45,38 @@ class MainFrameController {
|
|||||||
params["uuid"] = uuid.toString()
|
params["uuid"] = uuid.toString()
|
||||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||||
model.results[uuid.toString()] = group
|
model.results[uuid.toString()] = group
|
||||||
|
|
||||||
// this can be improved a lot
|
def searchEvent
|
||||||
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
|
if (model.hashSearch) {
|
||||||
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
|
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
|
||||||
|
} else {
|
||||||
|
// this can be improved a lot
|
||||||
|
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||||
|
def terms = replaced.split(" ")
|
||||||
|
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
|
||||||
|
}
|
||||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||||
originator : core.me))
|
originator : core.me))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void search(String infoHash, String tabTitle) {
|
||||||
|
def cardsPanel = builder.getVariable("cards-panel")
|
||||||
|
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||||
|
def uuid = UUID.randomUUID()
|
||||||
|
Map<String, Object> params = new HashMap<>()
|
||||||
|
params["search-terms"] = tabTitle
|
||||||
|
params["uuid"] = uuid.toString()
|
||||||
|
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||||
|
model.results[uuid.toString()] = group
|
||||||
|
|
||||||
|
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
|
||||||
|
oobInfohash: true)
|
||||||
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||||
|
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||||
|
originator : core.me))
|
||||||
|
}
|
||||||
|
|
||||||
private def selectedResult() {
|
private def selectedResult() {
|
||||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||||
def group = selected.getClientProperty("mvc-group")
|
def group = selected.getClientProperty("mvc-group")
|
||||||
@@ -58,12 +84,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
|
||||||
@@ -100,13 +133,14 @@ class MainFrameController {
|
|||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void cancel() {
|
void cancel() {
|
||||||
def downloader = selectedDownload()
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
downloader.cancel()
|
downloader.cancel()
|
||||||
|
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void resume() {
|
void resume() {
|
||||||
def downloader = selectedDownload()
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
downloader.resume()
|
downloader.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +171,20 @@ class MainFrameController {
|
|||||||
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void keywordSearch() {
|
||||||
|
model.hashSearch = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void hashSearch() {
|
||||||
|
model.hashSearch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
void unshareSelectedFiles() {
|
||||||
|
println "unsharing selected files"
|
||||||
|
}
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
application.addPropertyChangeListener("core", {e->
|
application.addPropertyChangeListener("core", {e->
|
||||||
core = e.getNewValue()
|
core = e.getNewValue()
|
||||||
|
@@ -6,6 +6,8 @@ import griffon.inject.MVCMember
|
|||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
@ArtifactProviderFor(GriffonController)
|
@ArtifactProviderFor(GriffonController)
|
||||||
class OptionsController {
|
class OptionsController {
|
||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
@@ -15,21 +17,87 @@ class OptionsController {
|
|||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void save() {
|
void save() {
|
||||||
String text = view.retryField.text
|
String text
|
||||||
model.downloadRetryInterval = text
|
Core core = application.context.get("core")
|
||||||
|
|
||||||
|
def i2pProps = core.i2pOptions
|
||||||
|
|
||||||
|
text = view.inboundLengthField.text
|
||||||
|
model.inboundLength = text
|
||||||
|
i2pProps["inbound.length"] = text
|
||||||
|
|
||||||
|
text = view.inboundQuantityField.text
|
||||||
|
model.inboundQuantity = text
|
||||||
|
i2pProps["inbound.quantity"] = text
|
||||||
|
|
||||||
|
text = view.outboundQuantityField.text
|
||||||
|
model.outboundQuantity = text
|
||||||
|
i2pProps["outbound.quantity"] = text
|
||||||
|
|
||||||
|
text = view.outboundLengthField.text
|
||||||
|
model.outboundLength = text
|
||||||
|
i2pProps["outbound.length"] = text
|
||||||
|
|
||||||
|
File i2pSettingsFile = new File(core.home, "i2p.properties")
|
||||||
|
i2pSettingsFile.withOutputStream {
|
||||||
|
i2pProps.store(it,"")
|
||||||
|
}
|
||||||
|
|
||||||
|
text = view.retryField.text
|
||||||
|
model.downloadRetryInterval = text
|
||||||
|
|
||||||
def settings = application.context.get("muwire-settings")
|
def settings = application.context.get("muwire-settings")
|
||||||
settings.downloadRetryInterval = Integer.valueOf(text)
|
settings.downloadRetryInterval = Integer.valueOf(text)
|
||||||
|
|
||||||
text = view.updateField.text
|
text = view.updateField.text
|
||||||
model.updateCheckInterval = text
|
model.updateCheckInterval = text
|
||||||
settings.updateCheckInterval = Integer.valueOf(text)
|
settings.updateCheckInterval = Integer.valueOf(text)
|
||||||
|
|
||||||
File settingsFile = new File(application.context.get("core").home, "MuWire.properties")
|
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||||
|
model.onlyTrusted = onlyTrusted
|
||||||
|
settings.setAllowUntrusted(!onlyTrusted)
|
||||||
|
|
||||||
|
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
||||||
|
model.shareDownloadedFiles = shareDownloaded
|
||||||
|
settings.shareDownloadedFiles = shareDownloaded
|
||||||
|
|
||||||
|
File settingsFile = new File(core.home, "MuWire.properties")
|
||||||
settingsFile.withOutputStream {
|
settingsFile.withOutputStream {
|
||||||
settings.write(it)
|
settings.write(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI Setttings
|
||||||
|
|
||||||
|
UISettings uiSettings = application.context.get("ui-settings")
|
||||||
|
text = view.lnfField.text
|
||||||
|
model.lnf = text
|
||||||
|
uiSettings.lnf = text
|
||||||
|
|
||||||
|
text = view.fontField.text
|
||||||
|
model.font = text
|
||||||
|
uiSettings.font = text
|
||||||
|
|
||||||
|
boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||||
|
model.showMonitor = showMonitor
|
||||||
|
uiSettings.showMonitor = showMonitor
|
||||||
|
|
||||||
|
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||||
|
model.clearCancelledDownloads = clearCancelledDownloads
|
||||||
|
uiSettings.clearCancelledDownloads = clearCancelledDownloads
|
||||||
|
|
||||||
|
boolean clearFinishedDownloads = view.clearFinishedDownloadsCheckbox.model.isSelected()
|
||||||
|
model.clearFinishedDownloads = clearFinishedDownloads
|
||||||
|
uiSettings.clearFinishedDownloads = clearFinishedDownloads
|
||||||
|
|
||||||
|
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
|
||||||
|
model.excludeLocalResult = excludeLocalResult
|
||||||
|
uiSettings.excludeLocalResult = excludeLocalResult
|
||||||
|
|
||||||
|
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||||
|
uiSettingsFile.withOutputStream {
|
||||||
|
uiSettings.write(it)
|
||||||
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,17 +1,25 @@
|
|||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
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.gui.UISettings
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.swing.JTable
|
||||||
|
import javax.swing.LookAndFeel
|
||||||
|
import javax.swing.UIManager
|
||||||
|
|
||||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||||
|
|
||||||
|
import java.awt.Font
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class Initialize extends AbstractLifecycleHandler {
|
class Initialize extends AbstractLifecycleHandler {
|
||||||
@Inject
|
@Inject
|
||||||
@@ -21,11 +29,87 @@ class Initialize extends AbstractLifecycleHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void execute() {
|
void execute() {
|
||||||
if (isMacOSX()) {
|
log.info "Loading home dir"
|
||||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
def portableHome = System.getProperty("portable.home")
|
||||||
} else {
|
def home = portableHome == null ?
|
||||||
lookAndFeel('system', 'gtk')
|
selectHome() :
|
||||||
|
portableHome
|
||||||
|
|
||||||
|
home = new File(home)
|
||||||
|
if (!home.exists()) {
|
||||||
|
log.info("creating home dir $home")
|
||||||
|
home.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application.context.put("muwire-home", home.getAbsolutePath())
|
||||||
|
|
||||||
|
def guiPropsFile = new File(home, "gui.properties")
|
||||||
|
UISettings uiSettings
|
||||||
|
if (guiPropsFile.exists()) {
|
||||||
|
Properties props = new Properties()
|
||||||
|
guiPropsFile.withInputStream { props.load(it) }
|
||||||
|
uiSettings = new UISettings(props)
|
||||||
|
|
||||||
|
log.info("settting user-specified lnf $uiSettings.lnf")
|
||||||
|
try {
|
||||||
|
lookAndFeel(uiSettings.lnf)
|
||||||
|
} catch (Throwable bad) {
|
||||||
|
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad)
|
||||||
|
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiSettings.font != null) {
|
||||||
|
log.info("setting user-specified font $uiSettings.font")
|
||||||
|
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
|
||||||
|
def defaults = UIManager.getDefaults()
|
||||||
|
defaults.put("Button.font", font)
|
||||||
|
defaults.put("RadioButton.font", font)
|
||||||
|
defaults.put("Label.font", font)
|
||||||
|
defaults.put("CheckBox.font", font)
|
||||||
|
defaults.put("Table.font", font)
|
||||||
|
defaults.put("TableHeader.font", font)
|
||||||
|
// TODO: add others
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Properties props = new Properties()
|
||||||
|
uiSettings = new UISettings(props)
|
||||||
|
log.info "will try default lnfs"
|
||||||
|
if (isMacOSX()) {
|
||||||
|
if (SystemVersion.isJava9()) {
|
||||||
|
uiSettings.lnf = "metal"
|
||||||
|
lookAndFeel("metal")
|
||||||
|
} else {
|
||||||
|
uiSettings.lnf = "nimbus"
|
||||||
|
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||||
|
uiSettings.lnf = chosen.getID()
|
||||||
|
log.info("ended up applying $chosen.name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application.context.put("ui-settings", uiSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
@@ -32,17 +34,8 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
@Override
|
@Override
|
||||||
void execute() {
|
void execute() {
|
||||||
log.info "starting core services"
|
log.info "starting core services"
|
||||||
def portableHome = System.getProperty("portable.home")
|
|
||||||
def home = portableHome == null ?
|
|
||||||
System.getProperty("user.home") + File.separator + ".MuWire" :
|
|
||||||
portableHome
|
|
||||||
|
|
||||||
home = new File(home)
|
|
||||||
if (!home.exists()) {
|
|
||||||
log.info("creating home dir")
|
|
||||||
home.mkdir()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def home = new File(application.getContext().getAsString("muwire-home"))
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
def propsFile = new File(home, "MuWire.properties")
|
def propsFile = new File(home, "MuWire.properties")
|
||||||
if (propsFile.exists()) {
|
if (propsFile.exists()) {
|
||||||
@@ -116,6 +109,8 @@ 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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ import com.muwire.core.connection.ConnectionEvent
|
|||||||
import com.muwire.core.connection.DisconnectionEvent
|
import com.muwire.core.connection.DisconnectionEvent
|
||||||
import com.muwire.core.download.DownloadStartedEvent
|
import com.muwire.core.download.DownloadStartedEvent
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
import com.muwire.core.files.FileLoadedEvent
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
@@ -28,6 +29,7 @@ import com.muwire.core.upload.UploadFinishedEvent
|
|||||||
|
|
||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.core.env.Metadata
|
||||||
import griffon.core.mvc.MVCGroup
|
import griffon.core.mvc.MVCGroup
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.transform.FXObservable
|
import griffon.transform.FXObservable
|
||||||
@@ -37,8 +39,11 @@ import griffon.metadata.ArtifactProviderFor
|
|||||||
|
|
||||||
@ArtifactProviderFor(GriffonModel)
|
@ArtifactProviderFor(GriffonModel)
|
||||||
class MainFrameModel {
|
class MainFrameModel {
|
||||||
|
@Inject Metadata metadata
|
||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
FactoryBuilderSupport builder
|
FactoryBuilderSupport builder
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MainFrameController controller
|
||||||
@Inject @Nonnull GriffonApplication application
|
@Inject @Nonnull GriffonApplication application
|
||||||
@Observable boolean coreInitialized = false
|
@Observable boolean coreInitialized = false
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ class MainFrameModel {
|
|||||||
def trusted = []
|
def trusted = []
|
||||||
def distrusted = []
|
def distrusted = []
|
||||||
|
|
||||||
|
boolean hashSearch
|
||||||
|
|
||||||
@Observable int connections
|
@Observable int connections
|
||||||
@Observable String me
|
@Observable String me
|
||||||
@Observable boolean searchButtonsEnabled
|
@Observable boolean searchButtonsEnabled
|
||||||
@@ -72,11 +79,28 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String, Object> args) {
|
void mvcGroupInit(Map<String, Object> args) {
|
||||||
|
|
||||||
|
UISettings uiSettings = application.context.get("ui-settings")
|
||||||
|
|
||||||
Timer timer = new Timer("download-pumper", true)
|
Timer timer = new Timer("download-pumper", true)
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
if (!mvcGroup.alive)
|
if (!mvcGroup.alive)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// remove cancelled or finished downloads
|
||||||
|
def toRemove = []
|
||||||
|
downloads.each {
|
||||||
|
if (uiSettings.clearCancelledDownloads &&
|
||||||
|
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
|
||||||
|
toRemove << it
|
||||||
|
if (uiSettings.clearFinishedDownloads &&
|
||||||
|
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
|
||||||
|
toRemove << it
|
||||||
|
}
|
||||||
|
toRemove.each {
|
||||||
|
downloads.remove(it)
|
||||||
|
}
|
||||||
|
|
||||||
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
||||||
|
|
||||||
updateTablePreservingSelection("downloads-table")
|
updateTablePreservingSelection("downloads-table")
|
||||||
@@ -100,6 +124,7 @@ class MainFrameModel {
|
|||||||
core.eventBus.register(TrustEvent.class, this)
|
core.eventBus.register(TrustEvent.class, this)
|
||||||
core.eventBus.register(QueryEvent.class, this)
|
core.eventBus.register(QueryEvent.class, this)
|
||||||
core.eventBus.register(UpdateAvailableEvent.class, this)
|
core.eventBus.register(UpdateAvailableEvent.class, this)
|
||||||
|
core.eventBus.register(FileDownloadedEvent.class, this)
|
||||||
|
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||||
@@ -110,7 +135,9 @@ class MainFrameModel {
|
|||||||
lastRetryTime = now
|
lastRetryTime = now
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
downloads.each {
|
downloads.each {
|
||||||
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
|
def state = it.downloader.currentState
|
||||||
|
if (state == Downloader.DownloadState.FAILED ||
|
||||||
|
state == Downloader.DownloadState.DOWNLOADING)
|
||||||
it.downloader.resume()
|
it.downloader.resume()
|
||||||
updateTablePreservingSelection("downloads-table")
|
updateTablePreservingSelection("downloads-table")
|
||||||
}
|
}
|
||||||
@@ -256,7 +283,24 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
JOptionPane.showMessageDialog(null, "A new version of MuWire is available from $e.signer. Please update to $e.version")
|
|
||||||
|
int option = JOptionPane.showConfirmDialog(null,
|
||||||
|
"MuWire $e.version is available from $e.signer. You have "+ metadata["application.version"]+" Update?",
|
||||||
|
"New MuWire version availble", JOptionPane.OK_CANCEL_OPTION)
|
||||||
|
if (option == JOptionPane.CANCEL_OPTION)
|
||||||
|
return
|
||||||
|
controller.search(e.infoHash,"MuWire update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
|
if (!core.muOptions.shareDownloadedFiles)
|
||||||
|
return
|
||||||
|
infoHashes.add(e.downloadedFile.infoHash)
|
||||||
|
runInsideUIAsync {
|
||||||
|
shared << e.downloadedFile
|
||||||
|
JTable table = builder.getVariable("shared-files-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,8 @@
|
|||||||
package com.muwire.gui
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
import griffon.transform.Observable
|
import griffon.transform.Observable
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
@@ -8,9 +11,42 @@ import griffon.metadata.ArtifactProviderFor
|
|||||||
class OptionsModel {
|
class OptionsModel {
|
||||||
@Observable String downloadRetryInterval
|
@Observable String downloadRetryInterval
|
||||||
@Observable String updateCheckInterval
|
@Observable String updateCheckInterval
|
||||||
|
@Observable boolean onlyTrusted
|
||||||
|
@Observable boolean shareDownloadedFiles
|
||||||
|
|
||||||
|
// i2p options
|
||||||
|
@Observable String inboundLength
|
||||||
|
@Observable String inboundQuantity
|
||||||
|
@Observable String outboundLength
|
||||||
|
@Observable String outboundQuantity
|
||||||
|
|
||||||
|
// gui options
|
||||||
|
@Observable boolean showMonitor
|
||||||
|
@Observable String lnf
|
||||||
|
@Observable String font
|
||||||
|
@Observable boolean clearCancelledDownloads
|
||||||
|
@Observable boolean clearFinishedDownloads
|
||||||
|
@Observable boolean excludeLocalResult
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
downloadRetryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
MuWireSettings settings = application.context.get("muwire-settings")
|
||||||
updateCheckInterval = application.context.get("muwire-settings").updateCheckInterval
|
downloadRetryInterval = settings.downloadRetryInterval
|
||||||
|
updateCheckInterval = settings.updateCheckInterval
|
||||||
|
onlyTrusted = !settings.allowUntrusted()
|
||||||
|
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||||
|
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
inboundLength = core.i2pOptions["inbound.length"]
|
||||||
|
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||||
|
outboundLength = core.i2pOptions["outbound.length"]
|
||||||
|
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||||
|
|
||||||
|
UISettings uiSettings = application.context.get("ui-settings")
|
||||||
|
showMonitor = uiSettings.showMonitor
|
||||||
|
lnf = uiSettings.lnf
|
||||||
|
font = uiSettings.font
|
||||||
|
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||||
|
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||||
|
excludeLocalResult = uiSettings.excludeLocalResult
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -19,6 +19,7 @@ class SearchTabModel {
|
|||||||
FactoryBuilderSupport builder
|
FactoryBuilderSupport builder
|
||||||
|
|
||||||
Core core
|
Core core
|
||||||
|
UISettings uiSettings
|
||||||
String uuid
|
String uuid
|
||||||
def results = []
|
def results = []
|
||||||
def hashBucket = [:]
|
def hashBucket = [:]
|
||||||
@@ -26,6 +27,7 @@ class SearchTabModel {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
core = mvcGroup.parentGroup.model.core
|
core = mvcGroup.parentGroup.model.core
|
||||||
|
uiSettings = application.context.get("ui-settings")
|
||||||
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
|
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,9 @@ class SearchTabModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleResult(UIResultEvent e) {
|
void handleResult(UIResultEvent e) {
|
||||||
|
if (uiSettings.excludeLocalResult &&
|
||||||
|
e.sender == core.me)
|
||||||
|
return
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
def bucket = hashBucket.get(e.infohash)
|
def bucket = hashBucket.get(e.infohash)
|
||||||
if (bucket == null) {
|
if (bucket == null) {
|
||||||
|
@@ -9,10 +9,14 @@ import javax.swing.BorderFactory
|
|||||||
import javax.swing.Box
|
import javax.swing.Box
|
||||||
import javax.swing.BoxLayout
|
import javax.swing.BoxLayout
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.JSplitPane
|
import javax.swing.JSplitPane
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.border.Border
|
import javax.swing.border.Border
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
@@ -24,6 +28,8 @@ import java.awt.FlowLayout
|
|||||||
import java.awt.GridBagConstraints
|
import java.awt.GridBagConstraints
|
||||||
import java.awt.GridBagLayout
|
import java.awt.GridBagLayout
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
@@ -35,7 +41,11 @@ class MainFrameView {
|
|||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
MainFrameModel model
|
MainFrameModel model
|
||||||
|
|
||||||
|
def downloadsTable
|
||||||
|
def lastDownloadSortEvent
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
|
UISettings settings = application.context.get("ui-settings")
|
||||||
builder.with {
|
builder.with {
|
||||||
application(size : [1024,768], id: 'main-frame',
|
application(size : [1024,768], id: 'main-frame',
|
||||||
locationRelativeTo : null,
|
locationRelativeTo : null,
|
||||||
@@ -58,7 +68,8 @@ class MainFrameView {
|
|||||||
gridLayout(rows:1, cols: 2)
|
gridLayout(rows:1, cols: 2)
|
||||||
button(text: "Searches", actionPerformed : showSearchWindow)
|
button(text: "Searches", actionPerformed : showSearchWindow)
|
||||||
button(text: "Uploads", actionPerformed : showUploadsWindow)
|
button(text: "Uploads", actionPerformed : showUploadsWindow)
|
||||||
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
if (settings.showMonitor)
|
||||||
|
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
||||||
button(text: "Trust", actionPerformed : showTrustWindow)
|
button(text: "Trust", actionPerformed : showTrustWindow)
|
||||||
}
|
}
|
||||||
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
||||||
@@ -74,6 +85,12 @@ class MainFrameView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
panel( constraints: BorderLayout.EAST) {
|
panel( constraints: BorderLayout.EAST) {
|
||||||
|
panel {
|
||||||
|
buttonGroup(id : "searchButtonGroup")
|
||||||
|
radioButton(text : "Keywords", selected : true, buttonGroup : searchButtonGroup, keywordSearchAction)
|
||||||
|
radioButton(text : "Hash", selected : false, buttonGroup : searchButtonGroup, hashSearchAction)
|
||||||
|
|
||||||
|
}
|
||||||
button(text: "Search", searchAction)
|
button(text: "Search", searchAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,10 +114,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()
|
||||||
@@ -128,11 +145,10 @@ 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 : Long, read : {row -> row.file.length() })
|
||||||
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +221,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() } )
|
||||||
}
|
}
|
||||||
@@ -220,7 +236,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() } )
|
||||||
}
|
}
|
||||||
@@ -252,8 +268,15 @@ 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
|
if (selectedRow < 0) {
|
||||||
|
model.cancelButtonEnabled = false
|
||||||
|
model.retryButtonEnabled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
def downloader = model.downloads[selectedRow]?.downloader
|
||||||
|
if (downloader == null)
|
||||||
|
return
|
||||||
switch(downloader.getCurrentState()) {
|
switch(downloader.getCurrentState()) {
|
||||||
case Downloader.DownloadState.CONNECTING :
|
case Downloader.DownloadState.CONNECTING :
|
||||||
case Downloader.DownloadState.DOWNLOADING :
|
case Downloader.DownloadState.DOWNLOADING :
|
||||||
@@ -269,8 +292,46 @@ class MainFrameView {
|
|||||||
model.retryButtonEnabled = false
|
model.retryButtonEnabled = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def centerRenderer = new DefaultTableCellRenderer()
|
||||||
|
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||||
|
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||||
|
|
||||||
|
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
||||||
|
|
||||||
|
// shared files table
|
||||||
|
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||||
|
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||||
|
|
||||||
|
JPopupMenu sharedFilesMenu = new JPopupMenu()
|
||||||
|
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
|
||||||
|
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
|
||||||
|
sharedFilesMenu.add(unshareSelectedFiles)
|
||||||
|
sharedFilesTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(sharedFilesMenu, e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(sharedFilesMenu, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
||||||
|
menu.show(event.getComponent(), event.getX(), event.getY())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
@@ -5,8 +5,11 @@ import griffon.inject.MVCMember
|
|||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.JTabbedPane
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
|
|
||||||
@@ -21,8 +24,28 @@ class OptionsView {
|
|||||||
|
|
||||||
def d
|
def d
|
||||||
def p
|
def p
|
||||||
|
def i
|
||||||
|
def u
|
||||||
|
|
||||||
def retryField
|
def retryField
|
||||||
def updateField
|
def updateField
|
||||||
|
def allowUntrustedCheckbox
|
||||||
|
def shareDownloadedCheckbox
|
||||||
|
|
||||||
|
def inboundLengthField
|
||||||
|
def inboundQuantityField
|
||||||
|
def outboundLengthField
|
||||||
|
def outboundQuantityField
|
||||||
|
|
||||||
|
def lnfField
|
||||||
|
def monitorCheckbox
|
||||||
|
def fontField
|
||||||
|
def clearCancelledDownloadsCheckbox
|
||||||
|
def clearFinishedDownloadsCheckbox
|
||||||
|
def excludeLocalResultCheckbox
|
||||||
|
|
||||||
|
def buttonsPanel
|
||||||
|
|
||||||
def mainFrame
|
def mainFrame
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
@@ -38,14 +61,61 @@ class OptionsView {
|
|||||||
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
|
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
|
||||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
|
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 : "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))
|
||||||
|
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
|
||||||
|
|
||||||
|
}
|
||||||
|
i = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||||
|
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
|
||||||
|
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
|
||||||
|
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
|
||||||
|
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
|
||||||
|
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
|
||||||
|
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
|
||||||
|
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
|
||||||
|
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
||||||
|
}
|
||||||
|
u = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||||
|
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
|
||||||
|
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
|
||||||
|
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
|
||||||
|
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
|
||||||
|
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
|
||||||
|
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
|
||||||
|
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
|
||||||
|
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
|
||||||
|
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
|
||||||
|
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
|
||||||
|
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
|
||||||
|
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
|
||||||
|
}
|
||||||
|
buttonsPanel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
||||||
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
|
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mvcGroupInit(Map<String,String> args) {
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
d.getContentPane().add(p)
|
def tabbedPane = new JTabbedPane()
|
||||||
|
tabbedPane.addTab("MuWire", p)
|
||||||
|
tabbedPane.addTab("I2P", i)
|
||||||
|
tabbedPane.addTab("GUI", u)
|
||||||
|
|
||||||
|
JPanel panel = new JPanel()
|
||||||
|
panel.setLayout(new BorderLayout())
|
||||||
|
panel.add(tabbedPane, BorderLayout.CENTER)
|
||||||
|
panel.add(buttonsPanel, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
d.getContentPane().add(panel)
|
||||||
d.pack()
|
d.pack()
|
||||||
d.setLocationRelativeTo(mainFrame)
|
d.setLocationRelativeTo(mainFrame)
|
||||||
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||||
|
@@ -6,12 +6,19 @@ import griffon.inject.MVCMember
|
|||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
import net.i2p.data.DataHelper
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
|
import javax.swing.JComponent
|
||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JTable
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.table.DefaultTableCellRenderer
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
@@ -26,19 +33,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})
|
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: Long, read : {row -> row.size})
|
||||||
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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +70,7 @@ class SearchTabView {
|
|||||||
searchTerms = args["search-terms"]
|
searchTerms = args["search-terms"]
|
||||||
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
|
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
|
||||||
parent.addTab(searchTerms, pane)
|
parent.addTab(searchTerms, pane)
|
||||||
int index = parent.indexOfTab(searchTerms)
|
int index = parent.indexOfComponent(pane)
|
||||||
parent.setSelectedIndex(index)
|
parent.setSelectedIndex(index)
|
||||||
|
|
||||||
def tabPanel
|
def tabPanel
|
||||||
@@ -84,6 +92,19 @@ 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.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||||
|
|
||||||
|
|
||||||
|
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||||
|
|
||||||
|
resultsTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
||||||
|
mvcGroup.parentGroup.controller.download()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
def closeTab = {
|
def closeTab = {
|
||||||
|
29
gui/src/main/groovy/com/muwire/gui/SizeRenderer.groovy
Normal file
29
gui/src/main/groovy/com/muwire/gui/SizeRenderer.groovy
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JTable
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
|
class SizeRenderer extends DefaultTableCellRenderer {
|
||||||
|
SizeRenderer() {
|
||||||
|
setHorizontalAlignment(JLabel.CENTER)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||||
|
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||||
|
Long l = (Long) value
|
||||||
|
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
|
||||||
|
setText(formatted)
|
||||||
|
if (isSelected) {
|
||||||
|
setForeground(table.getSelectionForeground())
|
||||||
|
setBackground(table.getSelectionBackground())
|
||||||
|
} else {
|
||||||
|
setForeground(table.getForeground())
|
||||||
|
setBackground(table.getBackground())
|
||||||
|
}
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
34
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
34
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
class UISettings {
|
||||||
|
|
||||||
|
String lnf
|
||||||
|
boolean showMonitor
|
||||||
|
String font
|
||||||
|
boolean clearCancelledDownloads
|
||||||
|
boolean clearFinishedDownloads
|
||||||
|
boolean excludeLocalResult
|
||||||
|
|
||||||
|
UISettings(Properties props) {
|
||||||
|
lnf = props.getProperty("lnf", "system")
|
||||||
|
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
|
||||||
|
font = props.getProperty("font",null)
|
||||||
|
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
|
||||||
|
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
|
||||||
|
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(OutputStream out) throws IOException {
|
||||||
|
Properties props = new Properties()
|
||||||
|
props.setProperty("lnf", lnf)
|
||||||
|
props.setProperty("showMonitor", String.valueOf(showMonitor))
|
||||||
|
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||||
|
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||||
|
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
|
||||||
|
if (font != null)
|
||||||
|
props.setProperty("font", font)
|
||||||
|
|
||||||
|
|
||||||
|
props.store(out, "UI Properties")
|
||||||
|
}
|
||||||
|
}
|
@@ -1,2 +1,3 @@
|
|||||||
apply plugin : 'application'
|
apply plugin : 'application'
|
||||||
mainClassName = 'com.muwire.hostcache.HostCache'
|
mainClassName = 'com.muwire.hostcache.HostCache'
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
package com.muwire.hostcache
|
package com.muwire.hostcache
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
@Log
|
||||||
class Crawler {
|
class Crawler {
|
||||||
|
|
||||||
final def pinger
|
final def pinger
|
||||||
@@ -22,12 +25,14 @@ class Crawler {
|
|||||||
|
|
||||||
synchronized def handleCrawlerPong(pong, Destination source) {
|
synchronized def handleCrawlerPong(pong, Destination source) {
|
||||||
if (!inFlight.containsKey(source)) {
|
if (!inFlight.containsKey(source)) {
|
||||||
|
log.info("response from host that hasn't been crawled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Host host = inFlight.remove(source)
|
Host host = inFlight.remove(source)
|
||||||
|
|
||||||
if (pong.uuid == null || pong.leafSlots == null || pong.peerSlots == null || pong.peers == null) {
|
if (pong.uuid == null || pong.leafSlots == null || pong.peerSlots == null || pong.peers == null) {
|
||||||
hostPool.fail(host)
|
hostPool.fail(host)
|
||||||
|
log.info("invalid crawler pong")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +45,7 @@ class Crawler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!uuid.equals(currentUUID)) {
|
if (!uuid.equals(currentUUID)) {
|
||||||
|
log.info("uuid mismatch")
|
||||||
hostPool.fail(host)
|
hostPool.fail(host)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -50,7 +56,9 @@ class Crawler {
|
|||||||
def peers
|
def peers
|
||||||
try {
|
try {
|
||||||
peers = pong.peers.stream().map({b64 -> new Destination(b64)}).collect(Collectors.toSet())
|
peers = pong.peers.stream().map({b64 -> new Destination(b64)}).collect(Collectors.toSet())
|
||||||
|
log.info("received ${peers.size()} peers")
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING,"couldn't parse peers", e)
|
||||||
hostPool.fail(host)
|
hostPool.fail(host)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
package com.muwire.hostcache
|
package com.muwire.hostcache
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
import net.i2p.client.I2PClientFactory
|
import net.i2p.client.I2PClientFactory
|
||||||
import net.i2p.client.I2PSession
|
import net.i2p.client.I2PSession
|
||||||
import net.i2p.client.I2PSessionMuxedListener
|
import net.i2p.client.I2PSessionMuxedListener
|
||||||
@@ -12,6 +14,7 @@ import net.i2p.client.datagram.I2PDatagramMaker
|
|||||||
import net.i2p.util.SystemVersion
|
import net.i2p.util.SystemVersion
|
||||||
import net.i2p.data.*
|
import net.i2p.data.*
|
||||||
|
|
||||||
|
@Log
|
||||||
public class HostCache {
|
public class HostCache {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@@ -53,7 +56,7 @@ public class HostCache {
|
|||||||
myDest = session.getMyDestination()
|
myDest = session.getMyDestination()
|
||||||
|
|
||||||
// initialize hostpool and crawler
|
// initialize hostpool and crawler
|
||||||
HostPool hostPool = new HostPool(3, 60 * 1000 * 1000)
|
HostPool hostPool = new HostPool(3, 60 * 60 * 1000)
|
||||||
Pinger pinger = new Pinger(session)
|
Pinger pinger = new Pinger(session)
|
||||||
Crawler crawler = new Crawler(pinger, hostPool, 5)
|
Crawler crawler = new Crawler(pinger, hostPool, 5)
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ public class HostCache {
|
|||||||
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
|
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
|
||||||
I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
|
I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
|
||||||
session.connect()
|
session.connect()
|
||||||
println "INFO: connected, going to sleep"
|
log.info("connected, going to sleep")
|
||||||
Thread.sleep(Integer.MAX_VALUE)
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -77,16 +80,16 @@ public class HostCache {
|
|||||||
|
|
||||||
void reportAbuse(I2PSession sesison, int severity) {}
|
void reportAbuse(I2PSession sesison, int severity) {}
|
||||||
void disconnected(I2PSession session) {
|
void disconnected(I2PSession session) {
|
||||||
println "ERROR: session disconnected, exiting"
|
log.severe("session disconnected, exiting")
|
||||||
System.exit(1)
|
System.exit(1)
|
||||||
}
|
}
|
||||||
void errorOccurred(I2PSession session, String message, Throwable error) {
|
void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
println "ERROR: ${message} ${error}"
|
log.warning("${message} ${error}")
|
||||||
}
|
}
|
||||||
void messageAvailable(I2PSession session, int msgId, long size, int proto,
|
void messageAvailable(I2PSession session, int msgId, long size, int proto,
|
||||||
int fromport, int toport) {
|
int fromport, int toport) {
|
||||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||||
println "WARN: received unexpected protocol ${proto}"
|
log.warning("received unexpected protocol ${proto}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,19 +98,19 @@ public class HostCache {
|
|||||||
try {
|
try {
|
||||||
dissector.loadI2PDatagram(payload)
|
dissector.loadI2PDatagram(payload)
|
||||||
def sender = dissector.getSender()
|
def sender = dissector.getSender()
|
||||||
println "INFO: Received something from ${sender.toBase32()}"
|
def b32 = sender.toBase32()
|
||||||
|
|
||||||
payload = dissector.getPayload()
|
payload = dissector.getPayload()
|
||||||
payload = json.parse(payload)
|
payload = json.parse(payload)
|
||||||
if (payload.type == null) {
|
if (payload.type == null) {
|
||||||
println "WARN: type field missing"
|
log.warning("type field missing from $b32")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch(payload.type) {
|
switch(payload.type) {
|
||||||
case "Ping" :
|
case "Ping" :
|
||||||
println "Ping"
|
log.info("ping from $b32")
|
||||||
if (payload.leaf == null) {
|
if (payload.leaf == null) {
|
||||||
println "WARN: ping didn't specify if leaf"
|
log.warning("ping didn't specify if leaf from $b32")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload.leaf = Boolean.parseBoolean(payload.leaf.toString())
|
payload.leaf = Boolean.parseBoolean(payload.leaf.toString())
|
||||||
@@ -116,14 +119,14 @@ public class HostCache {
|
|||||||
respond(session, sender, payload)
|
respond(session, sender, payload)
|
||||||
break
|
break
|
||||||
case "CrawlerPong":
|
case "CrawlerPong":
|
||||||
println "CrawlerPong"
|
log.info("CrawlerPong from $b32")
|
||||||
crawler.handleCrawlerPong(payload, sender)
|
crawler.handleCrawlerPong(payload, sender)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
println "WARN: Unexpected message type ${payload.type}, dropping"
|
log.warning("Unexpected message type ${payload.type}, dropping from $b32")
|
||||||
}
|
}
|
||||||
} catch (Exception dfe) {
|
} catch (Exception dfe) {
|
||||||
println "WARN: invalid datagram ${dfe}"
|
log.log(Level.WARNING,"invalid datagram", dfe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void messageAvailable(I2PSession session, int msgId, long size) {
|
void messageAvailable(I2PSession session, int msgId, long size) {
|
||||||
|
@@ -2,6 +2,7 @@ package com.muwire.update
|
|||||||
|
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.client.I2PClientFactory
|
import net.i2p.client.I2PClientFactory
|
||||||
import net.i2p.client.I2PSession
|
import net.i2p.client.I2PSession
|
||||||
@@ -55,7 +56,7 @@ class UpdateServer {
|
|||||||
static class Listener implements I2PSessionMuxedListener {
|
static class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
private final File json
|
private final File json
|
||||||
|
private final def slurper = new JsonSlurper()
|
||||||
Listener(File json) {
|
Listener(File json) {
|
||||||
this.json = json
|
this.json = json
|
||||||
}
|
}
|
||||||
@@ -76,8 +77,9 @@ class UpdateServer {
|
|||||||
try {
|
try {
|
||||||
dissector.loadI2PDatagram(payload)
|
dissector.loadI2PDatagram(payload)
|
||||||
def sender = dissector.getSender()
|
def sender = dissector.getSender()
|
||||||
log.info("Got an update ping from "+sender.toBase32())
|
payload = slurper.parse(dissector.getPayload())
|
||||||
// I don't think we care about the payload at this point
|
log.info("Got an update ping from "+sender.toBase32() + " reported version "+payload?.myVersion)
|
||||||
|
|
||||||
def maker = new I2PDatagramMaker(session)
|
def maker = new I2PDatagramMaker(session)
|
||||||
def response = maker.makeI2PDatagram(json.bytes)
|
def response = maker.makeI2PDatagram(json.bytes)
|
||||||
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
|
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
|
||||||
|
Reference in New Issue
Block a user