Compare commits
265 Commits
muwire-0.1
...
throttle-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4468a262ae | ||
![]() |
1780901cb0 | ||
![]() |
d830d9261f | ||
![]() |
f5e1833a48 | ||
![]() |
9feb2a3c8f | ||
![]() |
b27665f5dd | ||
![]() |
4465aa4134 | ||
![]() |
ad766ac748 | ||
![]() |
d9e7d67d86 | ||
![]() |
3fefbc94b3 | ||
![]() |
21034209a5 | ||
![]() |
7c04c0f83c | ||
![]() |
f5293d65dd | ||
![]() |
8191bf6066 | ||
![]() |
29b6bfd463 | ||
![]() |
2f3d23bc34 | ||
![]() |
98dd80c4b8 | ||
![]() |
d9edb2e128 | ||
![]() |
de04b40b86 | ||
![]() |
7206a3d926 | ||
![]() |
98b98d8938 | ||
![]() |
294b8fcc2f | ||
![]() |
32f601a1b1 | ||
![]() |
8e3a398080 | ||
![]() |
720b9688b4 | ||
![]() |
e3066161c5 | ||
![]() |
a9aa3a524f | ||
![]() |
92848e818a | ||
![]() |
a7aa3008c0 | ||
![]() |
485325e824 | ||
![]() |
0df2a0e039 | ||
![]() |
fb7b4466c2 | ||
![]() |
53105245f4 | ||
![]() |
b68eab91e0 | ||
![]() |
f72cf91462 | ||
![]() |
a655c4ef50 | ||
![]() |
5d46e9b796 | ||
![]() |
642e6e67b3 | ||
![]() |
2b6b86f903 | ||
![]() |
f2706a4426 | ||
![]() |
1af75413aa | ||
![]() |
adc4077b1a | ||
![]() |
01f4e2453b | ||
![]() |
61267374dd | ||
![]() |
970f814685 | ||
![]() |
4fd9fc1991 | ||
![]() |
26207ffd1b | ||
![]() |
2614cfbe5f | ||
![]() |
f11d461ec0 | ||
![]() |
b2eb2d2755 | ||
![]() |
ea46a54f19 | ||
![]() |
627add45ad | ||
![]() |
d364855459 | ||
![]() |
14ee35e77a | ||
![]() |
8773eb4ee0 | ||
![]() |
51425bbfd9 | ||
![]() |
6a4879bc0b | ||
![]() |
e7fe56439b | ||
![]() |
2886feab4a | ||
![]() |
fb91194026 | ||
![]() |
4527478b0d | ||
![]() |
b0062f146e | ||
![]() |
bf16561170 | ||
![]() |
3b23dc29c4 | ||
![]() |
c0645b670e | ||
![]() |
30613fe530 | ||
![]() |
e7822f6edc | ||
![]() |
7e5c9ba115 | ||
![]() |
647fa3a481 | ||
![]() |
538eca9297 | ||
![]() |
e73a23d4a4 | ||
![]() |
76e41a0383 | ||
![]() |
7045927666 | ||
![]() |
5fb3086b42 | ||
![]() |
2de18227c1 | ||
![]() |
bd12a1de3d | ||
![]() |
a3a91050c8 | ||
![]() |
6c1cc28e49 | ||
![]() |
b6e5b54f05 | ||
![]() |
a6e559ec67 | ||
![]() |
f11badb824 | ||
![]() |
44da44ff6f | ||
![]() |
aae3fc29ca | ||
![]() |
c30aa19d8b | ||
![]() |
c79e8712d0 | ||
![]() |
ed12d78a48 | ||
![]() |
d27872cc8b | ||
![]() |
f794c39760 | ||
![]() |
2be9c425f7 | ||
![]() |
ab5fea9216 | ||
![]() |
d1c8328080 | ||
![]() |
89e761f53b | ||
![]() |
40410eba63 | ||
![]() |
85466a8e80 | ||
![]() |
c210af7870 | ||
![]() |
38ff49d28f | ||
![]() |
710f9f52a8 | ||
![]() |
1b6eda5a40 | ||
![]() |
1ee9ccf098 | ||
![]() |
0f07562de3 | ||
![]() |
6eb1aa07f5 | ||
![]() |
05b02834af | ||
![]() |
56125f6df8 | ||
![]() |
8f9996848b | ||
![]() |
dd655ed60f | ||
![]() |
8923c6ff7d | ||
![]() |
807ab22f8e | ||
![]() |
a26ad229ee | ||
![]() |
5504dd2251 | ||
![]() |
f9777d29f4 | ||
![]() |
b23226e8c6 | ||
![]() |
1249ad29e0 | ||
![]() |
7bb5e5b632 | ||
![]() |
b2e43f9765 | ||
![]() |
2aa73c203a | ||
![]() |
18d2b56563 | ||
![]() |
a455b4ad6e | ||
![]() |
761b683a81 | ||
![]() |
1d41bcd825 | ||
![]() |
f1ac038b55 | ||
![]() |
396c636e42 | ||
![]() |
e32c858e90 | ||
![]() |
821555f3f1 | ||
![]() |
089ab4f0d9 | ||
![]() |
948b6292fe | ||
![]() |
4e2a530a13 | ||
![]() |
03646e2b90 | ||
![]() |
3dce228bbb | ||
![]() |
15a49ad550 | ||
![]() |
3d91c0f4c7 | ||
![]() |
2825a8d9a4 | ||
![]() |
8dcce9bda6 | ||
![]() |
d8d3e2cd58 | ||
![]() |
51d5dbe47e | ||
![]() |
84cee0aa43 | ||
![]() |
162844787f | ||
![]() |
d8a2b59055 | ||
![]() |
67a0939de4 | ||
![]() |
37ca922a2c | ||
![]() |
1d6781819b | ||
![]() |
64d45da94a | ||
![]() |
59c84d8a5e | ||
![]() |
8b55021a4b | ||
![]() |
8bd3ebfaf5 | ||
![]() |
526ec45da3 | ||
![]() |
deb7c0b4b0 | ||
![]() |
e85a0c7b2c | ||
![]() |
7b021a47eb | ||
![]() |
0c21d4d6c1 | ||
![]() |
8e9f79d404 | ||
![]() |
bf33a6ff61 | ||
![]() |
19c8d84afd | ||
![]() |
6a40787863 | ||
![]() |
c698cbd737 | ||
![]() |
9c049b9301 | ||
![]() |
84a9bb9482 | ||
![]() |
0c1008d6b3 | ||
![]() |
c46f1b1ccd | ||
![]() |
7e2c4d48c6 | ||
![]() |
71a919e62b | ||
![]() |
d5eb65bdc2 | ||
![]() |
aef7533bd5 | ||
![]() |
e78016ead4 | ||
![]() |
52ced669dd | ||
![]() |
b52fb38ede | ||
![]() |
5dcef3ca05 | ||
![]() |
eaa0e46ce5 | ||
![]() |
c4f48c02b6 | ||
![]() |
5c16335969 | ||
![]() |
546eb4e9d3 | ||
![]() |
c3d9e852ba | ||
![]() |
0db7077a45 | ||
![]() |
614ecc85fe | ||
![]() |
af66a79376 | ||
![]() |
465171c81d | ||
![]() |
b507361c58 | ||
![]() |
4d001ae74b | ||
![]() |
36a6e2769f | ||
![]() |
69eeb7d77a | ||
![]() |
551982b72a | ||
![]() |
8d808f0b8f | ||
![]() |
7833a83c87 | ||
![]() |
3160c1a8f3 | ||
![]() |
e295aa67d5 | ||
![]() |
a9f5625dc3 | ||
![]() |
cc0af5b9ed | ||
![]() |
041fc3bef3 | ||
![]() |
03c3b1ebf1 | ||
![]() |
aece390daa | ||
![]() |
cf63be68e8 | ||
![]() |
88ece4dc23 | ||
![]() |
13767d58f2 | ||
![]() |
05a1ccd3d8 | ||
![]() |
6807c14a5f | ||
![]() |
684be0c50e | ||
![]() |
6655c262c6 | ||
![]() |
b1ccd55030 | ||
![]() |
a3becd0f7e | ||
![]() |
af2f3e0ebf | ||
![]() |
e2b7ffa1db | ||
![]() |
0e0176acfc | ||
![]() |
7f09bb079c | ||
![]() |
77e48b01bb | ||
![]() |
12db6857c1 | ||
![]() |
acd67733a5 | ||
![]() |
8d3ce7aa8e | ||
![]() |
0eb5870e9b | ||
![]() |
051efbfaba | ||
![]() |
6b38d7bffb | ||
![]() |
5778d537ce | ||
![]() |
93664a7985 | ||
![]() |
edd58e0c90 | ||
![]() |
9ac52b61dc | ||
![]() |
0a4b9c7029 | ||
![]() |
87b366a205 | ||
![]() |
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 |
32
README.md
@@ -4,26 +4,26 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
|||||||
|
|
||||||
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 first stable release - 0.1.0 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
The current stable release - 0.4.0 is avaiable for download at https://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 clean assemble
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to run the unit tests, type
|
If you want to run the unit tests, type
|
||||||
```
|
```
|
||||||
./gradlew build
|
./gradlew clean build
|
||||||
```
|
```
|
||||||
|
|
||||||
Some of the UI tests will fail because they haven't been written yet :-/
|
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 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.
|
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.port=<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. It is best to leave MuWire running all the time, just like I2P.
|
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
|
||||||
|
|
||||||
@@ -32,3 +32,25 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
|
|||||||
|
|
||||||
* Many UI features you would expect are not there yet
|
* Many UI features you would expect are not there yet
|
||||||
|
|
||||||
|
### Quick FAQ
|
||||||
|
|
||||||
|
* why is MuWire slow ?
|
||||||
|
|
||||||
|
- too few sources you're downloading from
|
||||||
|
- you can increase the number of tunnels by using more tunnels via Options->I2P Inbound/Outbound Quantity
|
||||||
|
the default is 4 and you could raise up to as high as 16 ( Caution !!!!)
|
||||||
|
|
||||||
|
* my search is not returning (enough) results !
|
||||||
|
|
||||||
|
- search is keyword or hash based
|
||||||
|
- keywords and hash(es) are NOT regexed or wildcarded so they have to be complete
|
||||||
|
so searching for 'musi' will not return results with 'music' - you have to search for 'music'
|
||||||
|
- ALL keywords have to match
|
||||||
|
- only use space for keyword separation
|
||||||
|
- if you already have the file in question it is not displayed ( can be changed via Options )
|
||||||
|
|
||||||
|
* what's this right click -> 'Copy hash to clipboard' for ?
|
||||||
|
|
||||||
|
- if you have a specific file you wish to share or download you can use the hash as a unique identifier
|
||||||
|
to make sure you have exactly the right file.
|
||||||
|
- you can share this hash with others to ensure they are getting the right file
|
||||||
|
36
TODO.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# TODO List
|
||||||
|
|
||||||
|
Not in any particular order yet
|
||||||
|
|
||||||
|
### Big Items
|
||||||
|
|
||||||
|
##### Bloom Filters
|
||||||
|
|
||||||
|
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
|
||||||
|
|
||||||
|
##### Two-tier Topology
|
||||||
|
|
||||||
|
This helps with scalability
|
||||||
|
|
||||||
|
##### Trust List Sharing
|
||||||
|
|
||||||
|
For helping users make better decisions whom to trust
|
||||||
|
|
||||||
|
##### Content Control Panel
|
||||||
|
|
||||||
|
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
|
||||||
|
|
||||||
|
##### Web UI, REST Interface, etc.
|
||||||
|
|
||||||
|
Basically any non-gui non-cli user interface
|
||||||
|
|
||||||
|
##### Metadata editing and search
|
||||||
|
|
||||||
|
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
|
||||||
|
|
||||||
|
### Small Items
|
||||||
|
|
||||||
|
* Wrapper of some kind for in-place upgrades
|
||||||
|
* Download file sequentially
|
||||||
|
* Unsharing of files (half done)
|
||||||
|
* Multiple-selection download, Ctrl-A
|
@@ -4,6 +4,7 @@ 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.UILoadedEvent
|
||||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||||
import com.muwire.core.connection.ConnectionEvent
|
import com.muwire.core.connection.ConnectionEvent
|
||||||
import com.muwire.core.connection.DisconnectionEvent
|
import com.muwire.core.connection.DisconnectionEvent
|
||||||
@@ -34,7 +35,7 @@ class Cli {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.1.4")
|
core = new Core(props, home, "0.4.4")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
@@ -72,7 +73,7 @@ class Cli {
|
|||||||
|
|
||||||
Timer timer = new Timer("status-printer", true)
|
Timer timer = new Timer("status-printer", true)
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
|
println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
|
||||||
} as TimerTask, 60000, 60000)
|
} as TimerTask, 60000, 60000)
|
||||||
|
|
||||||
def latch = new CountDownLatch(1)
|
def latch = new CountDownLatch(1)
|
||||||
@@ -83,7 +84,8 @@ class Cli {
|
|||||||
}
|
}
|
||||||
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
|
core.eventBus.publish(new UILoadedEvent())
|
||||||
println "waiting for files to load"
|
println "waiting for files to load"
|
||||||
latch.await()
|
latch.await()
|
||||||
// now we begin
|
// now we begin
|
||||||
@@ -117,11 +119,11 @@ class Cli {
|
|||||||
volatile int uploads
|
volatile int uploads
|
||||||
public void onUploadEvent(UploadEvent e) {
|
public void onUploadEvent(UploadEvent e) {
|
||||||
uploads++
|
uploads++
|
||||||
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
println String.valueOf(new Date()) + " Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
}
|
}
|
||||||
public void onUploadFinishedEvent(UploadFinishedEvent e) {
|
public void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||||
uploads--
|
uploads--
|
||||||
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.1.4")
|
core = new Core(props, home, "0.4.4")
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
bad.printStackTrace(System.out)
|
bad.printStackTrace(System.out)
|
||||||
println "Failed to initialize core, exiting"
|
println "Failed to initialize core, exiting"
|
||||||
|
23
cli/src/main/groovy/com/muwire/cli/FileList.groovy
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class FileList {
|
||||||
|
public static void main(String [] args) {
|
||||||
|
if (args.length < 1) {
|
||||||
|
println "pass files.json as argument"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
File filesJson = new File(args[0])
|
||||||
|
filesJson.eachLine {
|
||||||
|
def json = slurper.parseText(it)
|
||||||
|
String name = DataUtil.readi18nString(Base64.decode(json.file))
|
||||||
|
println "$name,$json.length,$json.pieceSize,$json.infoHash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ apply plugin : 'application'
|
|||||||
mainClassName = 'com.muwire.core.Core'
|
mainClassName = 'com.muwire.core.Core'
|
||||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compile 'net.i2p:router:0.9.40'
|
||||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
compile 'net.i2p.client:mstreaming:0.9.40'
|
||||||
compile 'net.i2p.client:streaming:0.9.40'
|
compile 'net.i2p.client:streaming:0.9.40'
|
||||||
|
|
||||||
|
@@ -9,7 +9,5 @@ class Constants {
|
|||||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||||
public static final int MAX_HEADERS = 16
|
public static final int MAX_HEADERS = 16
|
||||||
|
|
||||||
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
|
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||||
|
|
||||||
public static final String SPLIT_PATTERN = "[\\.,_-]"
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core
|
package com.muwire.core
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
import com.muwire.core.connection.ConnectionAcceptor
|
import com.muwire.core.connection.ConnectionAcceptor
|
||||||
import com.muwire.core.connection.ConnectionEstablisher
|
import com.muwire.core.connection.ConnectionEstablisher
|
||||||
@@ -12,7 +13,11 @@ 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.SourceDiscoveredEvent
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
|
import com.muwire.core.download.UIDownloadPausedEvent
|
||||||
|
import com.muwire.core.download.UIDownloadResumedEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
@@ -22,14 +27,19 @@ import com.muwire.core.files.FileSharedEvent
|
|||||||
import com.muwire.core.files.FileUnsharedEvent
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.files.HasherService
|
import com.muwire.core.files.HasherService
|
||||||
import com.muwire.core.files.PersisterService
|
import com.muwire.core.files.PersisterService
|
||||||
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
|
import com.muwire.core.files.DirectoryWatcher
|
||||||
import com.muwire.core.hostcache.CacheClient
|
import com.muwire.core.hostcache.CacheClient
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.ResultsEvent
|
import com.muwire.core.search.ResultsEvent
|
||||||
import com.muwire.core.search.ResultsSender
|
import com.muwire.core.search.ResultsSender
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.update.UpdateClient
|
import com.muwire.core.update.UpdateClient
|
||||||
@@ -43,6 +53,7 @@ import net.i2p.client.I2PSession
|
|||||||
import net.i2p.client.streaming.I2PSocketManager
|
import net.i2p.client.streaming.I2PSocketManager
|
||||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||||
import net.i2p.client.streaming.I2PSocketOptions
|
import net.i2p.client.streaming.I2PSocketOptions
|
||||||
|
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||||
import net.i2p.crypto.DSAEngine
|
import net.i2p.crypto.DSAEngine
|
||||||
import net.i2p.crypto.SigType
|
import net.i2p.crypto.SigType
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -50,6 +61,9 @@ import net.i2p.data.PrivateKey
|
|||||||
import net.i2p.data.Signature
|
import net.i2p.data.Signature
|
||||||
import net.i2p.data.SigningPrivateKey
|
import net.i2p.data.SigningPrivateKey
|
||||||
|
|
||||||
|
import net.i2p.router.Router
|
||||||
|
import net.i2p.router.RouterContext
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Core {
|
public class Core {
|
||||||
|
|
||||||
@@ -68,13 +82,65 @@ 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
|
||||||
|
private final DirectoryWatcher directoryWatcher
|
||||||
|
final FileManager fileManager
|
||||||
|
final UploadManager uploadManager
|
||||||
|
|
||||||
|
private final Router router
|
||||||
|
|
||||||
|
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||||
|
|
||||||
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
|
this.muOptions = props
|
||||||
log.info "Initializing I2P context"
|
|
||||||
I2PAppContext.getGlobalContext().logManager()
|
i2pOptions = new Properties()
|
||||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
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"] = "4"
|
||||||
|
i2pOptions["outbound.length"] = "3"
|
||||||
|
i2pOptions["outbound.quantity"] = "4"
|
||||||
|
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||||
|
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||||
|
Random r = new Random()
|
||||||
|
int port = r.nextInt(60000) + 4000
|
||||||
|
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||||
|
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||||
|
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.embeddedRouter) {
|
||||||
|
log.info "Initializing I2P context"
|
||||||
|
I2PAppContext.getGlobalContext().logManager()
|
||||||
|
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||||
|
router = null
|
||||||
|
} else {
|
||||||
|
log.info("launching embedded router")
|
||||||
|
Properties routerProps = new Properties()
|
||||||
|
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||||
|
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||||
|
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||||
|
routerProps.setProperty("i2cp.disableInterface", "true")
|
||||||
|
routerProps.setProperty("i2np.ntcp.port", i2pOptions["i2np.ntcp.port"])
|
||||||
|
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
|
||||||
|
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
|
||||||
|
router = new Router(routerProps)
|
||||||
|
I2PAppContext.getGlobalContext().metaClass = new RouterContextMetaClass()
|
||||||
|
router.runRouter()
|
||||||
|
while(!router.isRunning())
|
||||||
|
Thread.sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
log.info("initializing I2P socket manager")
|
log.info("initializing I2P socket manager")
|
||||||
def i2pClient = new I2PClientFactory().createClient()
|
def i2pClient = new I2PClientFactory().createClient()
|
||||||
@@ -86,31 +152,16 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i2pOptions = new Properties()
|
|
||||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
|
||||||
if (i2pOptionsFile.exists()) {
|
|
||||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
|
||||||
if (!i2pOptions.hasProperty("inbound.nickname"))
|
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
|
||||||
if (!i2pOptions.hasProperty("outbound.nickname"))
|
|
||||||
i2pOptions["outbound.nickname"] = "MuWire"
|
|
||||||
} else {
|
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
|
||||||
i2pOptions["outbound.nickname"] = "MuWire"
|
|
||||||
i2pOptions["inbound.length"] = "3"
|
|
||||||
i2pOptions["inbound.quantity"] = "2"
|
|
||||||
i2pOptions["outbound.length"] = "3"
|
|
||||||
i2pOptions["outbound.quantity"] = "2"
|
|
||||||
}
|
|
||||||
|
|
||||||
// options like tunnel length and quantity
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
I2PSession i2pSession
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions)
|
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)
|
||||||
|
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||||
i2pSession = socketManager.getSession()
|
i2pSession = socketManager.getSession()
|
||||||
|
|
||||||
def destination = new Destination()
|
def destination = new Destination()
|
||||||
@@ -148,15 +199,21 @@ public class Core {
|
|||||||
|
|
||||||
|
|
||||||
log.info "initializing file manager"
|
log.info "initializing file manager"
|
||||||
FileManager fileManager = new FileManager(eventBus, props)
|
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)
|
||||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||||
eventBus.register(SearchEvent.class, fileManager)
|
eventBus.register(SearchEvent.class, fileManager)
|
||||||
|
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||||
|
|
||||||
|
log.info("initializing mesh manager")
|
||||||
|
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||||
|
|
||||||
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)
|
||||||
|
eventBus.register(UILoadedEvent.class, persisterService)
|
||||||
|
|
||||||
log.info("initializing host cache")
|
log.info("initializing host cache")
|
||||||
File hostStorage = new File(home, "hosts.json")
|
File hostStorage = new File(home, "hosts.json")
|
||||||
@@ -177,7 +234,9 @@ public class Core {
|
|||||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||||
|
|
||||||
log.info("initializing update client")
|
log.info("initializing update client")
|
||||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
|
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||||
|
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||||
|
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||||
|
|
||||||
log.info("initializing connector")
|
log.info("initializing connector")
|
||||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||||
@@ -191,13 +250,17 @@ 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, home, me)
|
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
log.info("initializing upload manager")
|
||||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||||
|
|
||||||
log.info("initializing connection establisher")
|
log.info("initializing connection establisher")
|
||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
@@ -207,6 +270,11 @@ public class Core {
|
|||||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||||
|
|
||||||
|
log.info("initializing directory watcher")
|
||||||
|
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||||
|
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||||
|
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||||
|
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||||
|
|
||||||
log.info("initializing hasher service")
|
log.info("initializing hasher service")
|
||||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||||
@@ -217,7 +285,6 @@ public class Core {
|
|||||||
hasherService.start()
|
hasherService.start()
|
||||||
trustService.start()
|
trustService.start()
|
||||||
trustService.waitForLoad()
|
trustService.waitForLoad()
|
||||||
persisterService.start()
|
|
||||||
hostCache.start()
|
hostCache.start()
|
||||||
connectionManager.start()
|
connectionManager.start()
|
||||||
cacheClient.start()
|
cacheClient.start()
|
||||||
@@ -228,9 +295,39 @@ public class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
if (!shutdown.compareAndSet(false, true)) {
|
||||||
|
log.info("already shutting down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info("shutting down download manageer")
|
||||||
|
downloadManager.shutdown()
|
||||||
|
log.info("shutting down connection acceeptor")
|
||||||
|
connectionAcceptor.stop()
|
||||||
|
log.info("shutting down connection establisher")
|
||||||
|
connectionEstablisher.stop()
|
||||||
|
log.info("shutting down directory watcher")
|
||||||
|
directoryWatcher.stop()
|
||||||
|
log.info("shutting down connection manager")
|
||||||
connectionManager.shutdown()
|
connectionManager.shutdown()
|
||||||
|
if (router != null) {
|
||||||
|
log.info("shutting down embedded router")
|
||||||
|
router.shutdown(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class RouterContextMetaClass extends DelegatingMetaClass {
|
||||||
|
private final Object logManager = new MuWireLogManager()
|
||||||
|
RouterContextMetaClass() {
|
||||||
|
super(RouterContext.class)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object invokeMethod(Object object, String name, Object[] args) {
|
||||||
|
if (name == "logManager")
|
||||||
|
return logManager
|
||||||
|
super.invokeMethod(object, name, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static main(args) {
|
static main(args) {
|
||||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||||
home = new File(home)
|
home = new File(home)
|
||||||
@@ -255,7 +352,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.1.4")
|
Core core = new Core(props, home, "0.4.4")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
package com.muwire.core
|
package com.muwire.core
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import com.muwire.core.hostcache.CrawlerResponse
|
import com.muwire.core.hostcache.CrawlerResponse
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class MuWireSettings {
|
class MuWireSettings {
|
||||||
|
|
||||||
@@ -8,11 +13,18 @@ class MuWireSettings {
|
|||||||
boolean allowUntrusted
|
boolean allowUntrusted
|
||||||
int downloadRetryInterval
|
int downloadRetryInterval
|
||||||
int updateCheckInterval
|
int updateCheckInterval
|
||||||
|
boolean autoDownloadUpdate
|
||||||
|
String updateType
|
||||||
String nickname
|
String nickname
|
||||||
File downloadLocation
|
File downloadLocation
|
||||||
String sharedFiles
|
|
||||||
CrawlerResponse crawlerResponse
|
CrawlerResponse crawlerResponse
|
||||||
boolean shareDownloadedFiles
|
boolean shareDownloadedFiles
|
||||||
|
Set<String> watchedDirectories
|
||||||
|
float downloadSequentialRatio
|
||||||
|
int hostClearInterval
|
||||||
|
int meshExpiration
|
||||||
|
boolean embeddedRouter
|
||||||
|
int inBw, outBw
|
||||||
|
|
||||||
MuWireSettings() {
|
MuWireSettings() {
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
@@ -25,10 +37,24 @@ class MuWireSettings {
|
|||||||
nickname = props.getProperty("nickname","MuWireUser")
|
nickname = props.getProperty("nickname","MuWireUser")
|
||||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||||
System.getProperty("user.home")))
|
System.getProperty("user.home")))
|
||||||
sharedFiles = props.getProperty("sharedFiles")
|
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||||
|
updateType = props.getProperty("updateType","jar")
|
||||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||||
|
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||||
|
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
|
||||||
|
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||||
|
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||||
|
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||||
|
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||||
|
|
||||||
|
watchedDirectories = new HashSet<>()
|
||||||
|
if (props.containsKey("watchedDirectories")) {
|
||||||
|
String[] encoded = props.getProperty("watchedDirectories").split(",")
|
||||||
|
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -40,9 +66,23 @@ 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("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||||
|
props.setProperty("updateType",updateType)
|
||||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||||
if (sharedFiles != null)
|
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||||
props.setProperty("sharedFiles", sharedFiles)
|
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||||
|
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||||
|
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||||
|
props.setProperty("inBw", String.valueOf(inBw))
|
||||||
|
props.setProperty("outBw", String.valueOf(outBw))
|
||||||
|
|
||||||
|
if (!watchedDirectories.isEmpty()) {
|
||||||
|
String encoded = watchedDirectories.stream().
|
||||||
|
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||||
|
collect(Collectors.joining(","))
|
||||||
|
props.setProperty("watchedDirectories", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
props.store(out, "")
|
props.store(out, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -82,4 +82,13 @@ public class Persona {
|
|||||||
Persona other = (Persona)o
|
Persona other = (Persona)o
|
||||||
name.equals(other.name) && destination.equals(other.destination)
|
name.equals(other.name) && destination.equals(other.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void main(String []args) {
|
||||||
|
if (args.length != 1) {
|
||||||
|
println "This utility decodes a bas64-encoded persona"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(args[0])))
|
||||||
|
println p.getHumanReadableName()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core
|
||||||
|
|
||||||
|
class RouterDisconnectedEvent extends Event {
|
||||||
|
}
|
@@ -21,6 +21,9 @@ import net.i2p.data.Destination
|
|||||||
|
|
||||||
@Log
|
@Log
|
||||||
abstract class Connection implements Closeable {
|
abstract class Connection implements Closeable {
|
||||||
|
|
||||||
|
private static final int SEARCHES = 10
|
||||||
|
private static final long INTERVAL = 1000
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Endpoint endpoint
|
final Endpoint endpoint
|
||||||
@@ -32,6 +35,7 @@ abstract class Connection implements Closeable {
|
|||||||
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
|
||||||
|
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
|
||||||
|
|
||||||
protected final String name
|
protected final String name
|
||||||
|
|
||||||
@@ -76,9 +80,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +160,25 @@ abstract class Connection implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean throttleSearch() {
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
if (searchTimestamps.size() < SEARCHES) {
|
||||||
|
searchTimestamps.addLast(now)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Long oldest = searchTimestamps.getFirst()
|
||||||
|
if (now - oldest.longValue() < INTERVAL)
|
||||||
|
return true
|
||||||
|
searchTimestamps.addLast(now)
|
||||||
|
searchTimestamps.removeFirst()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
protected void handleSearch(def search) {
|
protected void handleSearch(def search) {
|
||||||
|
if (throttleSearch()) {
|
||||||
|
log.info("dropping excessive search")
|
||||||
|
return
|
||||||
|
}
|
||||||
UUID uuid = UUID.fromString(search.uuid)
|
UUID uuid = UUID.fromString(search.uuid)
|
||||||
byte [] infohash = null
|
byte [] infohash = null
|
||||||
if (search.infohash != null) {
|
if (search.infohash != null) {
|
||||||
|
@@ -17,6 +17,8 @@ import com.muwire.core.upload.UploadManager
|
|||||||
import com.muwire.core.search.InvalidSearchResultException
|
import com.muwire.core.search.InvalidSearchResultException
|
||||||
import com.muwire.core.search.ResultsParser
|
import com.muwire.core.search.ResultsParser
|
||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.search.UnexpectedResultsException
|
import com.muwire.core.search.UnexpectedResultsException
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
@@ -38,6 +40,8 @@ class ConnectionAcceptor {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -73,11 +77,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()}")
|
||||||
@@ -93,6 +99,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) {
|
||||||
@@ -108,6 +119,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
|
||||||
@@ -178,9 +192,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())
|
||||||
@@ -204,13 +227,15 @@ class ConnectionAcceptor {
|
|||||||
if (sender.destination != e.getDestination())
|
if (sender.destination != e.getDestination())
|
||||||
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
||||||
int nResults = dis.readUnsignedShort()
|
int nResults = dis.readUnsignedShort()
|
||||||
|
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||||
for (int i = 0; i < nResults; i++) {
|
for (int i = 0; i < nResults; i++) {
|
||||||
int jsonSize = dis.readUnsignedShort()
|
int jsonSize = dis.readUnsignedShort()
|
||||||
byte [] payload = new byte[jsonSize]
|
byte [] payload = new byte[jsonSize]
|
||||||
dis.readFully(payload)
|
dis.readFully(payload)
|
||||||
def json = slurper.parse(payload)
|
def json = slurper.parse(payload)
|
||||||
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
|
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||||
}
|
}
|
||||||
|
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||||
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
||||||
log.log(Level.WARNING, "failed to process POST", bad)
|
log.log(Level.WARNING, "failed to process POST", bad)
|
||||||
} finally {
|
} finally {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -24,7 +25,7 @@ class Endpoint implements Closeable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
log.warning("Close loop detected for ${destination.toBase32()}", new Exception())
|
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
@@ -34,7 +35,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) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -104,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()
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,10 @@ 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.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
|
import com.muwire.core.trust.TrustLevel
|
||||||
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.json.JsonBuilder
|
import groovy.json.JsonBuilder
|
||||||
@@ -14,24 +18,33 @@ import net.i2p.util.ConcurrentHashSet
|
|||||||
|
|
||||||
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.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.UILoadedEvent
|
import com.muwire.core.UILoadedEvent
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
public class DownloadManager {
|
public class DownloadManager {
|
||||||
|
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
|
private final TrustService trustService
|
||||||
|
private final MeshManager meshManager
|
||||||
|
private final MuWireSettings muSettings
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Executor executor
|
private final Executor executor
|
||||||
private final File incompletes, home
|
private final File incompletes, home
|
||||||
private final Persona me
|
private final Persona me
|
||||||
|
|
||||||
private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
|
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
|
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
|
||||||
|
I2PConnector connector, File home, Persona me) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
this.trustService = trustService
|
||||||
|
this.meshManager = meshManager
|
||||||
|
this.muSettings = muSettings
|
||||||
this.connector = connector
|
this.connector = connector
|
||||||
this.incompletes = new File(home,"incompletes")
|
this.incompletes = new File(home,"incompletes")
|
||||||
this.home = home
|
this.home = home
|
||||||
@@ -58,16 +71,33 @@ public class DownloadManager {
|
|||||||
e.result.each {
|
e.result.each {
|
||||||
destinations.add(it.sender.destination)
|
destinations.add(it.sender.destination)
|
||||||
}
|
}
|
||||||
|
destinations.addAll(e.sources)
|
||||||
|
destinations.remove(me.destination)
|
||||||
|
|
||||||
|
Pieces pieces = getPieces(infohash, size, pieceSize)
|
||||||
|
|
||||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||||
infohash, pieceSize, connector, destinations,
|
infohash, pieceSize, connector, destinations,
|
||||||
incompletes)
|
incompletes, pieces)
|
||||||
downloaders.add(downloader)
|
downloaders.put(infohash, downloader)
|
||||||
persistDownloaders()
|
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.infoHash)
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
void resume(Downloader downloader) {
|
void resume(Downloader downloader) {
|
||||||
executor.execute({downloader.download() as Runnable})
|
executor.execute({downloader.download() as Runnable})
|
||||||
}
|
}
|
||||||
@@ -84,24 +114,62 @@ public class DownloadManager {
|
|||||||
json.destinations.each { destination ->
|
json.destinations.each { destination ->
|
||||||
destinations.add new Destination(destination)
|
destinations.add new Destination(destination)
|
||||||
}
|
}
|
||||||
byte[] hashList = Base64.decode(json.hashList)
|
InfoHash infoHash
|
||||||
InfoHash infoHash = InfoHash.fromHashList(hashList)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||||
|
|
||||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||||
downloader.download()
|
if (json.paused != null)
|
||||||
|
downloader.paused = json.paused
|
||||||
|
downloaders.put(infoHash, downloader)
|
||||||
|
downloader.readPieces()
|
||||||
|
if (!downloader.paused)
|
||||||
|
downloader.download()
|
||||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||||
|
int pieceSize = 0x1 << pieceSizePow2
|
||||||
|
int nPieces = (int)(length / pieceSize)
|
||||||
|
if (length % pieceSize != 0)
|
||||||
|
nPieces++
|
||||||
|
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
|
||||||
|
mesh.pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
Downloader downloader = downloaders.get(e.infoHash)
|
||||||
|
if (downloader == null)
|
||||||
|
return
|
||||||
|
boolean ok = false
|
||||||
|
switch(trustService.getLevel(e.source.destination)) {
|
||||||
|
case TrustLevel.TRUSTED: ok = true; break
|
||||||
|
case TrustLevel.NEUTRAL: ok = muSettings.allowUntrusted; break
|
||||||
|
case TrustLevel.DISTRUSTED: ok = false; break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
downloader.addSource(e.source.destination)
|
||||||
|
}
|
||||||
|
|
||||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
downloaders.remove(e.downloader)
|
downloaders.remove(e.downloader.infoHash)
|
||||||
persistDownloaders()
|
persistDownloaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistDownloaders() {
|
private void persistDownloaders() {
|
||||||
File downloadsFile = new File(home,"downloads.json")
|
File downloadsFile = new File(home,"downloads.json")
|
||||||
downloadsFile.withPrintWriter { writer ->
|
downloadsFile.withPrintWriter { writer ->
|
||||||
downloaders.each { downloader ->
|
downloaders.values().each { downloader ->
|
||||||
if (!downloader.cancelled) {
|
if (!downloader.cancelled) {
|
||||||
def json = [:]
|
def json = [:]
|
||||||
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
||||||
@@ -113,11 +181,21 @@ public class DownloadManager {
|
|||||||
}
|
}
|
||||||
json.destinations = destinations
|
json.destinations = destinations
|
||||||
|
|
||||||
json.hashList = Base64.encode(downloader.infoHash.hashList)
|
InfoHash infoHash = downloader.getInfoHash()
|
||||||
|
if (infoHash.hashList != null)
|
||||||
|
json.hashList = Base64.encode(infoHash.hashList)
|
||||||
|
else
|
||||||
|
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||||
|
|
||||||
|
json.paused = downloader.paused
|
||||||
writer.println(JsonOutput.toJson(json))
|
writer.println(JsonOutput.toJson(json))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
downloaders.values().each { it.stop() }
|
||||||
|
Downloader.executorService.shutdownNow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,12 @@ package com.muwire.core.download;
|
|||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import static com.muwire.core.util.DataUtil.readTillRN
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
@@ -16,19 +20,22 @@ import java.nio.file.Files
|
|||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class DownloadSession {
|
class DownloadSession {
|
||||||
|
|
||||||
private static int SAMPLES = 10
|
private static int SAMPLES = 10
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
private final String meB64
|
private final String meB64
|
||||||
private final Pieces downloaded, claimed
|
private final Pieces pieces
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final Endpoint endpoint
|
private final Endpoint endpoint
|
||||||
private final File file
|
private final File file
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final long fileLength
|
private final long fileLength
|
||||||
|
private final Set<Integer> available
|
||||||
private final MessageDigest digest
|
private final MessageDigest digest
|
||||||
|
|
||||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||||
@@ -36,16 +43,17 @@ class DownloadSession {
|
|||||||
|
|
||||||
private ByteBuffer mapped
|
private ByteBuffer mapped
|
||||||
|
|
||||||
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
|
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||||
int pieceSize, long fileLength) {
|
int pieceSize, long fileLength, Set<Integer> available) {
|
||||||
|
this.eventBus = eventBus
|
||||||
this.meB64 = meB64
|
this.meB64 = meB64
|
||||||
this.downloaded = downloaded
|
this.pieces = pieces
|
||||||
this.claimed = claimed
|
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.file = file
|
this.file = file
|
||||||
this.pieceSize = pieceSize
|
this.pieceSize = pieceSize
|
||||||
this.fileLength = fileLength
|
this.fileLength = fileLength
|
||||||
|
this.available = available
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("SHA-256")
|
digest = MessageDigest.getInstance("SHA-256")
|
||||||
} catch (NoSuchAlgorithmException impossible) {
|
} catch (NoSuchAlgorithmException impossible) {
|
||||||
@@ -64,19 +72,14 @@ class DownloadSession {
|
|||||||
InputStream is = endpoint.getInputStream()
|
InputStream is = endpoint.getInputStream()
|
||||||
|
|
||||||
int piece
|
int piece
|
||||||
while(true) {
|
if (available.isEmpty())
|
||||||
piece = downloaded.getRandomPiece()
|
piece = pieces.claim()
|
||||||
if (claimed.isMarked(piece)) {
|
else
|
||||||
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
|
piece = pieces.claim(new HashSet<>(available))
|
||||||
log.info("all pieces claimed")
|
if (piece == -1)
|
||||||
return false
|
return false
|
||||||
}
|
boolean unclaim = true
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
claimed.markDownloaded(piece)
|
|
||||||
|
|
||||||
log.info("will download piece $piece")
|
log.info("will download piece $piece")
|
||||||
|
|
||||||
long start = piece * pieceSize
|
long start = piece * pieceSize
|
||||||
@@ -85,91 +88,130 @@ class DownloadSession {
|
|||||||
|
|
||||||
String root = Base64.encode(infoHash.getRoot())
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
try {
|
||||||
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
|
||||||
|
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.flush()
|
os.flush()
|
||||||
String code = readTillRN(is)
|
String codeString = readTillRN(is)
|
||||||
if (code.startsWith("404 ")) {
|
int space = codeString.indexOf(' ')
|
||||||
|
if (space > 0)
|
||||||
|
codeString = codeString.substring(0, space)
|
||||||
|
|
||||||
|
int code = Integer.parseInt(codeString.trim())
|
||||||
|
|
||||||
|
if (code == 404) {
|
||||||
log.warning("file not found")
|
log.warning("file not found")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code.startsWith("416 ")) {
|
if (!(code == 200 || code == 416)) {
|
||||||
log.warning("range $start-$end cannot be satisfied")
|
|
||||||
return // leave endpoint open
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
Set<String> headers = new HashSet<>()
|
Map<String,String> headers = new HashMap<>()
|
||||||
String header
|
String header
|
||||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||||
headers.add(header)
|
int colon = header.indexOf(':')
|
||||||
|
if (colon == -1 || colon == header.length() - 1)
|
||||||
long receivedStart = -1
|
throw new IOException("invalid header $header")
|
||||||
long receivedEnd = -1
|
String key = header.substring(0, colon)
|
||||||
for (String receivedHeader : headers) {
|
String value = header.substring(colon + 1)
|
||||||
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
headers[key] = value.trim()
|
||||||
if (group.size() != 1) {
|
|
||||||
log.info("ignoring header $receivedHeader")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
receivedStart = Long.parseLong(group[0][1])
|
|
||||||
receivedEnd = Long.parseLong(group[0][2])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prase X-Alt if present
|
||||||
|
if (headers.containsKey("X-Alt")) {
|
||||||
|
headers["X-Alt"].split(",").each {
|
||||||
|
if (it.length() > 0) {
|
||||||
|
byte [] raw = Base64.decode(it)
|
||||||
|
Persona source = new Persona(new ByteArrayInputStream(raw))
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : infoHash, source : source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse X-Have if present
|
||||||
|
if (headers.containsKey("X-Have")) {
|
||||||
|
DataUtil.decodeXHave(headers["X-Have"]).each {
|
||||||
|
available.add(it)
|
||||||
|
}
|
||||||
|
if (!available.contains(piece))
|
||||||
|
return true // try again next time
|
||||||
|
} else {
|
||||||
|
if (code != 200)
|
||||||
|
throw new IOException("Code $code but no X-Have")
|
||||||
|
available.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code != 200)
|
||||||
|
return true
|
||||||
|
|
||||||
|
String range = headers["Content-Range"]
|
||||||
|
if (range == null)
|
||||||
|
throw new IOException("Code 200 but no Content-Range")
|
||||||
|
|
||||||
|
def group = (range =~ /^(\d+)-(\d+)$/)
|
||||||
|
if (group.size() != 1)
|
||||||
|
throw new IOException("invalid Content-Range header $range")
|
||||||
|
|
||||||
|
long receivedStart = Long.parseLong(group[0][1])
|
||||||
|
long receivedEnd = Long.parseLong(group[0][2])
|
||||||
|
|
||||||
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
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
FileChannel channel
|
||||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
try {
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
||||||
byte[] tmp = new byte[0x1 << 13]
|
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
||||||
while(mapped.hasRemaining()) {
|
|
||||||
if (mapped.remaining() < tmp.length)
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
tmp = new byte[mapped.remaining()]
|
while(mapped.hasRemaining()) {
|
||||||
int read = is.read(tmp)
|
if (mapped.remaining() < tmp.length)
|
||||||
if (read == -1)
|
tmp = new byte[mapped.remaining()]
|
||||||
throw new IOException()
|
int read = is.read(tmp)
|
||||||
synchronized(this) {
|
if (read == -1)
|
||||||
mapped.put(tmp, 0, read)
|
throw new IOException()
|
||||||
|
synchronized(this) {
|
||||||
if (timestamps.size() == SAMPLES) {
|
mapped.put(tmp, 0, read)
|
||||||
timestamps.removeFirst()
|
|
||||||
reads.removeFirst()
|
if (timestamps.size() == SAMPLES) {
|
||||||
|
timestamps.removeFirst()
|
||||||
|
reads.removeFirst()
|
||||||
|
}
|
||||||
|
timestamps.addLast(System.currentTimeMillis())
|
||||||
|
reads.addLast(read)
|
||||||
}
|
}
|
||||||
timestamps.addLast(System.currentTimeMillis())
|
|
||||||
reads.addLast(read)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapped.clear()
|
||||||
|
digest.update(mapped)
|
||||||
|
DataUtil.tryUnmap(mapped)
|
||||||
|
byte [] hash = digest.digest()
|
||||||
|
byte [] expected = new byte[32]
|
||||||
|
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||||
|
if (hash != expected)
|
||||||
|
throw new BadHashException()
|
||||||
|
} finally {
|
||||||
|
try { channel?.close() } catch (IOException ignore) {}
|
||||||
}
|
}
|
||||||
|
pieces.markDownloaded(piece)
|
||||||
mapped.clear()
|
unclaim = false
|
||||||
digest.update(mapped)
|
|
||||||
byte [] hash = digest.digest()
|
|
||||||
byte [] expected = new byte[32]
|
|
||||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
|
||||||
if (hash != expected)
|
|
||||||
throw new BadHashException()
|
|
||||||
|
|
||||||
downloaded.markDownloaded(piece)
|
|
||||||
} finally {
|
} finally {
|
||||||
claimed.clear(piece)
|
if (unclaim)
|
||||||
try { channel?.close() } catch (IOException ignore) {}
|
pieces.unclaim(piece)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -4,9 +4,13 @@ import com.muwire.core.InfoHash
|
|||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
@@ -14,14 +18,16 @@ import com.muwire.core.DownloadedFile
|
|||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.connection.I2PConnector
|
import com.muwire.core.connection.I2PConnector
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Downloader {
|
public class Downloader {
|
||||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, 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)
|
||||||
@@ -34,25 +40,28 @@ public class Downloader {
|
|||||||
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 pieces
|
||||||
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
|
||||||
|
private final File incompleteFile
|
||||||
final int pieceSizePow2
|
final int pieceSizePow2
|
||||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||||
|
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
|
||||||
|
|
||||||
|
|
||||||
private volatile boolean cancelled
|
private volatile boolean cancelled, paused
|
||||||
private volatile boolean eventFired
|
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||||
|
private boolean piecesFileClosed
|
||||||
|
|
||||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||||
Persona me, File file, long length, InfoHash infoHash,
|
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, Pieces pieces) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
this.downloadManager = downloadManager
|
this.downloadManager = downloadManager
|
||||||
@@ -62,18 +71,19 @@ 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.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||||
this.pieceSizePow2 = pieceSizePow2
|
this.pieceSizePow2 = pieceSizePow2
|
||||||
this.pieceSize = 1 << pieceSizePow2
|
this.pieceSize = 1 << pieceSizePow2
|
||||||
|
this.pieces = pieces
|
||||||
int nPieces
|
this.nPieces = pieces.nPieces
|
||||||
if (length % pieceSize == 0)
|
}
|
||||||
nPieces = length / pieceSize
|
|
||||||
else
|
public synchronized InfoHash getInfoHash() {
|
||||||
nPieces = length / pieceSize + 1
|
infoHash
|
||||||
this.nPieces = nPieces
|
}
|
||||||
|
|
||||||
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
claimed = new Pieces(nPieces)
|
this.infoHash = infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
@@ -92,20 +102,24 @@ public class Downloader {
|
|||||||
return
|
return
|
||||||
piecesFile.eachLine {
|
piecesFile.eachLine {
|
||||||
int piece = Integer.parseInt(it)
|
int piece = Integer.parseInt(it)
|
||||||
downloaded.markDownloaded(piece)
|
pieces.markDownloaded(piece)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void writePieces() {
|
void writePieces() {
|
||||||
piecesFile.withPrintWriter { writer ->
|
synchronized(piecesFile) {
|
||||||
downloaded.getDownloaded().each { piece ->
|
if (piecesFileClosed)
|
||||||
writer.println(piece)
|
return
|
||||||
|
piecesFile.withPrintWriter { writer ->
|
||||||
|
pieces.getDownloaded().each { piece ->
|
||||||
|
writer.println(piece)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long donePieces() {
|
public long donePieces() {
|
||||||
downloaded.donePieces()
|
pieces.donePieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,12 +137,15 @@ public class Downloader {
|
|||||||
public DownloadState getCurrentState() {
|
public DownloadState getCurrentState() {
|
||||||
if (cancelled)
|
if (cancelled)
|
||||||
return DownloadState.CANCELLED
|
return DownloadState.CANCELLED
|
||||||
|
if (paused)
|
||||||
|
return DownloadState.PAUSED
|
||||||
|
|
||||||
boolean allFinished = true
|
boolean allFinished = true
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
allFinished &= it.currentState == WorkerState.FINISHED
|
allFinished &= it.currentState == WorkerState.FINISHED
|
||||||
}
|
}
|
||||||
if (allFinished) {
|
if (allFinished) {
|
||||||
if (downloaded.isComplete())
|
if (pieces.isComplete())
|
||||||
return DownloadState.FINISHED
|
return DownloadState.FINISHED
|
||||||
return DownloadState.FAILED
|
return DownloadState.FAILED
|
||||||
}
|
}
|
||||||
@@ -145,11 +162,37 @@ 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()
|
||||||
|
synchronized(piecesFile) {
|
||||||
|
piecesFileClosed = true
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
|
incompleteFile.delete()
|
||||||
|
pieces.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
paused = true
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
activeWorkers.values().each {
|
activeWorkers.values().each {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
@@ -165,6 +208,8 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
|
paused = false
|
||||||
|
readPieces()
|
||||||
destinations.each { destination ->
|
destinations.each { destination ->
|
||||||
def worker = activeWorkers.get(destination)
|
def worker = activeWorkers.get(destination)
|
||||||
if (worker != null) {
|
if (worker != null) {
|
||||||
@@ -181,12 +226,21 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addSource(Destination d) {
|
||||||
|
if (activeWorkers.containsKey(d))
|
||||||
|
return
|
||||||
|
DownloadWorker newWorker = new DownloadWorker(d)
|
||||||
|
activeWorkers.put(d, newWorker)
|
||||||
|
executorService.submit(newWorker)
|
||||||
|
}
|
||||||
|
|
||||||
class DownloadWorker implements Runnable {
|
class DownloadWorker implements Runnable {
|
||||||
private final Destination destination
|
private final Destination destination
|
||||||
private volatile WorkerState currentState
|
private volatile WorkerState currentState
|
||||||
private volatile Thread downloadThread
|
private volatile Thread downloadThread
|
||||||
private Endpoint endpoint
|
private Endpoint endpoint
|
||||||
private volatile DownloadSession currentSession
|
private volatile DownloadSession currentSession
|
||||||
|
private final Set<Integer> available = new HashSet<>()
|
||||||
|
|
||||||
DownloadWorker(Destination destination) {
|
DownloadWorker(Destination destination) {
|
||||||
this.destination = destination
|
this.destination = destination
|
||||||
@@ -198,25 +252,44 @@ 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(!pieces.isComplete()) {
|
||||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
|
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||||
|
endpoint, incompleteFile, pieceSize, length, available)
|
||||||
requestPerformed = currentSession.request()
|
requestPerformed = currentSession.request()
|
||||||
if (!requestPerformed)
|
if (!requestPerformed)
|
||||||
break
|
break
|
||||||
|
successfulDestinations.add(endpoint.destination)
|
||||||
writePieces()
|
writePieces()
|
||||||
}
|
}
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||||
} finally {
|
} finally {
|
||||||
currentState = WorkerState.FINISHED
|
currentState = WorkerState.FINISHED
|
||||||
if (downloaded.isComplete() && !eventFired) {
|
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||||
piecesFile.delete()
|
synchronized(piecesFile) {
|
||||||
eventFired = true
|
piecesFileClosed = true
|
||||||
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, pieceSize, Collections.emptySet())),
|
piecesFile.delete()
|
||||||
downloader : Downloader.this)
|
}
|
||||||
}
|
try {
|
||||||
|
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
incompleteFile.delete()
|
||||||
|
}
|
||||||
|
eventBus.publish(
|
||||||
|
new FileDownloadedEvent(
|
||||||
|
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
class Pieces {
|
class Pieces {
|
||||||
private final BitSet bitSet
|
private final BitSet done, claimed
|
||||||
private final int nPieces
|
private final int nPieces
|
||||||
private final float ratio
|
private final float ratio
|
||||||
private final Random random = new Random()
|
private final Random random = new Random()
|
||||||
@@ -13,52 +13,74 @@ class Pieces {
|
|||||||
Pieces(int nPieces, float ratio) {
|
Pieces(int nPieces, float ratio) {
|
||||||
this.nPieces = nPieces
|
this.nPieces = nPieces
|
||||||
this.ratio = ratio
|
this.ratio = ratio
|
||||||
bitSet = new BitSet(nPieces)
|
done = new BitSet(nPieces)
|
||||||
|
claimed = new BitSet(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int getRandomPiece() {
|
synchronized int claim() {
|
||||||
int cardinality = bitSet.cardinality()
|
int claimedCardinality = claimed.cardinality()
|
||||||
if (cardinality == nPieces)
|
if (claimedCardinality == nPieces)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
// if fuller than ratio just do sequential
|
// if fuller than ratio just do sequential
|
||||||
if ( (1.0f * cardinality) / nPieces > ratio) {
|
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||||
return bitSet.nextClearBit(0)
|
int rv = claimed.nextClearBit(0)
|
||||||
|
claimed.set(rv)
|
||||||
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
int start = random.nextInt(nPieces)
|
int start = random.nextInt(nPieces)
|
||||||
if (bitSet.get(start))
|
if (claimed.get(start))
|
||||||
continue
|
continue
|
||||||
|
claimed.set(start)
|
||||||
return start
|
return start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDownloaded() {
|
synchronized int claim(Set<Integer> available) {
|
||||||
|
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||||
|
available.remove(i)
|
||||||
|
if (available.isEmpty())
|
||||||
|
return -1
|
||||||
|
List<Integer> toList = available.toList()
|
||||||
|
Collections.shuffle(toList)
|
||||||
|
int rv = toList[0]
|
||||||
|
claimed.set(rv)
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized def getDownloaded() {
|
||||||
def rv = []
|
def rv = []
|
||||||
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
|
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||||
rv << i
|
rv << i
|
||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void markDownloaded(int piece) {
|
synchronized void markDownloaded(int piece) {
|
||||||
bitSet.set(piece)
|
done.set(piece)
|
||||||
|
claimed.set(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void clear(int piece) {
|
synchronized void unclaim(int piece) {
|
||||||
bitSet.clear(piece)
|
claimed.clear(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isComplete() {
|
synchronized boolean isComplete() {
|
||||||
bitSet.cardinality() == nPieces
|
done.cardinality() == nPieces
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean isMarked(int piece) {
|
|
||||||
bitSet.get(piece)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int donePieces() {
|
synchronized int donePieces() {
|
||||||
bitSet.cardinality()
|
done.cardinality()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized boolean isDownloaded(int piece) {
|
||||||
|
done.get(piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void clearAll() {
|
||||||
|
done.clear()
|
||||||
|
claimed.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
class SourceDiscoveredEvent extends Event {
|
||||||
|
InfoHash infoHash
|
||||||
|
Persona source
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadCancelledEvent extends Event {
|
||||||
|
Downloader downloader
|
||||||
|
}
|
@@ -3,8 +3,11 @@ package com.muwire.core.download
|
|||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class UIDownloadEvent extends Event {
|
class UIDownloadEvent extends Event {
|
||||||
|
|
||||||
UIResultEvent[] result
|
UIResultEvent[] result
|
||||||
|
Set<Destination> sources
|
||||||
File target
|
File target
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadPausedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadResumedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class DirectoryUnsharedEvent extends Event {
|
||||||
|
File directory
|
||||||
|
}
|
@@ -0,0 +1,148 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import static java.nio.file.StandardWatchEventKinds.*
|
||||||
|
|
||||||
|
import java.nio.file.ClosedWatchServiceException
|
||||||
|
import java.nio.file.WatchEvent
|
||||||
|
import java.nio.file.WatchKey
|
||||||
|
import java.nio.file.WatchService
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.util.SystemVersion
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class DirectoryWatcher {
|
||||||
|
|
||||||
|
private static final long WAIT_TIME = 1000
|
||||||
|
|
||||||
|
private static final WatchEvent.Kind[] kinds
|
||||||
|
static {
|
||||||
|
if (SystemVersion.isMac())
|
||||||
|
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
|
||||||
|
else
|
||||||
|
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||||
|
}
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
|
private final FileManager fileManager
|
||||||
|
private final Thread watcherThread, publisherThread
|
||||||
|
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||||
|
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||||
|
private WatchService watchService
|
||||||
|
private volatile boolean shutdown
|
||||||
|
|
||||||
|
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||||
|
watcherThread.setDaemon(true)
|
||||||
|
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
|
||||||
|
publisherThread.setDaemon(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
|
watchService = FileSystems.getDefault().newWatchService()
|
||||||
|
watcherThread.start()
|
||||||
|
publisherThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
shutdown = true
|
||||||
|
watcherThread?.interrupt()
|
||||||
|
publisherThread?.interrupt()
|
||||||
|
watchService?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileSharedEvent(FileSharedEvent e) {
|
||||||
|
if (!e.file.isDirectory())
|
||||||
|
return
|
||||||
|
Path path = e.file.getCanonicalFile().toPath()
|
||||||
|
WatchKey wk = path.register(watchService, kinds)
|
||||||
|
watchedDirectories.put(e.file, wk)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||||
|
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||||
|
wk?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void watch() {
|
||||||
|
try {
|
||||||
|
while(!shutdown) {
|
||||||
|
WatchKey key = watchService.take()
|
||||||
|
key.pollEvents().each {
|
||||||
|
switch(it.kind()) {
|
||||||
|
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
|
||||||
|
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
|
||||||
|
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key.reset()
|
||||||
|
}
|
||||||
|
} catch (InterruptedException|ClosedWatchServiceException e) {
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void processCreated(Path parent, Path path) {
|
||||||
|
File f= join(parent, path)
|
||||||
|
log.fine("created entry $f")
|
||||||
|
if (f.isDirectory())
|
||||||
|
f.toPath().register(watchService, kinds)
|
||||||
|
else
|
||||||
|
waitingFiles.put(f, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processModified(Path parent, Path path) {
|
||||||
|
File f = join(parent, path)
|
||||||
|
log.fine("modified entry $f")
|
||||||
|
waitingFiles.put(f, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processDeleted(Path parent, Path path) {
|
||||||
|
File f = join(parent, path)
|
||||||
|
log.fine("deleted entry $f")
|
||||||
|
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||||
|
if (sf != null)
|
||||||
|
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File join(Path parent, Path path) {
|
||||||
|
File parentFile = parent.toFile().getCanonicalFile()
|
||||||
|
new File(parentFile, path.toFile().getName()).getCanonicalFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish() {
|
||||||
|
try {
|
||||||
|
while(!shutdown) {
|
||||||
|
Thread.sleep(WAIT_TIME)
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
def published = []
|
||||||
|
waitingFiles.each { file, timestamp ->
|
||||||
|
if (now - timestamp > WAIT_TIME) {
|
||||||
|
log.fine("publishing file $file")
|
||||||
|
eventBus.publish new FileSharedEvent(file : file)
|
||||||
|
published << file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
published.each {
|
||||||
|
waitingFiles.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,4 +7,10 @@ class FileHashedEvent extends Event {
|
|||||||
|
|
||||||
SharedFile sharedFile
|
SharedFile sharedFile
|
||||||
String error
|
String error
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
@@ -18,14 +19,16 @@ class FileHasher {
|
|||||||
/**
|
/**
|
||||||
* @param size of the file to be shared
|
* @param size of the file to be shared
|
||||||
* @return the size of each piece in power of 2
|
* @return the size of each piece in power of 2
|
||||||
|
* piece size is minimum 128 KBytees and maximum 16 MBytes in power of 2 steps (2^17 - 2^24)
|
||||||
|
* there can be up to 8192 pieces maximum per file
|
||||||
*/
|
*/
|
||||||
static int getPieceSize(long size) {
|
static int getPieceSize(long size) {
|
||||||
if (size <= 0x1 << 27)
|
if (size <= 0x1 << 30)
|
||||||
return 17
|
return 17
|
||||||
|
|
||||||
for (int i = 28; i <= 37; i++) {
|
for (int i = 31; i <= 37; i++) {
|
||||||
if (size <= 0x1L << i) {
|
if (size <= 0x1L << i) {
|
||||||
return i-10
|
return i-13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,7 @@ class FileHasher {
|
|||||||
for (int i = 0; i < numPieces - 1; i++) {
|
for (int i = 0; i < numPieces - 1; i++) {
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
|
DataUtil.tryUnmap(buf)
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
}
|
}
|
||||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||||
|
@@ -108,7 +108,7 @@ class FileManager {
|
|||||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||||
found = filter(found, e.oobInfohash)
|
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<>()
|
||||||
@@ -117,7 +117,7 @@ class FileManager {
|
|||||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||||
files = filter(sharedFiles, e.oobInfohash)
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +135,16 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||||
|
e.directory.listFiles().each {
|
||||||
|
if (it.isDirectory())
|
||||||
|
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||||
|
else {
|
||||||
|
SharedFile sf = fileToSharedFile.get(it)
|
||||||
|
if (sf != null)
|
||||||
|
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,4 +5,9 @@ import com.muwire.core.Event
|
|||||||
class FileSharedEvent extends Event {
|
class FileSharedEvent extends Event {
|
||||||
|
|
||||||
File file
|
File file
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return super.toString() + " file: "+file.getAbsolutePath()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ class HasherService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileSharedEvent(FileSharedEvent evt) {
|
void onFileSharedEvent(FileSharedEvent evt) {
|
||||||
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||||
return
|
return
|
||||||
executor.execute( { -> process(evt.file) } as Runnable)
|
executor.execute( { -> process(evt.file) } as Runnable)
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class HasherService {
|
|||||||
private void process(File f) {
|
private void process(File f) {
|
||||||
f = f.getCanonicalFile()
|
f = f.getCanonicalFile()
|
||||||
if (f.isDirectory()) {
|
if (f.isDirectory()) {
|
||||||
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
|
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||||
} else {
|
} else {
|
||||||
if (f.length() == 0) {
|
if (f.length() == 0) {
|
||||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||||
|
@@ -11,6 +11,7 @@ import com.muwire.core.EventBus
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Service
|
import com.muwire.core.Service
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
@@ -36,14 +37,14 @@ class PersisterService extends Service {
|
|||||||
timer = new Timer("file persister", true)
|
timer = new Timer("file persister", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
|
||||||
timer.schedule({load()} as TimerTask, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
|
}
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
if (location.exists() && location.isFile()) {
|
if (location.exists() && location.isFile()) {
|
||||||
def slurper = new JsonSlurper()
|
def slurper = new JsonSlurper()
|
||||||
@@ -62,7 +63,9 @@ class PersisterService extends Service {
|
|||||||
} 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
|
||||||
}
|
}
|
||||||
|
@@ -7,20 +7,25 @@ class Host {
|
|||||||
private static final int MAX_FAILURES = 3
|
private static final int MAX_FAILURES = 3
|
||||||
|
|
||||||
final Destination destination
|
final Destination destination
|
||||||
|
private final int clearInterval
|
||||||
int failures,successes
|
int failures,successes
|
||||||
|
long lastAttempt
|
||||||
|
|
||||||
public Host(Destination destination) {
|
public Host(Destination destination, int clearInterval) {
|
||||||
this.destination = destination
|
this.destination = destination
|
||||||
|
this.clearInterval = clearInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void onConnect() {
|
synchronized void onConnect() {
|
||||||
failures = 0
|
failures = 0
|
||||||
successes++
|
successes++
|
||||||
|
lastAttempt = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void onFailure() {
|
synchronized void onFailure() {
|
||||||
failures++
|
failures++
|
||||||
successes = 0
|
successes = 0
|
||||||
|
lastAttempt = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isFailed() {
|
synchronized boolean isFailed() {
|
||||||
@@ -34,4 +39,8 @@ class Host {
|
|||||||
synchronized void clearFailures() {
|
synchronized void clearFailures() {
|
||||||
failures = 0
|
failures = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void canTryAgain() {
|
||||||
|
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -52,7 +52,7 @@ class HostCache extends Service {
|
|||||||
hosts.get(e.destination).clearFailures()
|
hosts.get(e.destination).clearFailures()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Host host = new Host(e.destination)
|
Host host = new Host(e.destination, settings.hostClearInterval)
|
||||||
if (allowHost(host)) {
|
if (allowHost(host)) {
|
||||||
hosts.put(e.destination, host)
|
hosts.put(e.destination, host)
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ class HostCache extends Service {
|
|||||||
Destination dest = e.endpoint.destination
|
Destination dest = e.endpoint.destination
|
||||||
Host host = hosts.get(dest)
|
Host host = hosts.get(dest)
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
host = new Host(dest)
|
host = new Host(dest, settings.hostClearInterval)
|
||||||
hosts.put(dest, host)
|
hosts.put(dest, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +106,11 @@ class HostCache extends Service {
|
|||||||
storage.eachLine {
|
storage.eachLine {
|
||||||
def entry = slurper.parseText(it)
|
def entry = slurper.parseText(it)
|
||||||
Destination dest = new Destination(entry.destination)
|
Destination dest = new Destination(entry.destination)
|
||||||
Host host = new Host(dest)
|
Host host = new Host(dest, settings.hostClearInterval)
|
||||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||||
|
if (entry.lastAttempt != null)
|
||||||
|
host.lastAttempt = entry.lastAttempt
|
||||||
if (allowHost(host))
|
if (allowHost(host))
|
||||||
hosts.put(dest, host)
|
hosts.put(dest, host)
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,7 @@ class HostCache extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean allowHost(Host host) {
|
private boolean allowHost(Host host) {
|
||||||
if (host.isFailed())
|
if (host.isFailed() && !host.canTryAgain())
|
||||||
return false
|
return false
|
||||||
if (host.destination == myself)
|
if (host.destination == myself)
|
||||||
return false
|
return false
|
||||||
@@ -143,6 +145,7 @@ class HostCache extends Service {
|
|||||||
map.destination = dest.toBase64()
|
map.destination = dest.toBase64()
|
||||||
map.failures = host.failures
|
map.failures = host.failures
|
||||||
map.successes = host.successes
|
map.successes = host.successes
|
||||||
|
map.lastAttempt = host.lastAttempt
|
||||||
def json = JsonOutput.toJson(map)
|
def json = JsonOutput.toJson(map)
|
||||||
writer.println json
|
writer.println json
|
||||||
}
|
}
|
||||||
|
28
core/src/main/groovy/com/muwire/core/mesh/Mesh.groovy
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.muwire.core.mesh
|
||||||
|
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.Pieces
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
|
class Mesh {
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||||
|
private final Pieces pieces
|
||||||
|
|
||||||
|
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||||
|
this.infoHash = infoHash
|
||||||
|
this.pieces = pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Persona> getRandom(int n, Persona exclude) {
|
||||||
|
List<Persona> tmp = new ArrayList<>(sources)
|
||||||
|
tmp.remove(exclude)
|
||||||
|
Collections.shuffle(tmp)
|
||||||
|
if (tmp.size() < n)
|
||||||
|
return tmp
|
||||||
|
tmp[0..n-1]
|
||||||
|
}
|
||||||
|
}
|
102
core/src/main/groovy/com/muwire/core/mesh/MeshManager.groovy
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package com.muwire.core.mesh
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.Pieces
|
||||||
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class MeshManager {
|
||||||
|
|
||||||
|
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
|
||||||
|
private final FileManager fileManager
|
||||||
|
private final File home
|
||||||
|
private final MuWireSettings settings
|
||||||
|
|
||||||
|
MeshManager(FileManager fileManager, File home, MuWireSettings settings) {
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.home = home
|
||||||
|
this.settings = settings
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh get(InfoHash infoHash) {
|
||||||
|
meshes.get(infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
|
||||||
|
synchronized(meshes) {
|
||||||
|
if (meshes.containsKey(infoHash))
|
||||||
|
return meshes.get(infoHash)
|
||||||
|
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio)
|
||||||
|
if (fileManager.rootToFiles.containsKey(infoHash)) {
|
||||||
|
for (int i = 0; i < nPieces; i++)
|
||||||
|
pieces.markDownloaded(i)
|
||||||
|
}
|
||||||
|
Mesh rv = new Mesh(infoHash, pieces)
|
||||||
|
meshes.put(infoHash, rv)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
Mesh mesh = meshes.get(e.infoHash)
|
||||||
|
if (mesh == null)
|
||||||
|
return
|
||||||
|
mesh.sources.add(e.source)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
File meshFile = new File(home, "mesh.json")
|
||||||
|
synchronized(meshes) {
|
||||||
|
meshFile.withPrintWriter { writer ->
|
||||||
|
meshes.values().each { mesh ->
|
||||||
|
def json = [:]
|
||||||
|
json.timestamp = System.currentTimeMillis()
|
||||||
|
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
||||||
|
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||||
|
json.nPieces = mesh.pieces.nPieces
|
||||||
|
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
|
||||||
|
writer.println(JsonOutput.toJson(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
File meshFile = new File(home, "mesh.json")
|
||||||
|
if (!meshFile.exists())
|
||||||
|
return
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
JsonSlurper slurper = new JsonSlurper()
|
||||||
|
meshFile.eachLine {
|
||||||
|
def json = slurper.parseText(it)
|
||||||
|
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
|
||||||
|
return
|
||||||
|
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
|
||||||
|
Pieces pieces = new Pieces(json.nPieces, settings.downloadSequentialRatio)
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh(infoHash, pieces)
|
||||||
|
json.sources.each { source ->
|
||||||
|
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
|
||||||
|
mesh.sources.add(persona)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.xHave != null)
|
||||||
|
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
|
||||||
|
|
||||||
|
if (!mesh.sources.isEmpty())
|
||||||
|
meshes.put(infoHash, mesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import javax.naming.directory.InvalidSearchControlsException
|
import javax.naming.directory.InvalidSearchControlsException
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
@@ -7,13 +9,25 @@ import com.muwire.core.Persona
|
|||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class ResultsParser {
|
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)
|
||||||
@@ -47,9 +61,45 @@ class ResultsParser {
|
|||||||
size : size,
|
size : size,
|
||||||
infohash : parsedIH,
|
infohash : parsedIH,
|
||||||
pieceSize : pieceSize,
|
pieceSize : pieceSize,
|
||||||
|
sources : Collections.emptySet(),
|
||||||
uuid : uuid)
|
uuid : uuid)
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
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
|
||||||
|
|
||||||
|
Set<Destination> sources = Collections.emptySet()
|
||||||
|
if (json.sources != null)
|
||||||
|
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
|
||||||
|
|
||||||
|
return new UIResultEvent( sender : p,
|
||||||
|
name : name,
|
||||||
|
size : size,
|
||||||
|
infohash : new InfoHash(infoHash),
|
||||||
|
pieceSize : pieceSize,
|
||||||
|
sources : sources,
|
||||||
|
uuid: uuid)
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,10 @@ import java.util.concurrent.Executor
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ThreadFactory
|
import java.util.concurrent.ThreadFactory
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -46,25 +49,30 @@ 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()
|
int pieceSize = it.getPieceSize()
|
||||||
if (pieceSize == 0)
|
if (pieceSize == 0)
|
||||||
pieceSize = FileHasher.getPieceSize(length)
|
pieceSize = FileHasher.getPieceSize(length)
|
||||||
|
Set<Destination> suggested = Collections.emptySet()
|
||||||
|
if (it instanceof DownloadedFile)
|
||||||
|
suggested = it.sources
|
||||||
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 : pieceSize,
|
pieceSize : pieceSize,
|
||||||
uuid : uuid
|
uuid : uuid,
|
||||||
|
sources : suggested
|
||||||
)
|
)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,48 +80,58 @@ class ResultsSender {
|
|||||||
UUID uuid
|
UUID uuid
|
||||||
SharedFile [] results
|
SharedFile [] results
|
||||||
Destination target
|
Destination target
|
||||||
|
boolean oobInfohash
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
byte [] tmp = new byte[InfoHash.SIZE]
|
|
||||||
JsonOutput jsonOutput = new JsonOutput()
|
|
||||||
Endpoint endpoint = null;
|
|
||||||
try {
|
try {
|
||||||
endpoint = connector.connect(target)
|
byte [] tmp = new byte[InfoHash.SIZE]
|
||||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
JsonOutput jsonOutput = new JsonOutput()
|
||||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
Endpoint endpoint = null;
|
||||||
me.write(os)
|
try {
|
||||||
os.writeShort((short)results.length)
|
endpoint = connector.connect(target)
|
||||||
results.each {
|
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||||
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
def baos = new ByteArrayOutputStream()
|
me.write(os)
|
||||||
def daos = new DataOutputStream(baos)
|
os.writeShort((short)results.length)
|
||||||
daos.writeShort((short) name.length)
|
results.each {
|
||||||
daos.write(name)
|
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||||
daos.flush()
|
def baos = new ByteArrayOutputStream()
|
||||||
String encodedName = Base64.encode(baos.toByteArray())
|
def daos = new DataOutputStream(baos)
|
||||||
def obj = [:]
|
daos.writeShort((short) name.length)
|
||||||
obj.type = "Result"
|
daos.write(name)
|
||||||
obj.version = 1
|
daos.flush()
|
||||||
obj.name = encodedName
|
String encodedName = Base64.encode(baos.toByteArray())
|
||||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
def obj = [:]
|
||||||
obj.size = it.getFile().length()
|
obj.type = "Result"
|
||||||
obj.pieceSize = it.getPieceSize()
|
obj.version = oobInfohash ? 2 : 1
|
||||||
byte [] hashList = it.getInfoHash().getHashList()
|
obj.name = encodedName
|
||||||
def hashListB64 = []
|
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
obj.size = it.getFile().length()
|
||||||
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
obj.pieceSize = it.getPieceSize()
|
||||||
hashListB64 << Base64.encode(tmp)
|
if (!oobInfohash) {
|
||||||
}
|
byte [] hashList = it.getInfoHash().getHashList()
|
||||||
obj.hashList = hashListB64
|
def hashListB64 = []
|
||||||
|
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||||
|
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||||
|
hashListB64 << Base64.encode(tmp)
|
||||||
|
}
|
||||||
|
obj.hashList = hashListB64
|
||||||
|
}
|
||||||
|
|
||||||
def json = jsonOutput.toJson(obj)
|
if (it instanceof DownloadedFile)
|
||||||
os.writeShort((short)json.length())
|
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
|
||||||
|
def json = jsonOutput.toJson(obj)
|
||||||
|
os.writeShort((short)json.length())
|
||||||
|
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||||
|
}
|
||||||
|
os.flush()
|
||||||
|
} finally {
|
||||||
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
os.flush()
|
} catch (Exception e) {
|
||||||
} finally {
|
log.log(Level.WARNING, "problem sending results",e)
|
||||||
endpoint?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,10 @@ class SearchIndex {
|
|||||||
|
|
||||||
private static String[] split(String source) {
|
private static String[] split(String source) {
|
||||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||||
source.split(" ")
|
String [] split = source.split(" ")
|
||||||
|
def rv = []
|
||||||
|
split.each { if (it.length() > 0) rv << it }
|
||||||
|
rv.toArray(new String[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] search(List<String> terms) {
|
String[] search(List<String> terms) {
|
||||||
|
@@ -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) {
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.search
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIResultBatchEvent extends Event {
|
||||||
|
UUID uuid
|
||||||
|
UIResultEvent[] results
|
||||||
|
}
|
@@ -4,11 +4,19 @@ import com.muwire.core.Event
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class UIResultEvent extends Event {
|
class UIResultEvent extends Event {
|
||||||
Persona sender
|
Persona sender
|
||||||
|
Set<Destination> sources
|
||||||
UUID uuid
|
UUID uuid
|
||||||
String name
|
String name
|
||||||
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,15 @@ package com.muwire.core.update
|
|||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.SearchEvent
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
@@ -13,6 +21,7 @@ import net.i2p.client.I2PSessionMuxedListener
|
|||||||
import net.i2p.client.SendMessageOptions
|
import net.i2p.client.SendMessageOptions
|
||||||
import net.i2p.client.datagram.I2PDatagramDissector
|
import net.i2p.client.datagram.I2PDatagramDissector
|
||||||
import net.i2p.client.datagram.I2PDatagramMaker
|
import net.i2p.client.datagram.I2PDatagramMaker
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.util.VersionComparator
|
import net.i2p.util.VersionComparator
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
@@ -21,16 +30,24 @@ class UpdateClient {
|
|||||||
final I2PSession session
|
final I2PSession session
|
||||||
final String myVersion
|
final String myVersion
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
|
final FileManager fileManager
|
||||||
|
final Persona me
|
||||||
|
|
||||||
private final Timer timer
|
private final Timer timer
|
||||||
|
|
||||||
private long lastUpdateCheckTime
|
private long lastUpdateCheckTime
|
||||||
|
|
||||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
|
private volatile InfoHash updateInfoHash
|
||||||
|
private volatile String version, signer
|
||||||
|
private volatile boolean updateDownloading
|
||||||
|
|
||||||
|
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.session = session
|
this.session = session
|
||||||
this.myVersion = myVersion
|
this.myVersion = myVersion
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.me = me
|
||||||
timer = new Timer("update-client",true)
|
timer = new Timer("update-client",true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +60,24 @@ class UpdateClient {
|
|||||||
timer.cancel()
|
timer.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUIResultBatchEvent(UIResultBatchEvent results) {
|
||||||
|
if (results.results[0].infohash != updateInfoHash)
|
||||||
|
return
|
||||||
|
if (updateDownloading)
|
||||||
|
return
|
||||||
|
updateDownloading = true
|
||||||
|
def file = new File(settings.downloadLocation, results.results[0].name)
|
||||||
|
def downloadEvent = new UIDownloadEvent(result: results.results[0], sources : results.results[0].sources, target : file)
|
||||||
|
eventBus.publish(downloadEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
|
if (e.downloadedFile.infoHash != updateInfoHash)
|
||||||
|
return
|
||||||
|
updateDownloading = false
|
||||||
|
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
|
||||||
|
}
|
||||||
|
|
||||||
private void checkUpdate() {
|
private void checkUpdate() {
|
||||||
final long now = System.currentTimeMillis()
|
final long now = System.currentTimeMillis()
|
||||||
if (lastUpdateCheckTime > 0) {
|
if (lastUpdateCheckTime > 0) {
|
||||||
@@ -106,8 +141,32 @@ class UpdateClient {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("new version $payload.version available, publishing event")
|
String infoHash
|
||||||
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
|
if (settings.updateType == "jar") {
|
||||||
|
infoHash = payload.infoHash
|
||||||
|
} else
|
||||||
|
infoHash = payload[settings.updateType]
|
||||||
|
|
||||||
|
|
||||||
|
if (!settings.autoDownloadUpdate) {
|
||||||
|
log.info("new version $payload.version available, publishing event")
|
||||||
|
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash))
|
||||||
|
} else {
|
||||||
|
log.info("new version $payload.version available")
|
||||||
|
updateInfoHash = new InfoHash(Base64.decode(infoHash))
|
||||||
|
if (fileManager.rootToFiles.containsKey(updateInfoHash))
|
||||||
|
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
|
||||||
|
else {
|
||||||
|
updateDownloading = false
|
||||||
|
version = payload.version
|
||||||
|
signer = payload.signer
|
||||||
|
log.info("starting search for new version hash $payload.infoHash")
|
||||||
|
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
|
||||||
|
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
|
||||||
|
receivedOn : me.destination, originator : me)
|
||||||
|
eventBus.publish(queryEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"Invalid datagram",e)
|
log.log(Level.WARNING,"Invalid datagram",e)
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UpdateDownloadedEvent extends Event {
|
||||||
|
String version
|
||||||
|
String signer
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class ContentRequest extends Request {
|
||||||
|
Range range
|
||||||
|
int have
|
||||||
|
}
|
@@ -0,0 +1,122 @@
|
|||||||
|
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 java.util.stream.Collectors
|
||||||
|
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
class ContentUploader extends Uploader {
|
||||||
|
|
||||||
|
private final File file
|
||||||
|
private final ContentRequest request
|
||||||
|
private final Mesh mesh
|
||||||
|
private final int pieceSize
|
||||||
|
|
||||||
|
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
||||||
|
super(endpoint)
|
||||||
|
this.file = file
|
||||||
|
this.request = request
|
||||||
|
this.mesh = mesh
|
||||||
|
this.pieceSize = pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
Range range = request.getRange()
|
||||||
|
boolean satisfiable = true
|
||||||
|
final long length = file.length()
|
||||||
|
if (range.start >= length || range.end >= length)
|
||||||
|
satisfiable = false
|
||||||
|
if (satisfiable) {
|
||||||
|
int startPiece = range.start / (0x1 << pieceSize)
|
||||||
|
int endPiece = range.end / (0x1 << pieceSize)
|
||||||
|
for (int i = startPiece; i <= endPiece; i++)
|
||||||
|
satisfiable &= mesh.pieces.isDownloaded(i)
|
||||||
|
}
|
||||||
|
if (!satisfiable) {
|
||||||
|
os.write("416 Range Not Satisfiable\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
writeMesh(request.downloader)
|
||||||
|
os.write("\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".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
writeMesh(request.downloader)
|
||||||
|
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
FileChannel channel = null
|
||||||
|
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()
|
||||||
|
synchronized(this) {
|
||||||
|
DataUtil.tryUnmap(mapped)
|
||||||
|
mapped = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMesh(Persona toExclude) {
|
||||||
|
String xHave = DataUtil.encodeXHave(mesh.pieces.getDownloaded(), mesh.pieces.nPieces)
|
||||||
|
endpoint.getOutputStream().write("X-Have: $xHave\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
Set<Persona> sources = mesh.getRandom(3, toExclude)
|
||||||
|
if (!sources.isEmpty()) {
|
||||||
|
String xAlts = sources.stream().map({ it.toBase64() }).collect(Collectors.joining(","))
|
||||||
|
endpoint.getOutputStream().write("X-Alt: $xAlts\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
if (mapped == null)
|
||||||
|
return 0
|
||||||
|
int position = mapped.position()
|
||||||
|
int total = request.getRange().end - request.getRange().start
|
||||||
|
(int)(position * 100.0 / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDonePieces() {
|
||||||
|
return request.have;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalPieces() {
|
||||||
|
return mesh.pieces.nPieces;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class HashListRequest extends Request {
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
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())
|
||||||
|
this.request = request
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Hash list for " + Base64.encode(infoHash.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDonePieces() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalPieces() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
|
|||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
@@ -16,57 +17,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 +49,75 @@ 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)
|
|
||||||
|
int have = 0
|
||||||
|
if (headers.containsKey("X-Have")) {
|
||||||
|
def encoded = headers["X-Have"].trim()
|
||||||
|
have = DataUtil.decodeXHave(encoded).size()
|
||||||
|
}
|
||||||
|
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||||
|
headers : headers, downloader : downloader, have : have)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,12 @@ import com.muwire.core.EventBus
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.download.DownloadManager
|
||||||
|
import com.muwire.core.download.Downloader
|
||||||
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
import com.muwire.core.files.FileManager
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
@@ -15,17 +20,22 @@ import net.i2p.data.Base64
|
|||||||
public class UploadManager {
|
public class UploadManager {
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
private final FileManager fileManager
|
private final FileManager fileManager
|
||||||
|
private final MeshManager meshManager
|
||||||
|
private final DownloadManager downloadManager
|
||||||
|
|
||||||
public UploadManager() {}
|
public UploadManager() {}
|
||||||
|
|
||||||
public UploadManager(EventBus eventBus, FileManager fileManager) {
|
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||||
|
MeshManager meshManager, DownloadManager downloadManager) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
|
this.meshManager = meshManager
|
||||||
|
this.downloadManager = downloadManager
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -44,8 +54,10 @@ public class UploadManager {
|
|||||||
log.info("Responding to upload request for root $infoHashString")
|
log.info("Responding to upload request for root $infoHashString")
|
||||||
|
|
||||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||||
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||||
log.info "file not found"
|
log.info "file not found"
|
||||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
e.getOutputStream().flush()
|
e.getOutputStream().flush()
|
||||||
@@ -61,13 +73,31 @@ public class UploadManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
|
ContentRequest request = Request.parseContentRequest(infoHash, 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)
|
|
||||||
|
if (request.have > 0)
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||||
|
|
||||||
|
Mesh mesh
|
||||||
|
File file
|
||||||
|
int pieceSize
|
||||||
|
if (downloader != null) {
|
||||||
|
mesh = meshManager.get(infoHash)
|
||||||
|
file = downloader.incompleteFile
|
||||||
|
pieceSize = downloader.pieceSizePow2
|
||||||
|
} else {
|
||||||
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||||
|
file = sharedFile.file
|
||||||
|
pieceSize = sharedFile.pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
Uploader uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
@@ -75,7 +105,131 @@ 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)
|
||||||
|
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||||
|
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (downloader == null && (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(infoHash, e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash fullInfoHash
|
||||||
|
if (downloader == null) {
|
||||||
|
fullInfoHash = sharedFiles.iterator().next().infoHash
|
||||||
|
} else {
|
||||||
|
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||||
|
if (hashList != null && hashList.length > 0)
|
||||||
|
fullInfoHash = downloader.getInfoHash()
|
||||||
|
else {
|
||||||
|
log.info("infohash not found in downloader")
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uploader uploader = new HashListUploader(e, fullInfoHash, 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)
|
||||||
|
infoHash = new InfoHash(infoHashRoot)
|
||||||
|
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
if (downloader == null && (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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.have > 0)
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||||
|
|
||||||
|
Mesh mesh
|
||||||
|
File file
|
||||||
|
int pieceSize
|
||||||
|
if (downloader != null) {
|
||||||
|
mesh = meshManager.get(infoHash)
|
||||||
|
file = downloader.incompleteFile
|
||||||
|
pieceSize = downloader.pieceSizePow2
|
||||||
|
} else {
|
||||||
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||||
|
file = sharedFile.file
|
||||||
|
pieceSize = sharedFile.pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,52 +8,32 @@ 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
|
||||||
mapped.position()
|
mapped.position()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer between 0 and 100
|
||||||
|
*/
|
||||||
|
abstract int getProgress();
|
||||||
|
|
||||||
|
abstract String getDownloader();
|
||||||
|
|
||||||
|
abstract int getDonePieces();
|
||||||
|
|
||||||
|
abstract int getTotalPieces()
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
package com.muwire.core.util
|
package com.muwire.core.util
|
||||||
|
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class DataUtil {
|
class DataUtil {
|
||||||
|
|
||||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||||
@@ -79,4 +84,73 @@ class DataUtil {
|
|||||||
}
|
}
|
||||||
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||||
|
int bytes = totalPieces / 8
|
||||||
|
if (totalPieces % 8 != 0)
|
||||||
|
bytes++
|
||||||
|
byte[] raw = new byte[bytes]
|
||||||
|
pieces.each {
|
||||||
|
int byteIdx = it / 8
|
||||||
|
int offset = it % 8
|
||||||
|
int mask = 0x80 >>> offset
|
||||||
|
raw[byteIdx] |= mask
|
||||||
|
}
|
||||||
|
Base64.encode(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> decodeXHave(String xHave) {
|
||||||
|
byte [] availablePieces = Base64.decode(xHave)
|
||||||
|
List<Integer> available = new ArrayList<>()
|
||||||
|
availablePieces.eachWithIndex {b, i ->
|
||||||
|
for (int j = 0; j < 8 ; j++) {
|
||||||
|
byte mask = 0x80 >>> j
|
||||||
|
if ((b & mask) == mask) {
|
||||||
|
available.add(i * 8 + j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
available
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Exception findRoot(Exception e) {
|
||||||
|
while(e.getCause() != null)
|
||||||
|
e = e.getCause()
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void tryUnmap(ByteBuffer cb) {
|
||||||
|
if (cb==null || !cb.isDirect()) return;
|
||||||
|
// we could use this type cast and call functions without reflection code,
|
||||||
|
// but static import from sun.* package is risky for non-SUN virtual machine.
|
||||||
|
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
|
||||||
|
|
||||||
|
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
|
||||||
|
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
|
||||||
|
try {
|
||||||
|
if (isOldJDK) {
|
||||||
|
Method cleaner = cb.getClass().getMethod("cleaner");
|
||||||
|
cleaner.setAccessible(true);
|
||||||
|
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
|
||||||
|
clean.setAccessible(true);
|
||||||
|
clean.invoke(cleaner.invoke(cb));
|
||||||
|
} else {
|
||||||
|
Class unsafeClass;
|
||||||
|
try {
|
||||||
|
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||||
|
} catch(Exception ex) {
|
||||||
|
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
|
||||||
|
// but that method should be added if sun.misc.Unsafe is removed.
|
||||||
|
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
|
||||||
|
}
|
||||||
|
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
|
||||||
|
clean.setAccessible(true);
|
||||||
|
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||||
|
theUnsafeField.setAccessible(true);
|
||||||
|
Object theUnsafe = theUnsafeField.get(null);
|
||||||
|
clean.invoke(theUnsafe, cb);
|
||||||
|
}
|
||||||
|
} catch(Exception ex) { }
|
||||||
|
cb = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core;
|
package com.muwire.core;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import net.i2p.data.Destination;
|
import net.i2p.data.Destination;
|
||||||
@@ -9,7 +10,8 @@ public class DownloadedFile extends SharedFile {
|
|||||||
|
|
||||||
private final Set<Destination> sources;
|
private final Set<Destination> sources;
|
||||||
|
|
||||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
|
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
||||||
|
throws IOException {
|
||||||
super(file, infoHash, pieceSize);
|
super(file, infoHash, pieceSize);
|
||||||
this.sources = sources;
|
this.sources = sources;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core;
|
package com.muwire.core;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class SharedFile {
|
public class SharedFile {
|
||||||
|
|
||||||
@@ -8,10 +9,15 @@ public class SharedFile {
|
|||||||
private final InfoHash infoHash;
|
private final InfoHash infoHash;
|
||||||
private final int pieceSize;
|
private final int pieceSize;
|
||||||
|
|
||||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) {
|
private final String cachedPath;
|
||||||
|
private final long cachedLength;
|
||||||
|
|
||||||
|
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.infoHash = infoHash;
|
this.infoHash = infoHash;
|
||||||
this.pieceSize = pieceSize;
|
this.pieceSize = pieceSize;
|
||||||
|
this.cachedPath = file.getAbsolutePath();
|
||||||
|
this.cachedLength = file.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
public File getFile() {
|
||||||
@@ -25,4 +31,34 @@ public class SharedFile {
|
|||||||
public int getPieceSize() {
|
public int getPieceSize() {
|
||||||
return pieceSize;
|
return pieceSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getNPieces() {
|
||||||
|
long length = file.length();
|
||||||
|
int rawPieceSize = 0x1 << pieceSize;
|
||||||
|
int rv = (int) (length / rawPieceSize);
|
||||||
|
if (length % rawPieceSize != 0)
|
||||||
|
rv++;
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCachedPath() {
|
||||||
|
return cachedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCachedLength() {
|
||||||
|
return cachedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return file.hashCode() ^ infoHash.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof SharedFile))
|
||||||
|
return false;
|
||||||
|
SharedFile other = (SharedFile)o;
|
||||||
|
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,30 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail
|
||||||
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.Personas
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
import static com.muwire.core.util.DataUtil.readTillRN
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
import static com.muwire.core.util.DataUtil.encodeXHave
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
class DownloadSessionTest {
|
class DownloadSessionTest {
|
||||||
|
|
||||||
|
private EventBus eventBus
|
||||||
private File source, target
|
private File source, target
|
||||||
private InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private Endpoint endpoint
|
private Endpoint endpoint
|
||||||
private Pieces pieces, claimed
|
private Pieces pieces
|
||||||
private String rootBase64
|
private String rootBase64
|
||||||
|
|
||||||
private DownloadSession session
|
private DownloadSession session
|
||||||
@@ -24,6 +33,16 @@ class DownloadSessionTest {
|
|||||||
private InputStream fromDownloader, fromUploader
|
private InputStream fromDownloader, fromUploader
|
||||||
private OutputStream toDownloader, toUploader
|
private OutputStream toDownloader, toUploader
|
||||||
|
|
||||||
|
private volatile boolean performed
|
||||||
|
private Set<Integer> available = new ConcurrentHashSet<>()
|
||||||
|
private volatile IOException thrown
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
eventBus = new EventBus()
|
||||||
|
}
|
||||||
|
|
||||||
private void initSession(int size, def claimedPieces = []) {
|
private void initSession(int size, def claimedPieces = []) {
|
||||||
Random r = new Random()
|
Random r = new Random()
|
||||||
byte [] content = new byte[size]
|
byte [] content = new byte[size]
|
||||||
@@ -48,8 +67,7 @@ class DownloadSessionTest {
|
|||||||
else
|
else
|
||||||
nPieces = size / pieceSize + 1
|
nPieces = size / pieceSize + 1
|
||||||
pieces = new Pieces(nPieces)
|
pieces = new Pieces(nPieces)
|
||||||
claimed = new Pieces(nPieces)
|
claimedPieces.each {pieces.claimed.set(it)}
|
||||||
claimedPieces.each {claimed.markDownloaded(it)}
|
|
||||||
|
|
||||||
fromDownloader = new PipedInputStream()
|
fromDownloader = new PipedInputStream()
|
||||||
fromUploader = new PipedInputStream()
|
fromUploader = new PipedInputStream()
|
||||||
@@ -57,12 +75,20 @@ class DownloadSessionTest {
|
|||||||
toUploader = new PipedOutputStream(fromDownloader)
|
toUploader = new PipedOutputStream(fromDownloader)
|
||||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||||
|
|
||||||
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
|
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
|
||||||
downloadThread = new Thread( { session.request() } as Runnable)
|
downloadThread = new Thread( { perform() } as Runnable)
|
||||||
downloadThread.setDaemon(true)
|
downloadThread.setDaemon(true)
|
||||||
downloadThread.start()
|
downloadThread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void perform() {
|
||||||
|
try {
|
||||||
|
performed = session.request()
|
||||||
|
} catch (IOException e) {
|
||||||
|
thrown = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void teardown() {
|
public void teardown() {
|
||||||
source?.delete()
|
source?.delete()
|
||||||
@@ -77,6 +103,7 @@ class DownloadSessionTest {
|
|||||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
assert "Range: 0-19" == readTillRN(fromDownloader)
|
assert "Range: 0-19" == readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
toDownloader.write("200 OK\r\n".bytes)
|
toDownloader.write("200 OK\r\n".bytes)
|
||||||
@@ -88,6 +115,9 @@ class DownloadSessionTest {
|
|||||||
|
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
assert target.bytes == source.bytes
|
assert target.bytes == source.bytes
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -99,6 +129,7 @@ class DownloadSessionTest {
|
|||||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
toDownloader.write("200 OK\r\n".bytes)
|
toDownloader.write("200 OK\r\n".bytes)
|
||||||
@@ -109,6 +140,9 @@ class DownloadSessionTest {
|
|||||||
Thread.sleep(150)
|
Thread.sleep(150)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
assert target.bytes == source.bytes
|
assert target.bytes == source.bytes
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -126,6 +160,7 @@ class DownloadSessionTest {
|
|||||||
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
||||||
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
||||||
|
|
||||||
|
readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
@@ -139,6 +174,9 @@ class DownloadSessionTest {
|
|||||||
Thread.sleep(150)
|
Thread.sleep(150)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
assert 1 == pieces.donePieces()
|
assert 1 == pieces.donePieces()
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -146,7 +184,10 @@ class DownloadSessionTest {
|
|||||||
initSession(20, [0])
|
initSession(20, [0])
|
||||||
long now = System.currentTimeMillis()
|
long now = System.currentTimeMillis()
|
||||||
downloadThread.join(100)
|
downloadThread.join(100)
|
||||||
assert 100 > (System.currentTimeMillis() - now)
|
assert 100 >= (System.currentTimeMillis() - now)
|
||||||
|
assert !performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -154,7 +195,7 @@ class DownloadSessionTest {
|
|||||||
int pieceSize = FileHasher.getPieceSize(1)
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
int size = (1 << pieceSize) * 10
|
int size = (1 << pieceSize) * 10
|
||||||
initSession(size, [1,2,3,4,5,6,7,8,9])
|
initSession(size, [1,2,3,4,5,6,7,8,9])
|
||||||
assert !claimed.isMarked(0)
|
assert !pieces.claimed.get(0)
|
||||||
|
|
||||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
String range = readTillRN(fromDownloader)
|
String range = readTillRN(fromDownloader)
|
||||||
@@ -162,7 +203,131 @@ class DownloadSessionTest {
|
|||||||
int start = Integer.parseInt(matcher[0][1])
|
int start = Integer.parseInt(matcher[0][1])
|
||||||
int end = Integer.parseInt(matcher[0][2])
|
int end = Integer.parseInt(matcher[0][2])
|
||||||
|
|
||||||
assert claimed.isMarked(0)
|
assert pieces.claimed.get(0)
|
||||||
assert start == 0 && end == (1 << pieceSize) - 1
|
assert start == 0 && end == (1 << pieceSize) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416NoHave() {
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert !performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416Have() {
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([0], 1)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert performed
|
||||||
|
assert available.contains(0)
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416Have2Pieces() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) + 1
|
||||||
|
initSession(size)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([1], 2)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert performed
|
||||||
|
assert available.contains(1)
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test200TwoPieces1Available() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) * 9 + 1
|
||||||
|
initSession(size)
|
||||||
|
|
||||||
|
Set<String> headers = readAllHeaders(fromDownloader)
|
||||||
|
def matcher = null
|
||||||
|
headers.each {
|
||||||
|
if (it.startsWith("Range"))
|
||||||
|
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
}
|
||||||
|
assert matcher.groupCount() > 0
|
||||||
|
int start = Integer.parseInt(matcher[0][1])
|
||||||
|
int end = Integer.parseInt(matcher[0][2])
|
||||||
|
|
||||||
|
if (start == 0)
|
||||||
|
fail("inconlcusive")
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it \r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([0],2)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
downloadThread.join()
|
||||||
|
|
||||||
|
assert performed
|
||||||
|
performed = false
|
||||||
|
assert available.contains(0)
|
||||||
|
assert thrown == null
|
||||||
|
|
||||||
|
// request same session
|
||||||
|
downloadThread = new Thread( { perform() } as Runnable)
|
||||||
|
downloadThread.setDaemon(true)
|
||||||
|
downloadThread.start()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
|
||||||
|
headers = readAllHeaders(fromDownloader)
|
||||||
|
matcher = null
|
||||||
|
headers.each {
|
||||||
|
if (it.startsWith("Range"))
|
||||||
|
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
}
|
||||||
|
assert matcher.groupCount() > 0
|
||||||
|
start = Integer.parseInt(matcher[0][1])
|
||||||
|
end = Integer.parseInt(matcher[0][2])
|
||||||
|
assert start == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testXAlt() throws Exception {
|
||||||
|
Personas personas = new Personas()
|
||||||
|
def sources = []
|
||||||
|
def listener = new Object() {
|
||||||
|
public void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
sources << e.source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, listener)
|
||||||
|
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Alt: ${personas.persona1.toBase64()},${personas.persona2.toBase64()}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert sources.contains(personas.persona1)
|
||||||
|
assert sources.contains(personas.persona2)
|
||||||
|
assert 2 == sources.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> readAllHeaders(InputStream is) {
|
||||||
|
Set<String> rv = new HashSet<>()
|
||||||
|
String header
|
||||||
|
while((header = readTillRN(is)) != "")
|
||||||
|
rv.add(header)
|
||||||
|
rv
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ class PiecesTest {
|
|||||||
public void testSinglePiece() {
|
public void testSinglePiece() {
|
||||||
pieces = new Pieces(1)
|
pieces = new Pieces(1)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
assert pieces.getRandomPiece() == 0
|
assert pieces.claim() == 0
|
||||||
pieces.markDownloaded(0)
|
pieces.markDownloaded(0)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,28 @@ class PiecesTest {
|
|||||||
public void testTwoPieces() {
|
public void testTwoPieces() {
|
||||||
pieces = new Pieces(2)
|
pieces = new Pieces(2)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
int piece = pieces.getRandomPiece()
|
int piece = pieces.claim()
|
||||||
assert piece == 0 || piece == 1
|
assert piece == 0 || piece == 1
|
||||||
pieces.markDownloaded(piece)
|
pieces.markDownloaded(piece)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
int piece2 = pieces.getRandomPiece()
|
int piece2 = pieces.claim()
|
||||||
assert piece != piece2
|
assert piece != piece2
|
||||||
pieces.markDownloaded(piece2)
|
pieces.markDownloaded(piece2)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimAvailable() {
|
||||||
|
pieces = new Pieces(2)
|
||||||
|
int claimed = pieces.claim([0].toSet())
|
||||||
|
assert claimed == 0
|
||||||
|
assert -1 == pieces.claim([0].toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimNoneAvailable() {
|
||||||
|
pieces = new Pieces(20)
|
||||||
|
int claimed = pieces.claim()
|
||||||
|
assert -1 == pieces.claim([claimed].toSet())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
void testPieceSize() {
|
void testPieceSize() {
|
||||||
assert 17 == FileHasher.getPieceSize(1000000)
|
assert 17 == FileHasher.getPieceSize(1000000)
|
||||||
assert 17 == FileHasher.getPieceSize(100000000)
|
assert 17 == FileHasher.getPieceSize(100000000)
|
||||||
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||||
shouldFail IllegalArgumentException, {
|
shouldFail IllegalArgumentException, {
|
||||||
FileHasher.getPieceSize(Long.MAX_VALUE)
|
FileHasher.getPieceSize(Long.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ class HasherServiceTest {
|
|||||||
hasher = new FileHasher()
|
hasher = new FileHasher()
|
||||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||||
eventBus.register(FileHashedEvent.class, listener)
|
eventBus.register(FileHashedEvent.class, listener)
|
||||||
|
eventBus.register(FileSharedEvent.class, service)
|
||||||
service.start()
|
service.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json
|
persisted.write json
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
@@ -121,7 +121,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json
|
persisted.write json
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
@@ -163,7 +163,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.append "$json2\n"
|
persisted.append "$json2\n"
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 2
|
assert listener.publishedFiles.size() == 2
|
||||||
@@ -195,7 +195,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json1
|
persisted.write json1
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
|
@@ -58,7 +58,7 @@ class PersisterServiceSavingTest {
|
|||||||
sf = new SharedFile(f, ih, 0)
|
sf = new SharedFile(f, ih, 0)
|
||||||
|
|
||||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(1500)
|
Thread.sleep(1500)
|
||||||
|
|
||||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||||
@@ -77,7 +77,7 @@ class PersisterServiceSavingTest {
|
|||||||
sf = new DownloadedFile(f, ih, 0, 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.onUILoadedEvent(null)
|
||||||
Thread.sleep(1500)
|
Thread.sleep(1500)
|
||||||
|
|
||||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||||
|
@@ -83,4 +83,11 @@ class SearchIndexTest {
|
|||||||
assert found.size() == 1
|
assert found.size() == 1
|
||||||
assert found.contains("b c.d")
|
assert found.contains("b c.d")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDuplicateTerm() {
|
||||||
|
initIndex(["MuWire-0.3.3.jar"])
|
||||||
|
def found = index.search(["muwire", "0", "3", "jar"])
|
||||||
|
assert found.size() == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
|
|||||||
|
|
||||||
class RequestParsingTest {
|
class RequestParsingTest {
|
||||||
|
|
||||||
Request request
|
ContentRequest request
|
||||||
|
|
||||||
private void fromString(String requestString) {
|
private void fromString(String requestString) {
|
||||||
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
||||||
request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
|
request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void failed(String requestString) {
|
private static void failed(String requestString) {
|
||||||
try {
|
try {
|
||||||
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
||||||
Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
|
Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
|
||||||
assert false
|
assert false
|
||||||
} catch (IOException expected) {}
|
} catch (IOException expected) {}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ class UploaderTest {
|
|||||||
InputStream is
|
InputStream is
|
||||||
OutputStream os
|
OutputStream os
|
||||||
|
|
||||||
Request request
|
ContentRequest request
|
||||||
Uploader uploader
|
Uploader uploader
|
||||||
|
|
||||||
byte[] inFile
|
byte[] inFile
|
||||||
@@ -52,7 +52,7 @@ class UploaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startUpload() {
|
private void startUpload() {
|
||||||
uploader = new Uploader(file, request, endpoint)
|
uploader = new ContentUploader(file, request, endpoint)
|
||||||
uploadThread = new Thread(uploader.respond() as Runnable)
|
uploadThread = new Thread(uploader.respond() as Runnable)
|
||||||
uploadThread.setDaemon(true)
|
uploadThread.setDaemon(true)
|
||||||
uploadThread.start()
|
uploadThread.start()
|
||||||
@@ -77,7 +77,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSmallFile() {
|
public void testSmallFile() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(0,19))
|
request = new ContentRequest(range : new Range(0,19))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "200 OK" == readUntilRN()
|
assert "200 OK" == readUntilRN()
|
||||||
assert "Content-Range: 0-19" == readUntilRN()
|
assert "Content-Range: 0-19" == readUntilRN()
|
||||||
@@ -92,7 +92,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testRequestMiddle() {
|
public void testRequestMiddle() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(5,15))
|
request = new ContentRequest(range : new Range(5,15))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "200 OK" == readUntilRN()
|
assert "200 OK" == readUntilRN()
|
||||||
assert "Content-Range: 5-15" == readUntilRN()
|
assert "Content-Range: 5-15" == readUntilRN()
|
||||||
@@ -108,7 +108,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testOutOfRange() {
|
public void testOutOfRange() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(0,20))
|
request = new ContentRequest(range : new Range(0,20))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "416 Range Not Satisfiable" == readUntilRN()
|
assert "416 Range Not Satisfiable" == readUntilRN()
|
||||||
assert "" == readUntilRN()
|
assert "" == readUntilRN()
|
||||||
@@ -118,7 +118,7 @@ class UploaderTest {
|
|||||||
public void testLargeFile() {
|
public void testLargeFile() {
|
||||||
final int length = 0x1 << 14
|
final int length = 0x1 << 14
|
||||||
fillFile(length)
|
fillFile(length)
|
||||||
request = new Request(range : new Range(0, length - 1))
|
request = new ContentRequest(range : new Range(0, length - 1))
|
||||||
startUpload()
|
startUpload()
|
||||||
readUntilRN()
|
readUntilRN()
|
||||||
readUntilRN()
|
readUntilRN()
|
||||||
|
@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
|
|||||||
|
|
||||||
### Mesh management
|
### Mesh management
|
||||||
|
|
||||||
Download mesh management is identical to Gnutella, except instead of ip addresses MuWire personas are used. [More information](http://rfc-gnutella.sourceforge.net/developer/tmp/download-mesh.html)
|
Download mesh management is a simplified version of Gnutella's "Alternate Location" system. For more information see the "download-mesh" document.
|
||||||
|
|
||||||
### In-Network updates
|
### In-Network updates
|
||||||
|
|
||||||
|
15
doc/download-mesh.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Download Mesh / Partial Sharing
|
||||||
|
|
||||||
|
MuWire uses a system similar to Gnutella's "Alternate Location" download mesh management system, however it is simplified to account for I2P's strengths and borrows a bit from BitTorrent's "Have" message.
|
||||||
|
|
||||||
|
### "X-Have" header
|
||||||
|
|
||||||
|
With every request a downloader makes it sends an "X-Have" header containing the Base64-encoded representation of a bitfield where bits set to 1 represent pieces of the file that the downloader already has. To make partial file sharing possible, if the uploader does not have the complete file it also sends this header in every response. If the header is missing it is assumed the uploader has the complete file.
|
||||||
|
|
||||||
|
### "X-Alt" header
|
||||||
|
|
||||||
|
The uploader can recommend other uploaders to the downloader via the "X-Alt" header. The format of this header is a comma-separated list of Base64-encoded Personas that have previously reported having at least one piece of the file to the uploader via the "X-Have" header.
|
||||||
|
|
||||||
|
### Differences from Gnutella
|
||||||
|
|
||||||
|
Unlike Gnutella the uploader is the sole repository where possible sources of the file are tracked. There is no negative "X-Nalt" header to prevent attacking the download mesh by mass downvoting of sources.
|
@@ -181,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,8 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.1.4
|
version = 0.4.4
|
||||||
groovyVersion = 2.4.15
|
groovyVersion = 2.4.15
|
||||||
slf4jVersion = 1.7.25
|
slf4jVersion = 1.7.25
|
||||||
spockVersion = 1.1-groovy-2.4
|
spockVersion = 1.1-groovy-2.4
|
||||||
|
|
||||||
|
sourceCompatibility=1.8
|
||||||
|
targetCompatibility=1.8
|
||||||
|
@@ -26,4 +26,14 @@ mvcGroups {
|
|||||||
view = 'com.muwire.gui.OptionsView'
|
view = 'com.muwire.gui.OptionsView'
|
||||||
controller = 'com.muwire.gui.OptionsController'
|
controller = 'com.muwire.gui.OptionsController'
|
||||||
}
|
}
|
||||||
|
"mu-wire-status" {
|
||||||
|
model = 'com.muwire.gui.MuWireStatusModel'
|
||||||
|
view = 'com.muwire.gui.MuWireStatusView'
|
||||||
|
controller = 'com.muwire.gui.MuWireStatusController'
|
||||||
|
}
|
||||||
|
'i-2-p-status' {
|
||||||
|
model = 'com.muwire.gui.I2PStatusModel'
|
||||||
|
view = 'com.muwire.gui.I2PStatusView'
|
||||||
|
controller = 'com.muwire.gui.I2PStatusController'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonController
|
||||||
|
import griffon.core.controller.ControllerAction
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.router.Router
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonController)
|
||||||
|
class I2PStatusController {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
I2PStatusModel model
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
I2PStatusView view
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void refresh() {
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
Router router = core.router
|
||||||
|
model.networkStatus = router._context.commSystem().status.toStatusString()
|
||||||
|
model.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
|
||||||
|
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
|
||||||
|
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()
|
||||||
|
model.activePeers = router._context.profileOrganizer().countActivePeers()
|
||||||
|
model.receiveBps = router._context.bandwidthLimiter().getReceiveBps15s()
|
||||||
|
model.sendBps = router._context.bandwidthLimiter().getSendBps15s()
|
||||||
|
model.participatingBW = router._context.bandwidthLimiter().getCurrentParticipatingBandwidth()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void close() {
|
||||||
|
view.dialog.setVisible(false)
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
}
|
@@ -14,8 +14,14 @@ 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.SharedFile
|
||||||
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.download.UIDownloadPausedEvent
|
||||||
|
import com.muwire.core.download.UIDownloadResumedEvent
|
||||||
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
@@ -29,6 +35,8 @@ class MainFrameController {
|
|||||||
|
|
||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
MainFrameModel model
|
MainFrameModel model
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MainFrameView view
|
||||||
|
|
||||||
private volatile Core core
|
private volatile Core core
|
||||||
|
|
||||||
@@ -38,6 +46,11 @@ class MainFrameController {
|
|||||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||||
|
|
||||||
def search = builder.getVariable("search-field").text
|
def search = builder.getVariable("search-field").text
|
||||||
|
search = search.trim()
|
||||||
|
if (search.length() == 0)
|
||||||
|
return
|
||||||
|
if (search.length() > 128)
|
||||||
|
search = search.substring(0,128)
|
||||||
def uuid = UUID.randomUUID()
|
def uuid = UUID.randomUUID()
|
||||||
Map<String, Object> params = new HashMap<>()
|
Map<String, Object> params = new HashMap<>()
|
||||||
params["search-terms"] = search
|
params["search-terms"] = search
|
||||||
@@ -45,14 +58,27 @@ class MainFrameController {
|
|||||||
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
|
||||||
|
|
||||||
|
boolean hashSearch = false
|
||||||
|
byte [] root = null
|
||||||
|
if (search.length() == 44 && search.indexOf(" ") < 0) {
|
||||||
|
try {
|
||||||
|
root = Base64.decode(search)
|
||||||
|
hashSearch = true
|
||||||
|
} catch (Exception e) {
|
||||||
|
// not a hash search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def searchEvent
|
def searchEvent
|
||||||
if (model.hashSearch) {
|
if (hashSearch) {
|
||||||
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
|
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
|
||||||
} else {
|
} else {
|
||||||
// this can be improved a lot
|
// this can be improved a lot
|
||||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||||
def terms = replaced.split(" ")
|
def terms = replaced.split(" ")
|
||||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: false)
|
def nonEmpty = []
|
||||||
|
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||||
|
searchEvent = new SearchEvent(searchTerms : nonEmpty, 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,
|
||||||
@@ -69,7 +95,8 @@ class MainFrameController {
|
|||||||
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
|
||||||
|
|
||||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid)
|
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), 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))
|
||||||
@@ -84,16 +111,17 @@ class MainFrameController {
|
|||||||
return
|
return
|
||||||
def sortEvt = group.view.lastSortEvent
|
def sortEvt = group.view.lastSortEvent
|
||||||
if (sortEvt != null) {
|
if (sortEvt != null) {
|
||||||
row = sortEvt.convertPreviousRowIndexToModel(row)
|
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||||
}
|
}
|
||||||
group.model.results[row]
|
group.model.results[row]
|
||||||
}
|
}
|
||||||
|
|
||||||
private int selectedDownload() {
|
private int selectedDownload() {
|
||||||
def selected = builder.getVariable("downloads-table").getSelectedRow()
|
def downloadsTable = builder.getVariable("downloads-table")
|
||||||
|
def selected = downloadsTable.getSelectedRow()
|
||||||
def sortEvt = mvcGroup.view.lastDownloadSortEvent
|
def sortEvt = mvcGroup.view.lastDownloadSortEvent
|
||||||
if (sortEvt != null)
|
if (sortEvt != null)
|
||||||
selected = sortEvt.convertPreviousRowIndexToModel(selected)
|
selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
|
||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,16 +129,20 @@ class MainFrameController {
|
|||||||
void download() {
|
void download() {
|
||||||
def result = selectedResult()
|
def result = selectedResult()
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return // TODO disable button
|
return
|
||||||
|
|
||||||
|
if (!model.canDownload(result.infohash))
|
||||||
|
return
|
||||||
|
|
||||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
def resultsBucket = group.model.hashBucket[result.infohash]
|
def resultsBucket = group.model.hashBucket[result.infohash]
|
||||||
|
def sources = group.model.sourcesBucket[result.infohash]
|
||||||
|
|
||||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
|
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
@@ -133,12 +165,22 @@ class MainFrameController {
|
|||||||
void cancel() {
|
void cancel() {
|
||||||
def downloader = model.downloads[selectedDownload()].downloader
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
downloader.cancel()
|
downloader.cancel()
|
||||||
|
model.downloadInfoHashes.remove(downloader.getInfoHash())
|
||||||
|
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void resume() {
|
void resume() {
|
||||||
def downloader = model.downloads[selectedDownload()].downloader
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
downloader.resume()
|
downloader.resume()
|
||||||
|
core.eventBus.publish(new UIDownloadResumedEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void pause() {
|
||||||
|
def downloader = model.downloads[selectedDownload()].downloader
|
||||||
|
downloader.pause()
|
||||||
|
core.eventBus.publish(new UIDownloadPausedEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markTrust(String tableName, TrustLevel level, def list) {
|
private void markTrust(String tableName, TrustLevel level, def list) {
|
||||||
@@ -168,14 +210,30 @@ class MainFrameController {
|
|||||||
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
void unshareSelectedFile() {
|
||||||
void keywordSearch() {
|
SharedFile sf = view.selectedSharedFile()
|
||||||
model.hashSearch = false
|
if (sf == null)
|
||||||
|
return
|
||||||
|
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
void stopWatchingDirectory() {
|
||||||
void hashSearch() {
|
String directory = mvcGroup.view.getSelectedWatchedDirectory()
|
||||||
model.hashSearch = true
|
if (directory == null)
|
||||||
|
return
|
||||||
|
core.muOptions.watchedDirectories.remove(directory)
|
||||||
|
saveMuWireSettings()
|
||||||
|
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory)))
|
||||||
|
|
||||||
|
model.watched.remove(directory)
|
||||||
|
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveMuWireSettings() {
|
||||||
|
File f = new File(core.home, "MuWire.properties")
|
||||||
|
f.withOutputStream {
|
||||||
|
core.muOptions.write(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
|
@@ -0,0 +1,45 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonController
|
||||||
|
import griffon.core.controller.ControllerAction
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonController)
|
||||||
|
class MuWireStatusController {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MuWireStatusModel model
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MuWireStatusView view
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void refresh() {
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
|
||||||
|
int incoming = 0
|
||||||
|
int outgoing = 0
|
||||||
|
core.connectionManager.getConnections().each {
|
||||||
|
if (it.incoming)
|
||||||
|
incoming++
|
||||||
|
else
|
||||||
|
outgoing++
|
||||||
|
}
|
||||||
|
model.incomingConnections = incoming
|
||||||
|
model.outgoingConnections = outgoing
|
||||||
|
|
||||||
|
model.knownHosts = core.hostCache.hosts.size()
|
||||||
|
|
||||||
|
model.sharedFiles = core.fileManager.fileToSharedFile.size()
|
||||||
|
|
||||||
|
model.downloads = core.downloadManager.downloaders.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void close() {
|
||||||
|
view.dialog.setVisible(false)
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
}
|
@@ -4,9 +4,15 @@ import griffon.core.artifact.GriffonController
|
|||||||
import griffon.core.controller.ControllerAction
|
import griffon.core.controller.ControllerAction
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
|
import javax.swing.JFileChooser
|
||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
|
||||||
@ArtifactProviderFor(GriffonController)
|
@ArtifactProviderFor(GriffonController)
|
||||||
class OptionsController {
|
class OptionsController {
|
||||||
@@ -19,6 +25,7 @@ class OptionsController {
|
|||||||
void save() {
|
void save() {
|
||||||
String text
|
String text
|
||||||
Core core = application.context.get("core")
|
Core core = application.context.get("core")
|
||||||
|
MuWireSettings settings = application.context.get("muwire-settings")
|
||||||
|
|
||||||
def i2pProps = core.i2pOptions
|
def i2pProps = core.i2pOptions
|
||||||
|
|
||||||
@@ -38,6 +45,17 @@ class OptionsController {
|
|||||||
model.outboundLength = text
|
model.outboundLength = text
|
||||||
i2pProps["outbound.length"] = text
|
i2pProps["outbound.length"] = text
|
||||||
|
|
||||||
|
if (settings.embeddedRouter) {
|
||||||
|
text = view.i2pNTCPPortField.text
|
||||||
|
model.i2pNTCPPort = text
|
||||||
|
i2pProps["i2np.ntcp.port"] = text
|
||||||
|
|
||||||
|
text = view.i2pUDPPortField.text
|
||||||
|
model.i2pUDPPort = text
|
||||||
|
i2pProps["i2np.udp.port"] = text
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
File i2pSettingsFile = new File(core.home, "i2p.properties")
|
File i2pSettingsFile = new File(core.home, "i2p.properties")
|
||||||
i2pSettingsFile.withOutputStream {
|
i2pSettingsFile.withOutputStream {
|
||||||
i2pProps.store(it,"")
|
i2pProps.store(it,"")
|
||||||
@@ -46,13 +64,16 @@ class OptionsController {
|
|||||||
text = view.retryField.text
|
text = view.retryField.text
|
||||||
model.downloadRetryInterval = text
|
model.downloadRetryInterval = text
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
|
||||||
|
model.autoDownloadUpdate = autoDownloadUpdate
|
||||||
|
settings.autoDownloadUpdate = autoDownloadUpdate
|
||||||
|
|
||||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||||
model.onlyTrusted = onlyTrusted
|
model.onlyTrusted = onlyTrusted
|
||||||
settings.setAllowUntrusted(!onlyTrusted)
|
settings.setAllowUntrusted(!onlyTrusted)
|
||||||
@@ -60,12 +81,60 @@ class OptionsController {
|
|||||||
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
||||||
model.shareDownloadedFiles = shareDownloaded
|
model.shareDownloadedFiles = shareDownloaded
|
||||||
settings.shareDownloadedFiles = shareDownloaded
|
settings.shareDownloadedFiles = shareDownloaded
|
||||||
|
|
||||||
|
String downloadLocation = model.downloadLocation
|
||||||
|
settings.downloadLocation = new File(downloadLocation)
|
||||||
|
|
||||||
|
if (settings.embeddedRouter) {
|
||||||
|
text = view.inBwField.text
|
||||||
|
model.inBw = text
|
||||||
|
settings.inBw = Integer.valueOf(text)
|
||||||
|
text = view.outBwField.text
|
||||||
|
model.outBw = text
|
||||||
|
settings.outBw = Integer.valueOf(text)
|
||||||
|
}
|
||||||
|
|
||||||
File settingsFile = new File(core.home, "MuWire.properties")
|
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
|
||||||
|
|
||||||
|
// boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
|
||||||
|
// model.showSearchHashes = showSearchHashes
|
||||||
|
// uiSettings.showSearchHashes = showSearchHashes
|
||||||
|
|
||||||
|
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||||
|
uiSettingsFile.withOutputStream {
|
||||||
|
uiSettings.write(it)
|
||||||
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,4 +143,15 @@ class OptionsController {
|
|||||||
view.d.setVisible(false)
|
view.d.setVisible(false)
|
||||||
mvcGroup.destroy()
|
mvcGroup.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void downloadLocation() {
|
||||||
|
def chooser = new JFileChooser()
|
||||||
|
chooser.setFileHidingEnabled(false)
|
||||||
|
chooser.setDialogTitle("Select location for downloaded files")
|
||||||
|
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||||
|
int rv = chooser.showOpenDialog(null)
|
||||||
|
if (rv == JFileChooser.APPROVE_OPTION)
|
||||||
|
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,18 +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.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
|
||||||
@@ -22,11 +29,89 @@ 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())
|
||||||
|
|
||||||
|
System.getProperties().setProperty("awt.useSystemAAFontSettings", "true")
|
||||||
|
|
||||||
|
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,3 +1,4 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -34,17 +35,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 ?
|
|
||||||
selectHome() :
|
|
||||||
portableHome
|
|
||||||
|
|
||||||
home = new File(home)
|
|
||||||
if (!home.exists()) {
|
|
||||||
log.info("creating home dir $home")
|
|
||||||
home.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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()) {
|
||||||
@@ -56,6 +48,8 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
} else {
|
} else {
|
||||||
log.info("creating new properties")
|
log.info("creating new properties")
|
||||||
props = new MuWireSettings()
|
props = new MuWireSettings()
|
||||||
|
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||||
|
props.updateType = System.getProperty("updateType")
|
||||||
def nickname
|
def nickname
|
||||||
while (true) {
|
while (true) {
|
||||||
nickname = JOptionPane.showInputDialog(null,
|
nickname = JOptionPane.showInputDialog(null,
|
||||||
@@ -82,6 +76,7 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
props.downloadLocation = new File(portableDownloads)
|
props.downloadLocation = new File(portableDownloads)
|
||||||
} else {
|
} else {
|
||||||
def chooser = new JFileChooser()
|
def chooser = new JFileChooser()
|
||||||
|
chooser.setFileHidingEnabled(false)
|
||||||
chooser.setDialogTitle("Select a directory where downloads will be saved")
|
chooser.setDialogTitle("Select a directory where downloads will be saved")
|
||||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||||
int rv = chooser.showOpenDialog(null)
|
int rv = chooser.showOpenDialog(null)
|
||||||
@@ -106,6 +101,9 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
||||||
System.exit(0)
|
System.exit(0)
|
||||||
}
|
}
|
||||||
|
Runtime.getRuntime().addShutdownHook({
|
||||||
|
core.shutdown()
|
||||||
|
})
|
||||||
core.startServices()
|
core.startServices()
|
||||||
application.context.put("muwire-settings", props)
|
application.context.put("muwire-settings", props)
|
||||||
application.context.put("core",core)
|
application.context.put("core",core)
|
||||||
@@ -113,33 +111,7 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
|
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.sharedFiles != null) {
|
|
||||||
props.sharedFiles.split(",").each {
|
|
||||||
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.eventBus.publish(new UILoadedEvent())
|
core.eventBus.publish(new UILoadedEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String selectHome() {
|
|
||||||
def home = new File(System.properties["user.home"])
|
|
||||||
def defaultHome = new File(home, ".MuWire")
|
|
||||||
if (defaultHome.exists())
|
|
||||||
return defaultHome.getAbsolutePath()
|
|
||||||
if (SystemVersion.isMac()) {
|
|
||||||
def library = new File(home, "Library")
|
|
||||||
def appSupport = new File(library, "Application Support")
|
|
||||||
def muwire = new File(appSupport,"MuWire")
|
|
||||||
return muwire.getAbsolutePath()
|
|
||||||
}
|
|
||||||
if (SystemVersion.isWindows()) {
|
|
||||||
def appData = new File(home,"AppData")
|
|
||||||
def roaming = new File(appData, "Roaming")
|
|
||||||
def muwire = new File(roaming, "MuWire")
|
|
||||||
return muwire.getAbsolutePath()
|
|
||||||
}
|
|
||||||
defaultHome.getAbsolutePath()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
gui/griffon-app/models/com/muwire/gui/I2PStatusModel.groovy
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.transform.Observable
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonModel)
|
||||||
|
class I2PStatusModel {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
I2PStatusController controller
|
||||||
|
|
||||||
|
@Observable int ntcpConnections
|
||||||
|
@Observable int ssuConnections
|
||||||
|
@Observable String networkStatus
|
||||||
|
@Observable int participatingTunnels
|
||||||
|
@Observable int activePeers
|
||||||
|
@Observable int receiveBps
|
||||||
|
@Observable int sendBps
|
||||||
|
@Observable int participatingBW
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
|
controller.refresh()
|
||||||
|
}
|
||||||
|
}
|
@@ -9,21 +9,27 @@ import javax.swing.JTable
|
|||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.RouterDisconnectedEvent
|
||||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||||
import com.muwire.core.connection.ConnectionEvent
|
import com.muwire.core.connection.ConnectionEvent
|
||||||
import com.muwire.core.connection.DisconnectionEvent
|
import com.muwire.core.connection.DisconnectionEvent
|
||||||
import com.muwire.core.download.DownloadStartedEvent
|
import com.muwire.core.download.DownloadStartedEvent
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
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
|
||||||
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.update.UpdateAvailableEvent
|
import com.muwire.core.update.UpdateAvailableEvent
|
||||||
|
import com.muwire.core.update.UpdateDownloadedEvent
|
||||||
import com.muwire.core.upload.UploadEvent
|
import com.muwire.core.upload.UploadEvent
|
||||||
import com.muwire.core.upload.UploadFinishedEvent
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
|
||||||
@@ -34,6 +40,7 @@ import griffon.core.mvc.MVCGroup
|
|||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.transform.FXObservable
|
import griffon.transform.FXObservable
|
||||||
import griffon.transform.Observable
|
import griffon.transform.Observable
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
@@ -46,29 +53,36 @@ class MainFrameModel {
|
|||||||
MainFrameController controller
|
MainFrameController controller
|
||||||
@Inject @Nonnull GriffonApplication application
|
@Inject @Nonnull GriffonApplication application
|
||||||
@Observable boolean coreInitialized = false
|
@Observable boolean coreInitialized = false
|
||||||
|
@Observable boolean routerPresent
|
||||||
|
|
||||||
def results = new ConcurrentHashMap<>()
|
def results = new ConcurrentHashMap<>()
|
||||||
def downloads = []
|
def downloads = []
|
||||||
def uploads = []
|
def uploads = []
|
||||||
def shared = []
|
def shared = []
|
||||||
|
def watched = []
|
||||||
def connectionList = []
|
def connectionList = []
|
||||||
def searches = new LinkedList()
|
def searches = new LinkedList()
|
||||||
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 downloadActionEnabled
|
||||||
|
@Observable boolean trustButtonsEnabled
|
||||||
@Observable boolean cancelButtonEnabled
|
@Observable boolean cancelButtonEnabled
|
||||||
@Observable boolean retryButtonEnabled
|
@Observable boolean retryButtonEnabled
|
||||||
|
@Observable boolean pauseButtonEnabled
|
||||||
|
@Observable String resumeButtonText
|
||||||
|
|
||||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||||
|
|
||||||
|
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||||
|
|
||||||
volatile Core core
|
@Observable volatile Core core
|
||||||
|
|
||||||
private long lastRetryTime = System.currentTimeMillis()
|
private long lastRetryTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
UISettings uiSettings
|
||||||
|
|
||||||
void updateTablePreservingSelection(String tableName) {
|
void updateTablePreservingSelection(String tableName) {
|
||||||
def downloadTable = builder.getVariable(tableName)
|
def downloadTable = builder.getVariable(tableName)
|
||||||
@@ -79,11 +93,28 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String, Object> args) {
|
void mvcGroupInit(Map<String, Object> args) {
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -95,8 +126,10 @@ class MainFrameModel {
|
|||||||
application.addPropertyChangeListener("core", {e ->
|
application.addPropertyChangeListener("core", {e ->
|
||||||
coreInitialized = (e.getNewValue() != null)
|
coreInitialized = (e.getNewValue() != null)
|
||||||
core = e.getNewValue()
|
core = e.getNewValue()
|
||||||
|
routerPresent = core.router != null
|
||||||
me = core.me.getHumanReadableName()
|
me = core.me.getHumanReadableName()
|
||||||
core.eventBus.register(UIResultEvent.class, this)
|
core.eventBus.register(UIResultEvent.class, this)
|
||||||
|
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||||
core.eventBus.register(ConnectionEvent.class, this)
|
core.eventBus.register(ConnectionEvent.class, this)
|
||||||
core.eventBus.register(DisconnectionEvent.class, this)
|
core.eventBus.register(DisconnectionEvent.class, this)
|
||||||
@@ -108,9 +141,15 @@ class MainFrameModel {
|
|||||||
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)
|
core.eventBus.register(FileDownloadedEvent.class, this)
|
||||||
|
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||||
|
core.eventBus.register(RouterDisconnectedEvent.class, this)
|
||||||
|
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||||
|
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||||
|
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
if (core.shutdown.get())
|
||||||
|
return
|
||||||
|
int retryInterval = core.muOptions.downloadRetryInterval
|
||||||
if (retryInterval > 0) {
|
if (retryInterval > 0) {
|
||||||
retryInterval *= 60000
|
retryInterval *= 60000
|
||||||
long now = System.currentTimeMillis()
|
long now = System.currentTimeMillis()
|
||||||
@@ -133,19 +172,43 @@ class MainFrameModel {
|
|||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
trusted.addAll(core.trustService.good.values())
|
trusted.addAll(core.trustService.good.values())
|
||||||
distrusted.addAll(core.trustService.bad.values())
|
distrusted.addAll(core.trustService.bad.values())
|
||||||
|
|
||||||
|
|
||||||
|
resumeButtonText = "Retry"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
watched.addAll(core.muOptions.watchedDirectories)
|
||||||
|
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||||
|
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
JOptionPane.showMessageDialog(null, "MuWire $e.version has been downloaded. You can update now",
|
||||||
|
"Update Downloaded", JOptionPane.INFORMATION_MESSAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onUIResultEvent(UIResultEvent e) {
|
void onUIResultEvent(UIResultEvent e) {
|
||||||
MVCGroup resultsGroup = results.get(e.uuid)
|
MVCGroup resultsGroup = results.get(e.uuid)
|
||||||
resultsGroup?.model.handleResult(e)
|
resultsGroup?.model.handleResult(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||||
|
MVCGroup resultsGroup = results.get(e.uuid)
|
||||||
|
resultsGroup?.model?.handleResultBatch(e.results)
|
||||||
|
}
|
||||||
|
|
||||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
downloads << e
|
downloads << e
|
||||||
|
downloadInfoHashes.add(e.downloader.infoHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +223,8 @@ class MainFrameModel {
|
|||||||
topPanel.getLayout().show(topPanel, "top-search-panel")
|
topPanel.getLayout().show(topPanel, "top-search-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionList.add(e.endpoint.destination)
|
UIConnection con = new UIConnection(destination : e.endpoint.destination, incoming : e.incoming)
|
||||||
|
connectionList.add(con)
|
||||||
JTable table = builder.getVariable("connections-table")
|
JTable table = builder.getVariable("connections-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
@@ -175,7 +239,8 @@ class MainFrameModel {
|
|||||||
topPanel.getLayout().show(topPanel, "top-connect-panel")
|
topPanel.getLayout().show(topPanel, "top-connect-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionList.remove(e.destination)
|
UIConnection con = new UIConnection(destination : e.destination)
|
||||||
|
connectionList.remove(con)
|
||||||
JTable table = builder.getVariable("connections-table")
|
JTable table = builder.getVariable("connections-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
@@ -205,6 +270,17 @@ class MainFrameModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||||
|
InfoHash infohash = e.unsharedFile.infoHash
|
||||||
|
if (!infoHashes.remove(infohash))
|
||||||
|
return
|
||||||
|
runInsideUIAsync {
|
||||||
|
shared.remove(e.unsharedFile)
|
||||||
|
JTable table = builder.getVariable("shared-files-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onUploadEvent(UploadEvent e) {
|
void onUploadEvent(UploadEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
uploads << e.uploader
|
uploads << e.uploader
|
||||||
@@ -232,8 +308,10 @@ class MainFrameModel {
|
|||||||
updateTablePreservingSelection("trusted-table")
|
updateTablePreservingSelection("trusted-table")
|
||||||
updateTablePreservingSelection("distrusted-table")
|
updateTablePreservingSelection("distrusted-table")
|
||||||
|
|
||||||
results.values().each {
|
results.values().each { MVCGroup group ->
|
||||||
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
|
if (group.alive) {
|
||||||
|
group.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,14 +319,23 @@ class MainFrameModel {
|
|||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
if (e.replyTo == core.me.destination)
|
if (e.replyTo == core.me.destination)
|
||||||
return
|
return
|
||||||
StringBuilder sb = new StringBuilder()
|
|
||||||
e.searchEvent.searchTerms?.each {
|
def search
|
||||||
sb.append(it)
|
if (e.searchEvent.searchHash != null) {
|
||||||
sb.append(" ")
|
if (!uiSettings.showSearchHashes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search = Base64.encode(e.searchEvent.searchHash)
|
||||||
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder()
|
||||||
|
e.searchEvent.searchTerms?.each {
|
||||||
|
sb.append(it)
|
||||||
|
sb.append(" ")
|
||||||
|
}
|
||||||
|
search = sb.toString()
|
||||||
|
if (search.trim().size() == 0)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
def search = sb.toString()
|
|
||||||
if (search.trim().size() == 0)
|
|
||||||
return
|
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
||||||
while(searches.size() > 200)
|
while(searches.size() > 200)
|
||||||
@@ -276,6 +363,14 @@ class MainFrameModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
|
||||||
|
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
|
||||||
|
System.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
if (!core.muOptions.shareDownloadedFiles)
|
if (!core.muOptions.shareDownloadedFiles)
|
||||||
return
|
return
|
||||||
@@ -286,4 +381,26 @@ class MainFrameModel {
|
|||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class UIConnection {
|
||||||
|
Destination destination
|
||||||
|
boolean incoming
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
destination.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof UIConnection))
|
||||||
|
return false
|
||||||
|
UIConnection other = (UIConnection) o
|
||||||
|
return destination == other.destination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canDownload(InfoHash hash) {
|
||||||
|
!downloadInfoHashes.contains(hash)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.transform.Observable
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonModel)
|
||||||
|
class MuWireStatusModel {
|
||||||
|
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MuWireStatusController controller
|
||||||
|
|
||||||
|
@Observable int incomingConnections
|
||||||
|
@Observable int outgoingConnections
|
||||||
|
@Observable int knownHosts
|
||||||
|
@Observable int sharedFiles
|
||||||
|
@Observable int downloads
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
|
controller.refresh()
|
||||||
|
}
|
||||||
|
}
|
@@ -11,26 +11,61 @@ import griffon.metadata.ArtifactProviderFor
|
|||||||
class OptionsModel {
|
class OptionsModel {
|
||||||
@Observable String downloadRetryInterval
|
@Observable String downloadRetryInterval
|
||||||
@Observable String updateCheckInterval
|
@Observable String updateCheckInterval
|
||||||
|
@Observable boolean autoDownloadUpdate
|
||||||
@Observable boolean onlyTrusted
|
@Observable boolean onlyTrusted
|
||||||
@Observable boolean shareDownloadedFiles
|
@Observable boolean shareDownloadedFiles
|
||||||
|
@Observable String downloadLocation
|
||||||
|
|
||||||
// i2p options
|
// i2p options
|
||||||
@Observable String inboundLength
|
@Observable String inboundLength
|
||||||
@Observable String inboundQuantity
|
@Observable String inboundQuantity
|
||||||
@Observable String outboundLength
|
@Observable String outboundLength
|
||||||
@Observable String outboundQuantity
|
@Observable String outboundQuantity
|
||||||
|
@Observable String i2pUDPPort
|
||||||
|
@Observable String i2pNTCPPort
|
||||||
|
|
||||||
|
// gui options
|
||||||
|
@Observable boolean showMonitor
|
||||||
|
@Observable String lnf
|
||||||
|
@Observable String font
|
||||||
|
@Observable boolean clearCancelledDownloads
|
||||||
|
@Observable boolean clearFinishedDownloads
|
||||||
|
@Observable boolean excludeLocalResult
|
||||||
|
@Observable boolean showSearchHashes
|
||||||
|
|
||||||
|
// bw options
|
||||||
|
@Observable String inBw
|
||||||
|
@Observable String outBw
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
MuWireSettings settings = application.context.get("muwire-settings")
|
MuWireSettings settings = application.context.get("muwire-settings")
|
||||||
downloadRetryInterval = settings.downloadRetryInterval
|
downloadRetryInterval = settings.downloadRetryInterval
|
||||||
updateCheckInterval = settings.updateCheckInterval
|
updateCheckInterval = settings.updateCheckInterval
|
||||||
|
autoDownloadUpdate = settings.autoDownloadUpdate
|
||||||
onlyTrusted = !settings.allowUntrusted()
|
onlyTrusted = !settings.allowUntrusted()
|
||||||
shareDownloadedFiles = settings.shareDownloadedFiles
|
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||||
|
downloadLocation = settings.downloadLocation.getAbsolutePath()
|
||||||
|
|
||||||
Core core = application.context.get("core")
|
Core core = application.context.get("core")
|
||||||
inboundLength = core.i2pOptions["inbound.length"]
|
inboundLength = core.i2pOptions["inbound.length"]
|
||||||
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||||
outboundLength = core.i2pOptions["outbound.length"]
|
outboundLength = core.i2pOptions["outbound.length"]
|
||||||
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||||
|
i2pUDPPort = core.i2pOptions["i2np.udp.port"]
|
||||||
|
i2pNTCPPort = core.i2pOptions["i2np.ntcp.port"]
|
||||||
|
|
||||||
|
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
|
||||||
|
showSearchHashes = uiSettings.showSearchHashes
|
||||||
|
|
||||||
|
if (core.router != null) {
|
||||||
|
inBw = String.valueOf(settings.inBw)
|
||||||
|
outBw = String.valueOf(settings.outBw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -19,13 +19,16 @@ class SearchTabModel {
|
|||||||
FactoryBuilderSupport builder
|
FactoryBuilderSupport builder
|
||||||
|
|
||||||
Core core
|
Core core
|
||||||
|
UISettings uiSettings
|
||||||
String uuid
|
String uuid
|
||||||
def results = []
|
def results = []
|
||||||
def hashBucket = [:]
|
def hashBucket = [:]
|
||||||
|
def sourcesBucket = [:]
|
||||||
|
|
||||||
|
|
||||||
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 +37,9 @@ class SearchTabModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleResult(UIResultEvent e) {
|
void handleResult(UIResultEvent e) {
|
||||||
|
if (uiSettings.excludeLocalResult &&
|
||||||
|
core.fileManager.rootToFiles.containsKey(e.infohash))
|
||||||
|
return
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
def bucket = hashBucket.get(e.infohash)
|
def bucket = hashBucket.get(e.infohash)
|
||||||
if (bucket == null) {
|
if (bucket == null) {
|
||||||
@@ -41,10 +47,44 @@ class SearchTabModel {
|
|||||||
hashBucket[e.infohash] = bucket
|
hashBucket[e.infohash] = bucket
|
||||||
}
|
}
|
||||||
bucket << e
|
bucket << e
|
||||||
|
|
||||||
|
Set sourceBucket = sourcesBucket.get(e.infohash)
|
||||||
|
if (sourceBucket == null) {
|
||||||
|
sourceBucket = new HashSet()
|
||||||
|
sourcesBucket.put(e.infohash, sourceBucket)
|
||||||
|
}
|
||||||
|
sourceBucket.addAll(e.sources)
|
||||||
|
|
||||||
results << e
|
results << e
|
||||||
JTable table = builder.getVariable("results-table")
|
JTable table = builder.getVariable("results-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleResultBatch(UIResultEvent[] batch) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
batch.each {
|
||||||
|
if (uiSettings.excludeLocalResult &&
|
||||||
|
core.fileManager.rootToFiles.containsKey(it.infohash))
|
||||||
|
return
|
||||||
|
def bucket = hashBucket.get(it.infohash)
|
||||||
|
if (bucket == null) {
|
||||||
|
bucket = []
|
||||||
|
hashBucket[it.infohash] = bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
Set sourceBucket = sourcesBucket.get(it.infohash)
|
||||||
|
if (sourceBucket == null) {
|
||||||
|
sourceBucket = new HashSet()
|
||||||
|
sourcesBucket.put(it.infohash, sourceBucket)
|
||||||
|
}
|
||||||
|
sourceBucket.addAll(it.sources)
|
||||||
|
|
||||||
|
bucket << it
|
||||||
|
results << it
|
||||||
|
}
|
||||||
|
JTable table = builder.getVariable("results-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
BIN
gui/griffon-app/resources/MuWire-128x128.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
gui/griffon-app/resources/MuWire-16x16.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
gui/griffon-app/resources/MuWire-32x32.png
Normal file
After Width: | Height: | Size: 459 B |
BIN
gui/griffon-app/resources/MuWire-48x48.png
Normal file
After Width: | Height: | Size: 1003 B |
BIN
gui/griffon-app/resources/MuWire-64x64.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 5.8 KiB |
@@ -21,10 +21,10 @@ class EventListView {
|
|||||||
application(size: [320, 80], id: 'event-list',
|
application(size: [320, 80], id: 'event-list',
|
||||||
locationRelativeTo : null,
|
locationRelativeTo : null,
|
||||||
title: application.configuration['application.title'],
|
title: application.configuration['application.title'],
|
||||||
iconImage: imageIcon('/griffon-icon-48x48.png').image,
|
iconImage: imageIcon('/MuWire-48x48.png').image,
|
||||||
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
|
iconImages: [imageIcon('/MuWire-48x48.png').image,
|
||||||
imageIcon('/griffon-icon-32x32.png').image,
|
imageIcon('/MuWire-32x32.png').image,
|
||||||
imageIcon('/griffon-icon-16x16.png').image],
|
imageIcon('/MuWire-16x16.png').image],
|
||||||
visible: bind { !model.coreInitialized} ) {
|
visible: bind { !model.coreInitialized} ) {
|
||||||
panel {
|
panel {
|
||||||
vbox {
|
vbox {
|
||||||
|
78
gui/griffon-app/views/com/muwire/gui/I2PStatusView.groovy
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonView)
|
||||||
|
class I2PStatusView {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
FactoryBuilderSupport builder
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
I2PStatusModel model
|
||||||
|
|
||||||
|
def mainFrame
|
||||||
|
def dialog
|
||||||
|
def panel
|
||||||
|
def buttonsPanel
|
||||||
|
|
||||||
|
void initUI() {
|
||||||
|
mainFrame = application.windowManager.findWindow("main-frame")
|
||||||
|
|
||||||
|
dialog = new JDialog(mainFrame, "I2P Status", true)
|
||||||
|
|
||||||
|
panel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Network status", constraints : gbc(gridx:0, gridy:0))
|
||||||
|
label(text : bind {model.networkStatus}, constraints : gbc(gridx: 1, gridy:0))
|
||||||
|
label(text : "NTCP Connections", constraints : gbc(gridx:0, gridy:1))
|
||||||
|
label(text : bind {model.ntcpConnections}, constraints : gbc(gridx: 1, gridy:1))
|
||||||
|
label(text : "SSU Connections", constraints : gbc(gridx:0, gridy:2))
|
||||||
|
label(text : bind {model.ssuConnections}, constraints : gbc(gridx: 1, gridy:2))
|
||||||
|
label(text : "Participating Tunnels", constraints : gbc(gridx:0, gridy:3))
|
||||||
|
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:3))
|
||||||
|
label(text : "Participating Bandwidth", constraints : gbc(gridx:0, gridy:4))
|
||||||
|
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:4))
|
||||||
|
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:5))
|
||||||
|
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:5))
|
||||||
|
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:6))
|
||||||
|
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:6))
|
||||||
|
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:7))
|
||||||
|
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:7))
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsPanel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
button(text : "Refresh", constraints: gbc(gridx: 0, gridy: 0), refreshAction)
|
||||||
|
button(text : "Close", constraints : gbc(gridx : 1, gridy :0), closeAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
|
JPanel statusPanel = new JPanel()
|
||||||
|
statusPanel.setLayout(new BorderLayout())
|
||||||
|
statusPanel.add(panel, BorderLayout.CENTER)
|
||||||
|
statusPanel.add(buttonsPanel, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
dialog.getContentPane().add(statusPanel)
|
||||||
|
dialog.pack()
|
||||||
|
dialog.setLocationRelativeTo(mainFrame)
|
||||||
|
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||||
|
dialog.addWindowListener(new WindowAdapter() {
|
||||||
|
public void windowClosed(WindowEvent e) {
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,10 @@
|
|||||||
package com.muwire.gui
|
package com.muwire.gui
|
||||||
|
|
||||||
import griffon.core.artifact.GriffonView
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.core.env.Metadata
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.DataHelper
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
@@ -10,13 +12,17 @@ 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.JLabel
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.JSplitPane
|
import javax.swing.JSplitPane
|
||||||
|
import javax.swing.JTable
|
||||||
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 javax.swing.table.DefaultTableCellRenderer
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
|
||||||
@@ -26,9 +32,14 @@ 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.Toolkit
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
|
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
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ArtifactProviderFor(GriffonView)
|
@ArtifactProviderFor(GriffonView)
|
||||||
class MainFrameView {
|
class MainFrameView {
|
||||||
@@ -37,24 +48,35 @@ class MainFrameView {
|
|||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
MainFrameModel model
|
MainFrameModel model
|
||||||
|
|
||||||
|
@Inject Metadata metadata
|
||||||
|
|
||||||
def downloadsTable
|
def downloadsTable
|
||||||
def lastDownloadSortEvent
|
def lastDownloadSortEvent
|
||||||
|
def lastSharedSortEvent
|
||||||
|
def lastWatchedSortEvent
|
||||||
|
|
||||||
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,
|
||||||
title: application.configuration['application.title'],
|
title: application.configuration['application.title'] + " " +
|
||||||
iconImage: imageIcon('/griffon-icon-48x48.png').image,
|
metadata["application.version"] + " revision " + metadata["build.revision"],
|
||||||
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
|
iconImage: imageIcon('/MuWire-48x48.png').image,
|
||||||
imageIcon('/griffon-icon-32x32.png').image,
|
iconImages: [imageIcon('/MuWire-48x48.png').image,
|
||||||
imageIcon('/griffon-icon-16x16.png').image],
|
imageIcon('/MuWire-32x32.png').image,
|
||||||
|
imageIcon('/MuWire-16x16.png').image],
|
||||||
pack : false,
|
pack : false,
|
||||||
visible : bind { model.coreInitialized }) {
|
visible : bind { model.coreInitialized }) {
|
||||||
menuBar {
|
menuBar {
|
||||||
menu (text : "Options") {
|
menu (text : "Options") {
|
||||||
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
|
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
|
||||||
}
|
}
|
||||||
|
menu (text : "Status") {
|
||||||
|
menuItem("MuWire", actionPerformed : {mvcGroup.createMVCGroup("mu-wire-status")})
|
||||||
|
MuWireSettings muSettings = application.context.get("muwire-settings")
|
||||||
|
menuItem("I2P", enabled : bind {model.routerPresent}, actionPerformed: {mvcGroup.createMVCGroup("i-2-p-status")})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
borderLayout()
|
borderLayout()
|
||||||
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
|
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
|
||||||
@@ -63,7 +85,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) {
|
||||||
@@ -79,12 +102,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,9 +117,9 @@ class MainFrameView {
|
|||||||
borderLayout()
|
borderLayout()
|
||||||
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
|
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
|
||||||
panel(constraints : BorderLayout.SOUTH) {
|
panel(constraints : BorderLayout.SOUTH) {
|
||||||
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
|
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||||
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
|
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||||
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
|
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel (constraints : JSplitPane.BOTTOM) {
|
panel (constraints : JSplitPane.BOTTOM) {
|
||||||
@@ -115,7 +132,7 @@ class MainFrameView {
|
|||||||
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()
|
||||||
"$done/$pieces pieces"
|
"$done/$pieces pieces".toString()
|
||||||
})
|
})
|
||||||
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
||||||
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
||||||
@@ -125,8 +142,9 @@ class MainFrameView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel (constraints : BorderLayout.SOUTH) {
|
panel (constraints : BorderLayout.SOUTH) {
|
||||||
|
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
|
||||||
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
|
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
|
||||||
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
|
button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,17 +152,32 @@ class MainFrameView {
|
|||||||
panel (constraints: "uploads window"){
|
panel (constraints: "uploads window"){
|
||||||
gridLayout(cols : 1, rows : 2)
|
gridLayout(cols : 1, rows : 2)
|
||||||
panel {
|
panel {
|
||||||
borderLayout()
|
gridLayout(cols : 2, rows : 1)
|
||||||
panel (constraints : BorderLayout.NORTH) {
|
panel {
|
||||||
button(text : "Click here to share files", actionPerformed : shareFiles)
|
borderLayout()
|
||||||
|
panel (constraints : BorderLayout.NORTH) {
|
||||||
|
button(text : "Add directories to watch", actionPerformed : watchDirectories)
|
||||||
|
}
|
||||||
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
|
table(id : "watched-directories-table", autoCreateRowSorter: true) {
|
||||||
|
tableModel(list : model.watched) {
|
||||||
|
closureColumn(header: "Watched Directories", type : String, read : { it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scrollPane ( constraints : BorderLayout.CENTER) {
|
panel {
|
||||||
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
borderLayout()
|
||||||
tableModel(list : model.shared) {
|
panel (constraints : BorderLayout.NORTH) {
|
||||||
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
|
button(text : "Share files", actionPerformed : shareFiles)
|
||||||
closureColumn(header : "Size", preferredWidth : 50, type : String,
|
}
|
||||||
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
|
scrollPane(constraints : BorderLayout.CENTER) {
|
||||||
}
|
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
||||||
|
tableModel(list : model.shared) {
|
||||||
|
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
|
||||||
|
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,16 +189,16 @@ class MainFrameView {
|
|||||||
scrollPane (constraints : BorderLayout.CENTER) {
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
table(id : "uploads-table") {
|
table(id : "uploads-table") {
|
||||||
tableModel(list : model.uploads) {
|
tableModel(list : model.uploads) {
|
||||||
closureColumn(header : "Name", type : String, read : {row -> row.file.getName() })
|
closureColumn(header : "Name", type : String, read : {row -> row.getName() })
|
||||||
closureColumn(header : "Progress", type : String, read : { row ->
|
closureColumn(header : "Progress", type : String, read : { row ->
|
||||||
int position = row.getPosition()
|
int percent = row.getProgress()
|
||||||
def range = row.request.getRange()
|
"$percent% of piece".toString()
|
||||||
int total = range.end - range.start
|
|
||||||
int percent = (int)((position * 100.0) / total)
|
|
||||||
"$percent%"
|
|
||||||
})
|
})
|
||||||
closureColumn(header : "Downloader", type : String, read : { row ->
|
closureColumn(header : "Downloader", type : String, read : { row ->
|
||||||
row.request.downloader?.getHumanReadableName()
|
row.getDownloader()
|
||||||
|
})
|
||||||
|
closureColumn(header : "Remote Pieces", type : String, read : { row ->
|
||||||
|
"${row.getDonePieces()}/${row.getTotalPieces()}".toString()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +215,13 @@ class MainFrameView {
|
|||||||
scrollPane(constraints : BorderLayout.CENTER) {
|
scrollPane(constraints : BorderLayout.CENTER) {
|
||||||
table(id : "connections-table") {
|
table(id : "connections-table") {
|
||||||
tableModel(list : model.connectionList) {
|
tableModel(list : model.connectionList) {
|
||||||
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
|
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
|
||||||
|
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
|
||||||
|
if (row.incoming)
|
||||||
|
return "In"
|
||||||
|
else
|
||||||
|
return "Out"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,20 +303,39 @@ class MainFrameView {
|
|||||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||||
selectionModel.addListSelectionListener({
|
selectionModel.addListSelectionListener({
|
||||||
int selectedRow = selectedDownloaderRow()
|
int selectedRow = selectedDownloaderRow()
|
||||||
def downloader = model.downloads[selectedRow].downloader
|
if (selectedRow < 0) {
|
||||||
|
model.cancelButtonEnabled = false
|
||||||
|
model.retryButtonEnabled = false
|
||||||
|
model.pauseButtonEnabled = 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 :
|
||||||
|
case Downloader.DownloadState.HASHLIST:
|
||||||
model.cancelButtonEnabled = true
|
model.cancelButtonEnabled = true
|
||||||
|
model.pauseButtonEnabled = true
|
||||||
model.retryButtonEnabled = false
|
model.retryButtonEnabled = false
|
||||||
break
|
break
|
||||||
case Downloader.DownloadState.FAILED:
|
case Downloader.DownloadState.FAILED:
|
||||||
model.cancelButtonEnabled = false
|
model.cancelButtonEnabled = true
|
||||||
model.retryButtonEnabled = true
|
model.retryButtonEnabled = true
|
||||||
|
model.resumeButtonText = "Retry"
|
||||||
|
model.pauseButtonEnabled = false
|
||||||
|
break
|
||||||
|
case Downloader.DownloadState.PAUSED:
|
||||||
|
model.cancelButtonEnabled = true
|
||||||
|
model.retryButtonEnabled = true
|
||||||
|
model.resumeButtonText = "Resume"
|
||||||
|
model.pauseButtonEnabled = false
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
model.cancelButtonEnabled = false
|
model.cancelButtonEnabled = false
|
||||||
model.retryButtonEnabled = false
|
model.retryButtonEnabled = false
|
||||||
|
model.pauseButtonEnabled = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -286,6 +344,122 @@ class MainFrameView {
|
|||||||
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||||
|
|
||||||
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
||||||
|
downloadsTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
|
downloadsTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showDownloadsMenu(e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showDownloadsMenu(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// shared files table
|
||||||
|
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||||
|
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||||
|
|
||||||
|
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
|
||||||
|
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
|
JPopupMenu sharedFilesMenu = new JPopupMenu()
|
||||||
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
|
||||||
|
sharedFilesMenu.add(copyHashToClipboard)
|
||||||
|
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
|
||||||
|
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFile()})
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// searches table
|
||||||
|
def searchesTable = builder.getVariable("searches-table")
|
||||||
|
JPopupMenu searchTableMenu = new JPopupMenu()
|
||||||
|
JMenuItem copySearchToClipboard = new JMenuItem("Copy search to clipboard")
|
||||||
|
copySearchToClipboard.addActionListener({mvcGroup.view.copySearchToClipboard(searchesTable)})
|
||||||
|
searchTableMenu.add(copySearchToClipboard)
|
||||||
|
searchesTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(searchTableMenu, e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(searchTableMenu, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// watched directories table
|
||||||
|
def watchedTable = builder.getVariable("watched-directories-table")
|
||||||
|
watchedTable.rowSorter.addRowSorterListener({evt -> lastWatchedSortEvent = evt})
|
||||||
|
watchedTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
JPopupMenu watchedMenu = new JPopupMenu()
|
||||||
|
JMenuItem stopWatching = new JMenuItem("Stop sharing")
|
||||||
|
stopWatching.addActionListener({mvcGroup.controller.stopWatchingDirectory()})
|
||||||
|
watchedMenu.add(stopWatching)
|
||||||
|
watchedTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(watchedMenu, e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(watchedMenu, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
||||||
|
menu.show(event.getComponent(), event.getX(), event.getY())
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectedSharedFile() {
|
||||||
|
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||||
|
int selected = sharedFilesTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return null
|
||||||
|
if (lastSharedSortEvent != null)
|
||||||
|
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
|
||||||
|
model.shared[selected]
|
||||||
|
}
|
||||||
|
|
||||||
|
def copyHashToClipboard() {
|
||||||
|
def selected = selectedSharedFile()
|
||||||
|
if (selected == null)
|
||||||
|
return
|
||||||
|
String root = Base64.encode(selected.infoHash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(root)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def copySearchToClipboard(JTable searchesTable) {
|
||||||
|
int selected = searchesTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
String search = model.searches[selected].search
|
||||||
|
StringSelection selection = new StringSelection(search)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
int selectedDownloaderRow() {
|
int selectedDownloaderRow() {
|
||||||
@@ -295,6 +469,71 @@ class MainFrameView {
|
|||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def showDownloadsMenu(MouseEvent e) {
|
||||||
|
int selected = selectedDownloaderRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
boolean pauseEnabled = false
|
||||||
|
boolean cancelEnabled = false
|
||||||
|
boolean retryEnabled = false
|
||||||
|
String resumeText = "Retry"
|
||||||
|
Downloader downloader = model.downloads[selected].downloader
|
||||||
|
switch(downloader.currentState) {
|
||||||
|
case Downloader.DownloadState.DOWNLOADING:
|
||||||
|
case Downloader.DownloadState.HASHLIST:
|
||||||
|
case Downloader.DownloadState.CONNECTING:
|
||||||
|
pauseEnabled = true
|
||||||
|
cancelEnabled = true
|
||||||
|
retryEnabled = false
|
||||||
|
break
|
||||||
|
case Downloader.DownloadState.FAILED:
|
||||||
|
pauseEnabled = false
|
||||||
|
cancelEnabled = true
|
||||||
|
retryEnabled = true
|
||||||
|
break
|
||||||
|
case Downloader.DownloadState.PAUSED:
|
||||||
|
pauseEnabled = false
|
||||||
|
cancelEnabled = true
|
||||||
|
retryEnabled = true
|
||||||
|
resumeText = "Resume"
|
||||||
|
break
|
||||||
|
default :
|
||||||
|
pauseEnabled = false
|
||||||
|
cancelEnabled = false
|
||||||
|
retryEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
JPopupMenu menu = new JPopupMenu()
|
||||||
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({
|
||||||
|
String hash = Base64.encode(downloader.infoHash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(hash)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
})
|
||||||
|
menu.add(copyHashToClipboard)
|
||||||
|
|
||||||
|
if (pauseEnabled) {
|
||||||
|
JMenuItem pause = new JMenuItem("Pause")
|
||||||
|
pause.addActionListener({mvcGroup.controller.pause()})
|
||||||
|
menu.add(pause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelEnabled) {
|
||||||
|
JMenuItem cancel = new JMenuItem("Cancel")
|
||||||
|
cancel.addActionListener({mvcGroup.controller.cancel()})
|
||||||
|
menu.add(cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryEnabled) {
|
||||||
|
JMenuItem retry = new JMenuItem(resumeText)
|
||||||
|
retry.addActionListener({mvcGroup.controller.resume()})
|
||||||
|
menu.add(retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopupMenu(menu, e)
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
@@ -317,11 +556,38 @@ class MainFrameView {
|
|||||||
|
|
||||||
def shareFiles = {
|
def shareFiles = {
|
||||||
def chooser = new JFileChooser()
|
def chooser = new JFileChooser()
|
||||||
chooser.setDialogTitle("Select file or directory to share")
|
chooser.setFileHidingEnabled(false)
|
||||||
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
|
chooser.setDialogTitle("Select file to share")
|
||||||
|
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
|
||||||
int rv = chooser.showOpenDialog(null)
|
int rv = chooser.showOpenDialog(null)
|
||||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||||
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
|
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def watchDirectories = {
|
||||||
|
def chooser = new JFileChooser()
|
||||||
|
chooser.setFileHidingEnabled(false)
|
||||||
|
chooser.setDialogTitle("Select directory to watch")
|
||||||
|
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||||
|
int rv = chooser.showOpenDialog(null)
|
||||||
|
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||||
|
File f = chooser.getSelectedFile()
|
||||||
|
model.watched << f.getAbsolutePath()
|
||||||
|
application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath()
|
||||||
|
mvcGroup.controller.saveMuWireSettings()
|
||||||
|
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||||
|
model.core.eventBus.publish(new FileSharedEvent(file : f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSelectedWatchedDirectory() {
|
||||||
|
def watchedTable = builder.getVariable("watched-directories-table")
|
||||||
|
int selectedRow = watchedTable.getSelectedRow()
|
||||||
|
if (selectedRow < 0)
|
||||||
|
return null
|
||||||
|
if (lastWatchedSortEvent != null)
|
||||||
|
selectedRow = watchedTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||||
|
model.watched[selectedRow]
|
||||||
|
}
|
||||||
}
|
}
|
73
gui/griffon-app/views/com/muwire/gui/MuWireStatusView.groovy
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.artifact.GriffonView
|
||||||
|
import griffon.inject.MVCMember
|
||||||
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
|
import javax.swing.JDialog
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
|
@ArtifactProviderFor(GriffonView)
|
||||||
|
class MuWireStatusView {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
FactoryBuilderSupport builder
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
MuWireStatusModel model
|
||||||
|
|
||||||
|
def mainFrame
|
||||||
|
def dialog
|
||||||
|
def panel
|
||||||
|
def buttonsPanel
|
||||||
|
|
||||||
|
void initUI() {
|
||||||
|
mainFrame = application.windowManager.findWindow("main-frame")
|
||||||
|
|
||||||
|
dialog = new JDialog(mainFrame, "MuWire Status", true)
|
||||||
|
|
||||||
|
panel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Incoming connections", constraints : gbc(gridx:0, gridy:0))
|
||||||
|
label(text : bind {model.incomingConnections}, constraints : gbc(gridx:1, gridy:0))
|
||||||
|
label(text : "Outgoing connections", constraints : gbc(gridx:0, gridy:1))
|
||||||
|
label(text : bind {model.outgoingConnections}, constraints : gbc(gridx:1, gridy:1))
|
||||||
|
label(text : "Known hosts", constraints : gbc(gridx:0, gridy:2))
|
||||||
|
label(text : bind {model.knownHosts}, constraints : gbc(gridx:1, gridy:2))
|
||||||
|
label(text : "Shared files", constraints : gbc(gridx:0, gridy:3))
|
||||||
|
label(text : bind {model.sharedFiles}, constraints : gbc(gridx:1, gridy:3))
|
||||||
|
label(text : "Downloads", constraints : gbc(gridx:0, gridy:4))
|
||||||
|
label(text : bind {model.downloads}, constraints : gbc(gridx:1, gridy:4))
|
||||||
|
}
|
||||||
|
buttonsPanel = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
button(text : "Refresh", constraints: gbc(gridx: 0, gridy: 0), refreshAction)
|
||||||
|
button(text : "Close", constraints : gbc(gridx : 1, gridy :0), closeAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
|
JPanel statusPanel = new JPanel()
|
||||||
|
statusPanel.setLayout(new BorderLayout())
|
||||||
|
statusPanel.add(panel, BorderLayout.CENTER)
|
||||||
|
statusPanel.add(buttonsPanel, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
dialog.getContentPane().add(statusPanel)
|
||||||
|
dialog.pack()
|
||||||
|
dialog.setLocationRelativeTo(mainFrame)
|
||||||
|
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||||
|
dialog.addWindowListener(new WindowAdapter() {
|
||||||
|
public void windowClosed(WindowEvent e) {
|
||||||
|
mvcGroup.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
@@ -9,6 +9,8 @@ import javax.swing.JPanel
|
|||||||
import javax.swing.JTabbedPane
|
import javax.swing.JTabbedPane
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
import java.awt.event.WindowEvent
|
import java.awt.event.WindowEvent
|
||||||
@@ -25,8 +27,12 @@ class OptionsView {
|
|||||||
def d
|
def d
|
||||||
def p
|
def p
|
||||||
def i
|
def i
|
||||||
|
def u
|
||||||
|
def bandwidth
|
||||||
|
|
||||||
def retryField
|
def retryField
|
||||||
def updateField
|
def updateField
|
||||||
|
def autoDownloadUpdateCheckbox
|
||||||
def allowUntrustedCheckbox
|
def allowUntrustedCheckbox
|
||||||
def shareDownloadedCheckbox
|
def shareDownloadedCheckbox
|
||||||
|
|
||||||
@@ -34,7 +40,21 @@ class OptionsView {
|
|||||||
def inboundQuantityField
|
def inboundQuantityField
|
||||||
def outboundLengthField
|
def outboundLengthField
|
||||||
def outboundQuantityField
|
def outboundQuantityField
|
||||||
|
def i2pUDPPortField
|
||||||
|
def i2pNTCPPortField
|
||||||
|
|
||||||
|
def lnfField
|
||||||
|
def monitorCheckbox
|
||||||
|
def fontField
|
||||||
|
def clearCancelledDownloadsCheckbox
|
||||||
|
def clearFinishedDownloadsCheckbox
|
||||||
|
def excludeLocalResultCheckbox
|
||||||
|
def showSearchHashesCheckbox
|
||||||
|
|
||||||
|
|
||||||
|
def inBwField
|
||||||
|
def outBwField
|
||||||
|
|
||||||
def buttonsPanel
|
def buttonsPanel
|
||||||
|
|
||||||
def mainFrame
|
def mainFrame
|
||||||
@@ -52,12 +72,19 @@ 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))
|
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2))
|
||||||
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
|
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2))
|
||||||
|
|
||||||
|
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 3))
|
||||||
|
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 3))
|
||||||
|
|
||||||
|
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:4))
|
||||||
|
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:4))
|
||||||
|
|
||||||
|
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:5))
|
||||||
|
button(text : "Choose", constraints : gbc(gridx : 1, gridy:5), downloadLocationAction)
|
||||||
|
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:6, gridwidth:2))
|
||||||
|
|
||||||
}
|
}
|
||||||
i = builder.panel {
|
i = builder.panel {
|
||||||
@@ -71,7 +98,44 @@ class OptionsView {
|
|||||||
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, 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))
|
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
|
||||||
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
||||||
|
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
if (core.router != null) {
|
||||||
|
label(text : "TCP Port", constraints : gbc(gridx :0, gridy: 5))
|
||||||
|
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:5))
|
||||||
|
label(text : "UDP Port", constraints : gbc(gridx :0, gridy: 6))
|
||||||
|
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:6))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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))
|
||||||
|
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
|
||||||
|
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
|
||||||
|
}
|
||||||
|
bandwidth = builder.panel {
|
||||||
|
gridBagLayout()
|
||||||
|
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||||
|
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1))
|
||||||
|
inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1))
|
||||||
|
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2))
|
||||||
|
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
buttonsPanel = builder.panel {
|
buttonsPanel = builder.panel {
|
||||||
gridBagLayout()
|
gridBagLayout()
|
||||||
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
||||||
@@ -81,8 +145,13 @@ class OptionsView {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String,String> args) {
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
def tabbedPane = new JTabbedPane()
|
def tabbedPane = new JTabbedPane()
|
||||||
tabbedPane.addTab("MuWire Options", p)
|
tabbedPane.addTab("MuWire", p)
|
||||||
tabbedPane.addTab("I2P Options", i)
|
tabbedPane.addTab("I2P", i)
|
||||||
|
tabbedPane.addTab("GUI", u)
|
||||||
|
Core core = application.context.get("core")
|
||||||
|
if (core.router != null) {
|
||||||
|
tabbedPane.addTab("Bandwidth", bandwidth)
|
||||||
|
}
|
||||||
|
|
||||||
JPanel panel = new JPanel()
|
JPanel panel = new JPanel()
|
||||||
panel.setLayout(new BorderLayout())
|
panel.setLayout(new BorderLayout())
|
||||||
|
@@ -4,14 +4,26 @@ import griffon.core.artifact.GriffonView
|
|||||||
import griffon.core.mvc.MVCGroup
|
import griffon.core.mvc.MVCGroup
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
import net.i2p.data.Base64
|
||||||
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.JMenuItem
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
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.Toolkit
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
|
||||||
import javax.annotation.Nonnull
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
@@ -35,8 +47,9 @@ class SearchTabView {
|
|||||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
|
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
|
||||||
tableModel(list: model.results) {
|
tableModel(list: model.results) {
|
||||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||||
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
|
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
|
||||||
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||||
|
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[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).toString()
|
model.core.trustService.getLevel(row.sender.destination).toString()
|
||||||
@@ -54,7 +67,13 @@ class SearchTabView {
|
|||||||
def selectionModel = resultsTable.getSelectionModel()
|
def selectionModel = resultsTable.getSelectionModel()
|
||||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||||
selectionModel.addListSelectionListener( {
|
selectionModel.addListSelectionListener( {
|
||||||
mvcGroup.parentGroup.model.searchButtonsEnabled = true
|
int row = resultsTable.getSelectedRow()
|
||||||
|
if (row < 0)
|
||||||
|
return
|
||||||
|
if (lastSortEvent != null)
|
||||||
|
row = resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||||
|
mvcGroup.parentGroup.model.trustButtonsEnabled = true
|
||||||
|
mvcGroup.parentGroup.model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,13 +105,59 @@ class SearchTabView {
|
|||||||
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.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||||
|
resultsTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
|
|
||||||
|
resultsTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.button == MouseEvent.BUTTON3)
|
||||||
|
showPopupMenu(e)
|
||||||
|
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
||||||
|
mvcGroup.parentGroup.controller.download()
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.button == MouseEvent.BUTTON3)
|
||||||
|
showPopupMenu(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
def closeTab = {
|
def closeTab = {
|
||||||
int index = parent.indexOfTab(searchTerms)
|
int index = parent.indexOfTab(searchTerms)
|
||||||
parent.removeTabAt(index)
|
parent.removeTabAt(index)
|
||||||
mvcGroup.parentGroup.model.searchButtonsEnabled = false
|
mvcGroup.parentGroup.model.trustButtonsEnabled = false
|
||||||
|
mvcGroup.parentGroup.model.downloadActionEnabled = false
|
||||||
mvcGroup.destroy()
|
mvcGroup.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def showPopupMenu(MouseEvent e) {
|
||||||
|
JPopupMenu menu = new JPopupMenu()
|
||||||
|
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
|
||||||
|
JMenuItem download = new JMenuItem("Download")
|
||||||
|
download.addActionListener({mvcGroup.parentGroup.controller.download()})
|
||||||
|
menu.add(download)
|
||||||
|
}
|
||||||
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
|
||||||
|
menu.add(copyHashToClipboard)
|
||||||
|
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||||
|
}
|
||||||
|
|
||||||
|
def copyHashToClipboard() {
|
||||||
|
int selected = resultsTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
if (lastSortEvent != null)
|
||||||
|
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||||
|
String hash = Base64.encode(model.results[selected].infohash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(hash)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.muwire.gui
|
||||||
|
|
||||||
|
import griffon.core.test.GriffonFestRule
|
||||||
|
import org.fest.swing.fixture.FrameFixture
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail
|
||||||
|
|
||||||
|
class I2PStatusIntegrationTest {
|
||||||
|
static {
|
||||||
|
System.setProperty('griffon.swing.edt.violations.check', 'true')
|
||||||
|
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final GriffonFestRule fest = new GriffonFestRule()
|
||||||
|
|
||||||
|
private FrameFixture window
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void smokeTest() {
|
||||||
|
fail('Not implemented yet!')
|
||||||
|
}
|
||||||
|
}
|