Compare commits
302 Commits
muwire-0.1
...
content-co
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ee2fd2ef68 | ||
![]() |
3f95d2bf1d | ||
![]() |
1390983732 | ||
![]() |
ce660cefe9 | ||
![]() |
72b81eb886 | ||
![]() |
57d593a68a | ||
![]() |
39a81a3376 | ||
![]() |
fd0bf17c24 | ||
![]() |
ac12bff69b | ||
![]() |
feef773bac | ||
![]() |
239d8f12a7 | ||
![]() |
8bbc61a7cb | ||
![]() |
7f31c4477f | ||
![]() |
6bad67c1bf | ||
![]() |
c76e6dc99f | ||
![]() |
acf9db0db3 | ||
![]() |
69b4f0b547 | ||
![]() |
80e165b505 | ||
![]() |
bcce55b873 | ||
![]() |
d5c92560db | ||
![]() |
f827c1c9bf | ||
![]() |
88c5f1a02d | ||
![]() |
d8e44f5f39 | ||
![]() |
72ff47ffe5 | ||
![]() |
066ee2c96d | ||
![]() |
0a8016dea7 | ||
![]() |
db36367b11 | ||
![]() |
b6c9ccb7f6 | ||
![]() |
a9dc636bce | ||
![]() |
3cc0574d11 | ||
![]() |
20fab9b16d | ||
![]() |
4015818323 | ||
![]() |
f569d45c8c | ||
![]() |
3773647869 | ||
![]() |
29cdbf018c | ||
![]() |
94bb7022eb | ||
![]() |
39808302df | ||
![]() |
2d22f9c39e | ||
![]() |
ee8f80bab6 | ||
![]() |
3e6242e583 | ||
![]() |
41181616ee | ||
![]() |
eb2530ca32 | ||
![]() |
b5233780ef | ||
![]() |
78753d7538 | ||
![]() |
4740e8b4f5 | ||
![]() |
ad5b00fc90 | ||
![]() |
d6c6880848 | ||
![]() |
4f948c1b9e | ||
![]() |
2b68c24f9c | ||
![]() |
bcdf0422db | ||
![]() |
f6434b478d | ||
![]() |
e979fdd26f | ||
![]() |
e6bfcaaab9 | ||
![]() |
9780108e8a | ||
![]() |
697c7d2d6d | ||
![]() |
887d10c8bf | ||
![]() |
ef6b8fe458 | ||
![]() |
20ab55d763 | ||
![]() |
eda58c9e0d | ||
![]() |
fb42fc0e35 | ||
![]() |
35cabc47ad | ||
![]() |
5be97d0404 | ||
![]() |
82b0fa253c | ||
![]() |
011a4d5766 | ||
![]() |
5cd1ca88c1 | ||
![]() |
44c880d911 | ||
![]() |
14857cb5ad | ||
![]() |
7daf981f1a | ||
![]() |
b99bc0ea32 | ||
![]() |
1ccf6fbdfa | ||
![]() |
5711979272 | ||
![]() |
9a5e2b1fa3 | ||
![]() |
cafc5f582e | ||
![]() |
a89b423dfc | ||
![]() |
79e8438941 | ||
![]() |
19c2c46491 | ||
![]() |
78f1d54b69 | ||
![]() |
9461649ed4 | ||
![]() |
8573ab2850 | ||
![]() |
8b3d752727 | ||
![]() |
7c54bd8966 | ||
![]() |
5d0fcb7027 | ||
![]() |
3ec9654d3c | ||
![]() |
7c8d64b462 | ||
![]() |
31e30e3d31 | ||
![]() |
8caf6e99b0 | ||
![]() |
624155debd | ||
![]() |
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 |
17
README.md
@@ -4,31 +4,32 @@ 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.
|
||||
|
||||
The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
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
|
||||
```
|
||||
./gradlew build
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Some of the UI tests will fail because they haven't been written yet :-/
|
||||
|
||||
### Running
|
||||
|
||||
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
|
||||
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
|
||||
|
||||
### Known bugs and limitations
|
||||
### GPG Fingerprint
|
||||
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||
|
||||
* Many UI features you would expect are not there yet
|
||||
* Sorting the results table sometimes causes the wrong result to be downloaded
|
||||
You can find the full key at https://keybase.io/zlatinb
|
||||
|
31
TODO.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
|
||||
##### 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
|
||||
* Multiple-selection download, Ctrl-A
|
@@ -2,7 +2,7 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'net.i2p:i2p:0.9.40'
|
||||
compile 'net.i2p:i2p:0.9.41'
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import java.util.concurrent.CountDownLatch
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
@@ -34,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.11")
|
||||
core = new Core(props, home, "0.4.8")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
@@ -72,7 +73,7 @@ class Cli {
|
||||
|
||||
Timer timer = new Timer("status-printer", true)
|
||||
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)
|
||||
|
||||
def latch = new CountDownLatch(1)
|
||||
@@ -84,6 +85,7 @@ class Cli {
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
println "waiting for files to load"
|
||||
latch.await()
|
||||
// now we begin
|
||||
@@ -117,11 +119,11 @@ class Cli {
|
||||
volatile int uploads
|
||||
public void onUploadEvent(UploadEvent e) {
|
||||
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) {
|
||||
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
|
||||
try {
|
||||
core = new Core(props, home, "0.1.11")
|
||||
core = new Core(props, home, "0.4.8")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
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,8 +2,9 @@ apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
||||
compile 'net.i2p.client:streaming:0.9.40'
|
||||
compile 'net.i2p:router:0.9.41'
|
||||
compile 'net.i2p.client:mstreaming:0.9.41'
|
||||
compile 'net.i2p.client:streaming:0.9.41'
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@@ -9,7 +9,5 @@ class Constants {
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||
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
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.connection.ConnectionAcceptor
|
||||
import com.muwire.core.connection.ConnectionEstablisher
|
||||
@@ -12,10 +13,14 @@ import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.connection.LeafConnectionManager
|
||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
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.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@@ -23,19 +28,28 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.HasherService
|
||||
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.HostCache
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.trust.TrustSubscriber
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.update.UpdateClient
|
||||
import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.MuWireLogManager
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@@ -44,6 +58,7 @@ import net.i2p.client.I2PSession
|
||||
import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||
import net.i2p.client.streaming.I2PSocketOptions
|
||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Destination
|
||||
@@ -51,6 +66,9 @@ import net.i2p.data.PrivateKey
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import net.i2p.router.Router
|
||||
import net.i2p.router.RouterContext
|
||||
|
||||
@Log
|
||||
public class Core {
|
||||
|
||||
@@ -61,6 +79,7 @@ public class Core {
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
@@ -70,23 +89,18 @@ public class Core {
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
|
||||
private final Router router
|
||||
|
||||
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
this.muOptions = props
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
|
||||
log.info("initializing I2P socket manager")
|
||||
def i2pClient = new I2PClientFactory().createClient()
|
||||
File keyDat = new File(home, "key.dat")
|
||||
if (!keyDat.exists()) {
|
||||
log.info("Creating new key.dat")
|
||||
keyDat.withOutputStream {
|
||||
i2pClient.createDestination(it, Constants.SIG_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
i2pOptions = new Properties()
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
@@ -101,13 +115,52 @@ public class Core {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.length"] = "3"
|
||||
i2pOptions["inbound.quantity"] = "2"
|
||||
i2pOptions["inbound.quantity"] = "4"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
i2pOptions["outbound.quantity"] = "2"
|
||||
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("router.excludePeerCaps", "KLM")
|
||||
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)
|
||||
router.getContext().setLogManager(new MuWireLogManager())
|
||||
router.runRouter()
|
||||
while(!router.isRunning())
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
log.info("initializing I2P socket manager")
|
||||
def i2pClient = new I2PClientFactory().createClient()
|
||||
File keyDat = new File(home, "key.dat")
|
||||
if (!keyDat.exists()) {
|
||||
log.info("Creating new key.dat")
|
||||
keyDat.withOutputStream {
|
||||
i2pClient.createDestination(it, Constants.SIG_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// options like tunnel length and quantity
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
@@ -116,6 +169,7 @@ public class Core {
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
@@ -153,15 +207,21 @@ public class Core {
|
||||
|
||||
|
||||
log.info "initializing file manager"
|
||||
FileManager fileManager = new FileManager(eventBus, props)
|
||||
fileManager = new FileManager(eventBus, props)
|
||||
eventBus.register(FileHashedEvent.class, fileManager)
|
||||
eventBus.register(FileLoadedEvent.class, fileManager)
|
||||
eventBus.register(FileDownloadedEvent.class, fileManager)
|
||||
eventBus.register(FileUnsharedEvent.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"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@@ -182,7 +242,9 @@ public class Core {
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
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")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
@@ -196,14 +258,17 @@ public class Core {
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.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")
|
||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@@ -213,17 +278,31 @@ public class Core {
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
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")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.class, hasherService)
|
||||
|
||||
log.info("initializing trust subscriber")
|
||||
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
|
||||
eventBus.register(UILoadedEvent.class, trustSubscriber)
|
||||
eventBus.register(TrustSubscriptionEvent.class, trustSubscriber)
|
||||
|
||||
log.info("initializing content manager")
|
||||
contentManager = new ContentManager()
|
||||
eventBus.register(ContentControlEvent.class, contentManager)
|
||||
eventBus.register(QueryEvent.class, contentManager)
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
hasherService.start()
|
||||
trustService.start()
|
||||
trustService.waitForLoad()
|
||||
persisterService.start()
|
||||
hostCache.start()
|
||||
connectionManager.start()
|
||||
cacheClient.start()
|
||||
@@ -234,14 +313,28 @@ public class Core {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (!shutdown.compareAndSet(false, true)) {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
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 cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (router != null) {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
}
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
@@ -268,7 +361,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.1.11")
|
||||
Core core = new Core(props, home, "0.4.8")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -48,4 +48,9 @@ class EventBus {
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
|
||||
synchronized void unregister(Class<? extends Event> eventType, def handler) {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,35 @@
|
||||
package com.muwire.core
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.hostcache.CrawlerResponse
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class MuWireSettings {
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int updateCheckInterval
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
File downloadLocation
|
||||
String sharedFiles
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval
|
||||
int meshExpiration
|
||||
boolean embeddedRouter
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
@@ -20,32 +37,92 @@ class MuWireSettings {
|
||||
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.get("allowUntrusted","true"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
sharedFiles = props.getProperty("sharedFiles")
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
updateType = props.getProperty("updateType","jar")
|
||||
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 = readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
||||
|
||||
trustSubscriptions = new HashSet<>()
|
||||
if (props.containsKey("trustSubscriptions")) {
|
||||
props.getProperty("trustSubscriptions").split(",").each {
|
||||
trustSubscriptions.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.toString())
|
||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
|
||||
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
|
||||
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
||||
props.setProperty("nickname", nickname)
|
||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
props.setProperty("updateType",String.valueOf(updateType))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
if (sharedFiles != null)
|
||||
props.setProperty("sharedFiles", sharedFiles)
|
||||
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||
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))
|
||||
|
||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
|
||||
if (!trustSubscriptions.isEmpty()) {
|
||||
String encoded = trustSubscriptions.stream().
|
||||
map({it.toBase64()}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new HashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
|
||||
if (set.isEmpty())
|
||||
return
|
||||
String encoded = set.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty(property, encoded)
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
|
@@ -82,4 +82,13 @@ public class Persona {
|
||||
Persona other = (Persona)o
|
||||
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 {
|
||||
}
|
@@ -22,6 +22,9 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
|
||||
private static final int SEARCHES = 10
|
||||
private static final long INTERVAL = 1000
|
||||
|
||||
final EventBus eventBus
|
||||
final Endpoint endpoint
|
||||
final boolean incoming
|
||||
@@ -32,6 +35,7 @@ abstract class Connection implements Closeable {
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
|
||||
|
||||
protected final String name
|
||||
|
||||
@@ -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) {
|
||||
if (throttleSearch()) {
|
||||
log.info("dropping excessive search")
|
||||
return
|
||||
}
|
||||
UUID uuid = UUID.fromString(search.uuid)
|
||||
byte [] infohash = null
|
||||
if (search.infohash != null) {
|
||||
|
@@ -14,9 +14,12 @@ import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.search.InvalidSearchResultException
|
||||
import com.muwire.core.search.ResultsParser
|
||||
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 groovy.json.JsonOutput
|
||||
@@ -123,6 +126,9 @@ class ConnectionAcceptor {
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
@@ -225,13 +231,15 @@ class ConnectionAcceptor {
|
||||
if (sender.destination != e.getDestination())
|
||||
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
||||
int nResults = dis.readUnsignedShort()
|
||||
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||
for (int i = 0; i < nResults; i++) {
|
||||
int jsonSize = dis.readUnsignedShort()
|
||||
byte [] payload = new byte[jsonSize]
|
||||
dis.readFully(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) {
|
||||
log.log(Level.WARNING, "failed to process POST", bad)
|
||||
} finally {
|
||||
@@ -239,4 +247,43 @@ class ConnectionAcceptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void processTRUST(Endpoint e) {
|
||||
byte[] RUST = new byte[6]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
e.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
@@ -24,7 +25,7 @@ class Endpoint implements Closeable {
|
||||
@Override
|
||||
public void close() {
|
||||
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
|
||||
}
|
||||
if (inputStream != null) {
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class ContentControlEvent extends Event {
|
||||
String term
|
||||
boolean regex
|
||||
boolean add
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.search.QueryEvent
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class ContentManager {
|
||||
|
||||
Set<Matcher> matchers = new ConcurrentHashSet()
|
||||
|
||||
void onContentControlEvent(ContentControlEvent e) {
|
||||
Matcher m
|
||||
if (e.regex)
|
||||
m = new RegexMatcher(e.term)
|
||||
else
|
||||
m = new KeywordMatcher(e.term)
|
||||
if (e.add)
|
||||
matchers.add(m)
|
||||
else
|
||||
matchers.remove(m)
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.searchEvent.searchTerms == null)
|
||||
return
|
||||
matchers.each { it.process(e) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
class KeywordMatcher extends Matcher {
|
||||
private final String keyword
|
||||
KeywordMatcher(String keyword) {
|
||||
this.keyword = keyword
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean match(List<String> searchTerms) {
|
||||
boolean found = false
|
||||
searchTerms.each {
|
||||
if (keyword == it)
|
||||
found = true
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTerm() {
|
||||
keyword
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
keyword.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof KeywordMatcher))
|
||||
return false
|
||||
KeywordMatcher other = (KeywordMatcher) o
|
||||
keyword.equals(other.keyword)
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class Match {
|
||||
Persona persona
|
||||
String [] keywords
|
||||
long timestamp
|
||||
}
|
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.search.QueryEvent
|
||||
|
||||
abstract class Matcher {
|
||||
final List<Match> matches = Collections.synchronizedList(new ArrayList<>())
|
||||
final Set<UUID> uuids = new HashSet<>()
|
||||
|
||||
protected abstract boolean match(List<String> searchTerms);
|
||||
|
||||
public abstract String getTerm();
|
||||
|
||||
public void process(QueryEvent qe) {
|
||||
def terms = qe.searchEvent.searchTerms
|
||||
if (match(terms) && uuids.add(qe.searchEvent.uuid)) {
|
||||
long now = System.currentTimeMillis()
|
||||
matches << new Match(persona : qe.originator, keywords : terms, timestamp : now)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import java.util.stream.Collectors
|
||||
|
||||
class RegexMatcher extends Matcher {
|
||||
private final Pattern pattern
|
||||
RegexMatcher(String pattern) {
|
||||
this.pattern = Pattern.compile(pattern)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean match(List<String> keywords) {
|
||||
String combined = keywords.join(" ")
|
||||
return pattern.matcher(combined).find()
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTerm() {
|
||||
pattern.pattern()
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
pattern.pattern().hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof RegexMatcher))
|
||||
return false
|
||||
RegexMatcher other = (RegexMatcher) o
|
||||
pattern.pattern() == other.pattern.pattern()
|
||||
}
|
||||
}
|
@@ -3,6 +3,10 @@ package com.muwire.core.download
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.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 groovy.json.JsonBuilder
|
||||
@@ -14,24 +18,33 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
public class DownloadManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final TrustService trustService
|
||||
private final MeshManager meshManager
|
||||
private final MuWireSettings muSettings
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes, home
|
||||
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.trustService = trustService
|
||||
this.meshManager = meshManager
|
||||
this.muSettings = muSettings
|
||||
this.connector = connector
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
@@ -58,18 +71,30 @@ public class DownloadManager {
|
||||
e.result.each {
|
||||
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,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes)
|
||||
downloaders.add(downloader)
|
||||
incompletes, pieces)
|
||||
downloaders.put(infohash, downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||
downloaders.remove(e.downloader)
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
@@ -97,23 +122,54 @@ public class DownloadManager {
|
||||
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,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
||||
downloaders.add(downloader)
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
downloaders.remove(e.downloader)
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
private void persistDownloaders() {
|
||||
File downloadsFile = new File(home,"downloads.json")
|
||||
downloadsFile.withPrintWriter { writer ->
|
||||
downloaders.each { downloader ->
|
||||
downloaders.values().each { downloader ->
|
||||
if (!downloader.cancelled) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
||||
@@ -130,6 +186,8 @@ public class DownloadManager {
|
||||
json.hashList = Base64.encode(infoHash.hashList)
|
||||
else
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
|
||||
json.paused = downloader.paused
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
@@ -137,7 +195,7 @@ public class DownloadManager {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
downloaders.each { it.stop() }
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
@@ -3,49 +3,56 @@ package com.muwire.core.download;
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import static com.muwire.core.util.DataUtil.readTillRN
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.MappedByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
class DownloadSession {
|
||||
|
||||
private static int SAMPLES = 10
|
||||
|
||||
private final EventBus eventBus
|
||||
private final String meB64
|
||||
private final Pieces downloaded, claimed
|
||||
private final Pieces pieces
|
||||
private final InfoHash infoHash
|
||||
private final Endpoint endpoint
|
||||
private final File file
|
||||
private final int pieceSize
|
||||
private final long fileLength
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||
private final LinkedList<Integer> reads = new LinkedList<>()
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
|
||||
private ByteBuffer mapped
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength) {
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.downloaded = downloaded
|
||||
this.claimed = claimed
|
||||
this.pieces = pieces
|
||||
this.endpoint = endpoint
|
||||
this.infoHash = infoHash
|
||||
this.file = file
|
||||
this.pieceSize = pieceSize
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@@ -63,70 +70,100 @@ class DownloadSession {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
|
||||
int piece
|
||||
while(true) {
|
||||
piece = downloaded.getRandomPiece()
|
||||
if (claimed.isMarked(piece)) {
|
||||
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
|
||||
log.info("all pieces claimed")
|
||||
int[] pieceAndPosition
|
||||
if (available.isEmpty())
|
||||
pieceAndPosition = pieces.claim()
|
||||
else
|
||||
pieceAndPosition = pieces.claim(new HashSet<>(available))
|
||||
if (pieceAndPosition == null)
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
claimed.markDownloaded(piece)
|
||||
int piece = pieceAndPosition[0]
|
||||
int position = pieceAndPosition[1]
|
||||
boolean steal = pieceAndPosition[2] == 1
|
||||
boolean unclaim = true
|
||||
|
||||
log.info("will download piece $piece")
|
||||
|
||||
long start = piece * pieceSize
|
||||
long end = Math.min(fileLength, start + pieceSize) - 1
|
||||
long length = end - start + 1
|
||||
log.info("will download piece $piece from position $position steal $steal")
|
||||
|
||||
long pieceStart = piece * ((long)pieceSize)
|
||||
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
|
||||
long start = pieceStart + position
|
||||
String root = Base64.encode(infoHash.getRoot())
|
||||
|
||||
FileChannel channel
|
||||
try {
|
||||
os.write("GET $root\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()
|
||||
String code = readTillRN(is)
|
||||
if (code.startsWith("404 ")) {
|
||||
String codeString = readTillRN(is)
|
||||
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")
|
||||
endpoint.close()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (code.startsWith("416 ")) {
|
||||
log.warning("range $start-$end cannot be satisfied")
|
||||
return // leave endpoint open
|
||||
}
|
||||
|
||||
if (!code.startsWith("200 ")) {
|
||||
if (!(code == 200 || code == 416)) {
|
||||
log.warning("unknown code $code")
|
||||
endpoint.close()
|
||||
return false
|
||||
}
|
||||
|
||||
// parse all headers
|
||||
Set<String> headers = new HashSet<>()
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
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
|
||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':')
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("invalid header $header")
|
||||
String key = header.substring(0, colon)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
|
||||
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) {
|
||||
log.warning("We don't support mismatching ranges yet")
|
||||
@@ -135,9 +172,12 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
// start the download
|
||||
FileChannel channel
|
||||
try {
|
||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE))
|
||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1)
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
@@ -148,13 +188,8 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
|
||||
if (timestamps.size() == SAMPLES) {
|
||||
timestamps.removeFirst()
|
||||
reads.removeFirst()
|
||||
}
|
||||
timestamps.addLast(System.currentTimeMillis())
|
||||
reads.addLast(read)
|
||||
dataSinceLastRead += read
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,13 +198,19 @@ class DownloadSession {
|
||||
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)
|
||||
if (hash != expected) {
|
||||
pieces.markPartial(piece, 0)
|
||||
throw new BadHashException("bad hash on piece $piece")
|
||||
}
|
||||
} finally {
|
||||
claimed.clear(piece)
|
||||
try { channel?.close() } catch (IOException ignore) {}
|
||||
DataUtil.tryUnmap(mapped)
|
||||
}
|
||||
pieces.markDownloaded(piece)
|
||||
unclaim = false
|
||||
} finally {
|
||||
if (unclaim && !steal)
|
||||
pieces.unclaim(piece)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -181,24 +222,11 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
if (timestamps.size() < SAMPLES)
|
||||
return 0
|
||||
int totalRead = 0
|
||||
int idx = 0
|
||||
final long now = System.currentTimeMillis()
|
||||
|
||||
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
|
||||
idx++
|
||||
if (idx == SAMPLES)
|
||||
return 0
|
||||
if (idx == SAMPLES - 1)
|
||||
return reads[idx]
|
||||
|
||||
long interval = timestamps.last - timestamps[idx]
|
||||
if (interval == 0)
|
||||
interval = 1
|
||||
for (int i = idx; i < SAMPLES; i++)
|
||||
totalRead += reads[idx]
|
||||
(int)(totalRead * 1000.0 / interval)
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,14 @@ import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import java.nio.file.AtomicMoveNotSupportedException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
@@ -14,13 +19,15 @@ import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
public class Downloader {
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
|
||||
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||
|
||||
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||
@@ -34,7 +41,7 @@ public class Downloader {
|
||||
private final DownloadManager downloadManager
|
||||
private final Persona me
|
||||
private final File file
|
||||
private final Pieces downloaded, claimed
|
||||
private final Pieces pieces
|
||||
private final long length
|
||||
private InfoHash infoHash
|
||||
private final int pieceSize
|
||||
@@ -42,17 +49,25 @@ public class Downloader {
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File piecesFile
|
||||
private final File incompleteFile
|
||||
final int pieceSizePow2
|
||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
|
||||
|
||||
|
||||
private volatile boolean cancelled
|
||||
private volatile boolean eventFired
|
||||
private volatile boolean cancelled, paused
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private ArrayList speedArr = new ArrayList<Integer>()
|
||||
private int speedPos = 0
|
||||
private int speedAvg = 0
|
||||
private long timestamp = Instant.now().toEpochMilli()
|
||||
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||
File incompletes) {
|
||||
File incompletes, Pieces pieces) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.downloadManager = downloadManager
|
||||
@@ -62,18 +77,15 @@ public class Downloader {
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||
this.pieceSizePow2 = pieceSizePow2
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
this.pieces = pieces
|
||||
this.nPieces = pieces.nPieces
|
||||
|
||||
int nPieces
|
||||
if (length % pieceSize == 0)
|
||||
nPieces = length / pieceSize
|
||||
else
|
||||
nPieces = length / pieceSize + 1
|
||||
this.nPieces = nPieces
|
||||
|
||||
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
||||
claimed = new Pieces(nPieces)
|
||||
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
|
||||
// it's easily adjustable by resizing the size of speedArr
|
||||
this.speedArr = [ 0, 0, 0, 0, 0 ]
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
@@ -99,44 +111,76 @@ public class Downloader {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
downloaded.markDownloaded(piece)
|
||||
String [] split = it.split(",")
|
||||
int piece = Integer.parseInt(split[0])
|
||||
if (split.length == 1)
|
||||
pieces.markDownloaded(piece)
|
||||
else {
|
||||
int position = Integer.parseInt(split[1])
|
||||
pieces.markPartial(piece, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writePieces() {
|
||||
synchronized(piecesFile) {
|
||||
if (piecesFileClosed)
|
||||
return
|
||||
piecesFile.withPrintWriter { writer ->
|
||||
downloaded.getDownloaded().each { piece ->
|
||||
writer.println(piece)
|
||||
pieces.write(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long donePieces() {
|
||||
downloaded.donePieces()
|
||||
pieces.donePieces()
|
||||
}
|
||||
|
||||
|
||||
public int speed() {
|
||||
int total = 0
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
total += it.speed()
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
}
|
||||
total
|
||||
|
||||
// normalize to speedArr.size
|
||||
currSpeed /= speedArr.size()
|
||||
|
||||
// compute new speedAvg and update speedArr
|
||||
if ( speedArr[speedPos] > speedAvg ) {
|
||||
speedAvg = 0
|
||||
} else {
|
||||
speedAvg -= speedArr[speedPos]
|
||||
}
|
||||
speedAvg += currSpeed
|
||||
speedArr[speedPos] = currSpeed
|
||||
// this might be necessary due to rounding errors
|
||||
if (speedAvg < 0)
|
||||
speedAvg = 0
|
||||
|
||||
// rolling index over the speedArr
|
||||
speedPos++
|
||||
if (speedPos >= speedArr.size())
|
||||
speedPos=0
|
||||
|
||||
speedAvg
|
||||
}
|
||||
|
||||
public DownloadState getCurrentState() {
|
||||
if (cancelled)
|
||||
return DownloadState.CANCELLED
|
||||
if (paused)
|
||||
return DownloadState.PAUSED
|
||||
|
||||
boolean allFinished = true
|
||||
activeWorkers.values().each {
|
||||
allFinished &= it.currentState == WorkerState.FINISHED
|
||||
}
|
||||
if (allFinished) {
|
||||
if (downloaded.isComplete())
|
||||
if (pieces.isComplete())
|
||||
return DownloadState.FINISHED
|
||||
return DownloadState.FAILED
|
||||
}
|
||||
@@ -170,9 +214,18 @@ public class Downloader {
|
||||
public void cancel() {
|
||||
cancelled = true
|
||||
stop()
|
||||
file.delete()
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
}
|
||||
incompleteFile.delete()
|
||||
pieces.clearAll()
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
paused = true
|
||||
stop()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
activeWorkers.values().each {
|
||||
@@ -190,6 +243,8 @@ public class Downloader {
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
readPieces()
|
||||
destinations.each { destination ->
|
||||
def worker = activeWorkers.get(destination)
|
||||
if (worker != null) {
|
||||
@@ -206,12 +261,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 {
|
||||
private final Destination destination
|
||||
private volatile WorkerState currentState
|
||||
private volatile Thread downloadThread
|
||||
private Endpoint endpoint
|
||||
private volatile DownloadSession currentSession
|
||||
private final Set<Integer> available = new HashSet<>()
|
||||
|
||||
DownloadWorker(Destination destination) {
|
||||
this.destination = destination
|
||||
@@ -231,23 +295,38 @@ public class Downloader {
|
||||
}
|
||||
currentState = WorkerState.DOWNLOADING
|
||||
boolean requestPerformed
|
||||
while(!downloaded.isComplete()) {
|
||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
successfulDestinations.add(endpoint.destination)
|
||||
writePieces()
|
||||
}
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||
} finally {
|
||||
writePieces()
|
||||
currentState = WorkerState.FINISHED
|
||||
if (downloaded.isComplete() && !eventFired) {
|
||||
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
eventFired = true
|
||||
}
|
||||
activeWorkers.values().each {
|
||||
if (it.destination != destination)
|
||||
it.cancel()
|
||||
}
|
||||
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, Collections.emptySet()),
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
class Pieces {
|
||||
private final BitSet bitSet
|
||||
private final BitSet done, claimed
|
||||
private final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@@ -13,52 +14,103 @@ class Pieces {
|
||||
Pieces(int nPieces, float ratio) {
|
||||
this.nPieces = nPieces
|
||||
this.ratio = ratio
|
||||
bitSet = new BitSet(nPieces)
|
||||
done = new BitSet(nPieces)
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
synchronized int getRandomPiece() {
|
||||
int cardinality = bitSet.cardinality()
|
||||
if (cardinality == nPieces)
|
||||
return -1
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces) {
|
||||
// steal
|
||||
int downloadedCardinality = done.cardinality()
|
||||
if (downloadedCardinality == nPieces)
|
||||
return null
|
||||
int rv = done.nextClearBit(0)
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
|
||||
// if fuller than ratio just do sequential
|
||||
if ( (1.0f * cardinality) / nPieces > ratio) {
|
||||
return bitSet.nextClearBit(0)
|
||||
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||
int rv = claimed.nextClearBit(0)
|
||||
claimed.set(rv)
|
||||
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
while(true) {
|
||||
int start = random.nextInt(nPieces)
|
||||
if (bitSet.get(start))
|
||||
if (claimed.get(start))
|
||||
continue
|
||||
return start
|
||||
claimed.set(start)
|
||||
return [start, partials.getOrDefault(start,0), 0]
|
||||
}
|
||||
}
|
||||
|
||||
def getDownloaded() {
|
||||
synchronized int[] claim(Set<Integer> available) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1))
|
||||
available.remove(i)
|
||||
if (available.isEmpty())
|
||||
return null
|
||||
Set<Integer> availableCopy = new HashSet<>(available)
|
||||
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||
availableCopy.remove(i)
|
||||
if (availableCopy.isEmpty()) {
|
||||
// steal
|
||||
int rv = available.first()
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
List<Integer> toList = availableCopy.toList()
|
||||
Collections.shuffle(toList)
|
||||
int rv = toList[0]
|
||||
claimed.set(rv)
|
||||
[rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
synchronized def getDownloaded() {
|
||||
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
|
||||
}
|
||||
|
||||
synchronized void markDownloaded(int piece) {
|
||||
bitSet.set(piece)
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
|
||||
synchronized void clear(int piece) {
|
||||
bitSet.clear(piece)
|
||||
synchronized void markPartial(int piece, int position) {
|
||||
partials.put(piece, position)
|
||||
}
|
||||
|
||||
synchronized void unclaim(int piece) {
|
||||
claimed.clear(piece)
|
||||
}
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
bitSet.cardinality() == nPieces
|
||||
}
|
||||
|
||||
synchronized boolean isMarked(int piece) {
|
||||
bitSet.get(piece)
|
||||
done.cardinality() == nPieces
|
||||
}
|
||||
|
||||
synchronized int donePieces() {
|
||||
bitSet.cardinality()
|
||||
done.cardinality()
|
||||
}
|
||||
|
||||
synchronized boolean isDownloaded(int piece) {
|
||||
done.get(piece)
|
||||
}
|
||||
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
||||
synchronized void write(PrintWriter writer) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
}
|
||||
partials.each { piece, position ->
|
||||
writer.println("$piece,$position")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
@@ -3,8 +3,11 @@ package com.muwire.core.download
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class UIDownloadEvent extends Event {
|
||||
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
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
|
||||
String error
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@@ -18,6 +19,8 @@ class FileHasher {
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
* @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) {
|
||||
if (size <= 0x1 << 30)
|
||||
@@ -57,6 +60,7 @@ class FileHasher {
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
digest.update buf
|
||||
DataUtil.tryUnmap(buf)
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashingEvent extends Event {
|
||||
|
||||
File hashingFile
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " hashingFile " + hashingFile.getAbsolutePath()
|
||||
}
|
||||
|
||||
}
|
@@ -135,4 +135,16 @@ class FileManager {
|
||||
}
|
||||
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 {
|
||||
|
||||
File file
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ class HasherService {
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||
return
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
}
|
||||
@@ -32,13 +32,14 @@ class HasherService {
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
if (f.isDirectory()) {
|
||||
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
|
||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||
} else {
|
||||
if (f.length() == 0) {
|
||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||
} else if (f.length() > FileHasher.MAX_SIZE) {
|
||||
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
||||
} else {
|
||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
@@ -36,14 +37,14 @@ class PersisterService extends Service {
|
||||
timer = new Timer("file persister", true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (location.exists() && location.isFile()) {
|
||||
def slurper = new JsonSlurper()
|
||||
|
@@ -6,7 +6,8 @@ class CacheServers {
|
||||
|
||||
private static final int TO_GIVE = 3
|
||||
private static Set<Destination> CACHES = [
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -7,20 +7,25 @@ class Host {
|
||||
private static final int MAX_FAILURES = 3
|
||||
|
||||
final Destination destination
|
||||
private final int clearInterval
|
||||
int failures,successes
|
||||
long lastAttempt
|
||||
|
||||
public Host(Destination destination) {
|
||||
public Host(Destination destination, int clearInterval) {
|
||||
this.destination = destination
|
||||
this.clearInterval = clearInterval
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
failures = 0
|
||||
successes++
|
||||
lastAttempt = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
synchronized void onFailure() {
|
||||
failures++
|
||||
successes = 0
|
||||
lastAttempt = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
synchronized boolean isFailed() {
|
||||
@@ -34,4 +39,8 @@ class Host {
|
||||
synchronized void clearFailures() {
|
||||
failures = 0
|
||||
}
|
||||
|
||||
synchronized void canTryAgain() {
|
||||
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ class HostCache extends Service {
|
||||
hosts.get(e.destination).clearFailures()
|
||||
return
|
||||
}
|
||||
Host host = new Host(e.destination)
|
||||
Host host = new Host(e.destination, settings.hostClearInterval)
|
||||
if (allowHost(host)) {
|
||||
hosts.put(e.destination, host)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class HostCache extends Service {
|
||||
Destination dest = e.endpoint.destination
|
||||
Host host = hosts.get(dest)
|
||||
if (host == null) {
|
||||
host = new Host(dest)
|
||||
host = new Host(dest, settings.hostClearInterval)
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
|
||||
@@ -106,9 +106,11 @@ class HostCache extends Service {
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
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.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
if (entry.lastAttempt != null)
|
||||
host.lastAttempt = entry.lastAttempt
|
||||
if (allowHost(host))
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
@@ -118,7 +120,7 @@ class HostCache extends Service {
|
||||
}
|
||||
|
||||
private boolean allowHost(Host host) {
|
||||
if (host.isFailed())
|
||||
if (host.isFailed() && !host.canTryAgain())
|
||||
return false
|
||||
if (host.destination == myself)
|
||||
return false
|
||||
@@ -143,6 +145,7 @@ class HostCache extends Service {
|
||||
map.destination = dest.toBase64()
|
||||
map.failures = host.failures
|
||||
map.successes = host.successes
|
||||
map.lastAttempt = host.lastAttempt
|
||||
def json = JsonOutput.toJson(map)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import javax.naming.directory.InvalidSearchControlsException
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
@@ -7,6 +9,7 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class ResultsParser {
|
||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||
@@ -58,6 +61,7 @@ class ResultsParser {
|
||||
size : size,
|
||||
infohash : parsedIH,
|
||||
pieceSize : pieceSize,
|
||||
sources : Collections.emptySet(),
|
||||
uuid : uuid)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
@@ -82,11 +86,17 @@ class ResultsParser {
|
||||
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.ThreadFactory
|
||||
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.InfoHash
|
||||
|
||||
@@ -54,12 +57,16 @@ class ResultsSender {
|
||||
int pieceSize = it.getPieceSize()
|
||||
if (pieceSize == 0)
|
||||
pieceSize = FileHasher.getPieceSize(length)
|
||||
Set<Destination> suggested = Collections.emptySet()
|
||||
if (it instanceof DownloadedFile)
|
||||
suggested = it.sources
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid
|
||||
uuid : uuid,
|
||||
sources : suggested
|
||||
)
|
||||
eventBus.publish(uiResultEvent)
|
||||
}
|
||||
@@ -77,6 +84,7 @@ class ResultsSender {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
@@ -110,6 +118,10 @@ class ResultsSender {
|
||||
}
|
||||
obj.hashList = hashListB64
|
||||
}
|
||||
|
||||
if (it instanceof DownloadedFile)
|
||||
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -118,6 +130,9 @@ class ResultsSender {
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "problem sending results",e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -33,7 +33,10 @@ class SearchIndex {
|
||||
|
||||
private static String[] split(String source) {
|
||||
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) {
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIResultBatchEvent extends Event {
|
||||
UUID uuid
|
||||
UIResultEvent[] results
|
||||
}
|
@@ -4,8 +4,11 @@ import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class UIResultEvent extends Event {
|
||||
Persona sender
|
||||
Set<Destination> sources
|
||||
UUID uuid
|
||||
String name
|
||||
long size
|
||||
|
@@ -0,0 +1,31 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<Persona> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
||||
RemoteTrustList(Persona persona) {
|
||||
this.persona = persona
|
||||
good = new ConcurrentHashSet<>()
|
||||
bad = new ConcurrentHashSet<>()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof RemoteTrustList))
|
||||
return false
|
||||
RemoteTrustList other = (RemoteTrustList)o
|
||||
persona == other.persona
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class TrustSubscriber {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector i2pConnector
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
|
||||
|
||||
private final Object waitLock = new Object()
|
||||
private volatile boolean shutdown
|
||||
private volatile Thread thread
|
||||
private final ExecutorService updateThreads = Executors.newCachedThreadPool()
|
||||
|
||||
TrustSubscriber(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.i2pConnector = i2pConnector
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
thread = new Thread({checkLoop()} as Runnable, "trust-subscriber")
|
||||
thread.setDaemon(true)
|
||||
thread.start()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
thread?.interrupt()
|
||||
updateThreads.shutdownNow()
|
||||
}
|
||||
|
||||
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
||||
if (!e.subscribe) {
|
||||
remoteTrustLists.remove(e.persona.destination)
|
||||
} else {
|
||||
RemoteTrustList trustList = remoteTrustLists.putIfAbsent(e.persona.destination, new RemoteTrustList(e.persona))
|
||||
trustList?.forceUpdate = true
|
||||
synchronized(waitLock) {
|
||||
waitLock.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLoop() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
synchronized(waitLock) {
|
||||
waitLock.wait(60 * 1000)
|
||||
}
|
||||
final long now = System.currentTimeMillis()
|
||||
remoteTrustLists.values().each { trustList ->
|
||||
if (trustList.status == RemoteTrustList.Status.UPDATING)
|
||||
return
|
||||
if (!trustList.forceUpdate &&
|
||||
now - trustList.timestamp < settings.trustListInterval * 60 * 60 * 1000)
|
||||
return
|
||||
trustList.forceUpdate = false
|
||||
updateThreads.submit(new UpdateJob(trustList))
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateJob implements Runnable {
|
||||
|
||||
private final RemoteTrustList trustList
|
||||
|
||||
UpdateJob(RemoteTrustList trustList) {
|
||||
this.trustList = trustList
|
||||
}
|
||||
|
||||
public void run() {
|
||||
trustList.status = RemoteTrustList.Status.UPDATING
|
||||
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||
if (check(trustList, System.currentTimeMillis()))
|
||||
trustList.status = RemoteTrustList.Status.UPDATED
|
||||
else
|
||||
trustList.status = RemoteTrustList.Status.UPDATE_FAILED
|
||||
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||
}
|
||||
}
|
||||
|
||||
private boolean check(RemoteTrustList trustList, long now) {
|
||||
log.info("fetching trust list from ${trustList.persona.getHumanReadableName()}")
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String codeString = DataUtil.readTillRN(is)
|
||||
int space = codeString.indexOf(' ')
|
||||
if (space > 0)
|
||||
codeString = codeString.substring(0,space)
|
||||
int code = Integer.parseInt(codeString.trim())
|
||||
|
||||
if (code != 200) {
|
||||
log.info("couldn't fetch trust list, code $code")
|
||||
return false
|
||||
}
|
||||
|
||||
// swallow any headers
|
||||
String header
|
||||
while (( header = DataUtil.readTillRN(is)) != "");
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
|
||||
Set<Persona> good = new HashSet<>()
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(p)
|
||||
}
|
||||
|
||||
Set<Persona> bad = new HashSet<>()
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(p)
|
||||
}
|
||||
|
||||
trustList.timestamp = now
|
||||
trustList.good.clear()
|
||||
trustList.good.addAll(good)
|
||||
trustList.bad.clear()
|
||||
trustList.bad.addAll(bad)
|
||||
|
||||
return true
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"exception fetching trust list from ${trustList.persona.getHumanReadableName()}",e)
|
||||
return false
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class TrustSubscriptionEvent extends Event {
|
||||
Persona persona
|
||||
boolean subscribe
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class TrustSubscriptionUpdatedEvent extends Event {
|
||||
RemoteTrustList trustList
|
||||
}
|
@@ -3,7 +3,15 @@ package com.muwire.core.update
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
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.JsonSlurper
|
||||
@@ -13,6 +21,7 @@ import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.VersionComparator
|
||||
|
||||
@Log
|
||||
@@ -21,16 +30,24 @@ class UpdateClient {
|
||||
final I2PSession session
|
||||
final String myVersion
|
||||
final MuWireSettings settings
|
||||
final FileManager fileManager
|
||||
final Persona me
|
||||
|
||||
private final Timer timer
|
||||
|
||||
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.session = session
|
||||
this.myVersion = myVersion
|
||||
this.settings = settings
|
||||
this.fileManager = fileManager
|
||||
this.me = me
|
||||
timer = new Timer("update-client",true)
|
||||
}
|
||||
|
||||
@@ -43,6 +60,24 @@ class UpdateClient {
|
||||
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() {
|
||||
final long now = System.currentTimeMillis()
|
||||
if (lastUpdateCheckTime > 0) {
|
||||
@@ -106,8 +141,32 @@ class UpdateClient {
|
||||
return
|
||||
}
|
||||
|
||||
String 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 : payload.infoHash))
|
||||
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) {
|
||||
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
|
||||
}
|
@@ -2,4 +2,5 @@ package com.muwire.core.upload
|
||||
|
||||
class ContentRequest extends Request {
|
||||
Range range
|
||||
int have
|
||||
}
|
||||
|
@@ -5,34 +5,58 @@ 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) {
|
||||
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()
|
||||
if (range.start >= file.length() || range.end >= file.length()) {
|
||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
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\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
|
||||
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)
|
||||
@@ -48,7 +72,55 @@ class ContentUploader extends Uploader {
|
||||
} 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(9, 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSize() {
|
||||
return file.length();
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ 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
|
||||
@@ -14,6 +16,7 @@ class HashListUploader extends Uploader {
|
||||
super(endpoint)
|
||||
this.infoHash = infoHash
|
||||
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||
this.request = request
|
||||
}
|
||||
|
||||
void respond() {
|
||||
@@ -32,4 +35,34 @@ class HashListUploader extends Uploader {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSize() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
@@ -48,8 +49,14 @@ class Request {
|
||||
def decoded = Base64.decode(encoded)
|
||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||
}
|
||||
|
||||
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)
|
||||
headers : headers, downloader : downloader, have : have)
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
@@ -6,7 +6,12 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
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.mesh.Mesh
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
@@ -15,12 +20,17 @@ import net.i2p.data.Base64
|
||||
public class UploadManager {
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final MeshManager meshManager
|
||||
private final DownloadManager downloadManager
|
||||
|
||||
public UploadManager() {}
|
||||
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager) {
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||
MeshManager meshManager, DownloadManager downloadManager) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.meshManager = meshManager
|
||||
this.downloadManager = downloadManager
|
||||
}
|
||||
|
||||
public void processGET(Endpoint e) throws IOException {
|
||||
@@ -44,8 +54,10 @@ public class UploadManager {
|
||||
log.info("Responding to upload request for root $infoHashString")
|
||||
|
||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||
InfoHash infoHash = new InfoHash(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"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
@@ -61,13 +73,31 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
ContentRequest request = Request.parseContentRequest(infoHash, e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
Uploader uploader = new ContentUploader(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))
|
||||
try {
|
||||
uploader.respond()
|
||||
@@ -85,8 +115,10 @@ public class UploadManager {
|
||||
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 (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
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()
|
||||
@@ -102,13 +134,30 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
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
|
||||
}
|
||||
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
|
||||
|
||||
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()
|
||||
@@ -130,8 +179,10 @@ public class UploadManager {
|
||||
log.info("Responding to upload request for root $infoHashString")
|
||||
|
||||
infoHashRoot = Base64.decode(infoHashString)
|
||||
infoHash = new InfoHash(infoHashRoot)
|
||||
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
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()
|
||||
@@ -153,7 +204,25 @@ public class UploadManager {
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
uploader = new ContentUploader(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 = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
|
@@ -23,4 +23,19 @@ abstract class Uploader {
|
||||
return -1
|
||||
mapped.position()
|
||||
}
|
||||
|
||||
abstract String getName();
|
||||
|
||||
/**
|
||||
* @return an integer between 0 and 100
|
||||
*/
|
||||
abstract int getProgress();
|
||||
|
||||
abstract String getDownloader();
|
||||
|
||||
abstract int getDonePieces();
|
||||
|
||||
abstract int getTotalPieces();
|
||||
|
||||
abstract long getTotalSize();
|
||||
}
|
||||
|
@@ -1,9 +1,14 @@
|
||||
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 com.muwire.core.Constants
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||
@@ -79,4 +84,73 @@ class DataUtil {
|
||||
}
|
||||
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;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.data.Destination;
|
||||
@@ -9,7 +10,8 @@ public class DownloadedFile extends SharedFile {
|
||||
|
||||
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);
|
||||
this.sources = sources;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class SharedFile {
|
||||
|
||||
@@ -8,10 +9,15 @@ public class SharedFile {
|
||||
private final InfoHash infoHash;
|
||||
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.infoHash = infoHash;
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
@@ -25,4 +31,34 @@ public class SharedFile {
|
||||
public int getPieceSize() {
|
||||
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
|
||||
|
||||
import static org.junit.Assert.fail
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
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.files.FileHasher
|
||||
import static com.muwire.core.util.DataUtil.readTillRN
|
||||
import static com.muwire.core.util.DataUtil.encodeXHave
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class DownloadSessionTest {
|
||||
|
||||
private EventBus eventBus
|
||||
private File source, target
|
||||
private InfoHash infoHash
|
||||
private Endpoint endpoint
|
||||
private Pieces pieces, claimed
|
||||
private Pieces pieces
|
||||
private String rootBase64
|
||||
|
||||
private DownloadSession session
|
||||
@@ -24,6 +33,16 @@ class DownloadSessionTest {
|
||||
private InputStream fromDownloader, fromUploader
|
||||
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 = []) {
|
||||
Random r = new Random()
|
||||
byte [] content = new byte[size]
|
||||
@@ -48,8 +67,7 @@ class DownloadSessionTest {
|
||||
else
|
||||
nPieces = size / pieceSize + 1
|
||||
pieces = new Pieces(nPieces)
|
||||
claimed = new Pieces(nPieces)
|
||||
claimedPieces.each {claimed.markDownloaded(it)}
|
||||
claimedPieces.each {pieces.claimed.set(it)}
|
||||
|
||||
fromDownloader = new PipedInputStream()
|
||||
fromUploader = new PipedInputStream()
|
||||
@@ -57,12 +75,20 @@ class DownloadSessionTest {
|
||||
toUploader = new PipedOutputStream(fromDownloader)
|
||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||
|
||||
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
|
||||
downloadThread = new Thread( { session.request() } as Runnable)
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
|
||||
downloadThread = new Thread( { perform() } as Runnable)
|
||||
downloadThread.setDaemon(true)
|
||||
downloadThread.start()
|
||||
}
|
||||
|
||||
private void perform() {
|
||||
try {
|
||||
performed = session.request()
|
||||
} catch (IOException e) {
|
||||
thrown = e
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
source?.delete()
|
||||
@@ -77,6 +103,7 @@ class DownloadSessionTest {
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
assert "Range: 0-19" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@@ -88,6 +115,9 @@ class DownloadSessionTest {
|
||||
|
||||
assert pieces.isComplete()
|
||||
assert target.bytes == source.bytes
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,6 +129,7 @@ class DownloadSessionTest {
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
toDownloader.write("200 OK\r\n".bytes)
|
||||
@@ -109,6 +140,9 @@ class DownloadSessionTest {
|
||||
Thread.sleep(150)
|
||||
assert pieces.isComplete()
|
||||
assert target.bytes == source.bytes
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,6 +160,7 @@ class DownloadSessionTest {
|
||||
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
||||
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
||||
|
||||
readTillRN(fromDownloader)
|
||||
readTillRN(fromDownloader)
|
||||
assert "" == readTillRN(fromDownloader)
|
||||
|
||||
@@ -139,6 +174,9 @@ class DownloadSessionTest {
|
||||
Thread.sleep(150)
|
||||
assert !pieces.isComplete()
|
||||
assert 1 == pieces.donePieces()
|
||||
assert performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,7 +184,10 @@ class DownloadSessionTest {
|
||||
initSession(20, [0])
|
||||
long now = System.currentTimeMillis()
|
||||
downloadThread.join(100)
|
||||
assert 100 > (System.currentTimeMillis() - now)
|
||||
assert 100 >= (System.currentTimeMillis() - now)
|
||||
assert !performed
|
||||
assert available.isEmpty()
|
||||
assert thrown == null
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -154,7 +195,7 @@ class DownloadSessionTest {
|
||||
int pieceSize = FileHasher.getPieceSize(1)
|
||||
int size = (1 << pieceSize) * 10
|
||||
initSession(size, [1,2,3,4,5,6,7,8,9])
|
||||
assert !claimed.isMarked(0)
|
||||
assert !pieces.claimed.get(0)
|
||||
|
||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||
String range = readTillRN(fromDownloader)
|
||||
@@ -162,7 +203,131 @@ class DownloadSessionTest {
|
||||
int start = Integer.parseInt(matcher[0][1])
|
||||
int end = Integer.parseInt(matcher[0][2])
|
||||
|
||||
assert claimed.isMarked(0)
|
||||
assert pieces.claimed.get(0)
|
||||
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() {
|
||||
pieces = new Pieces(1)
|
||||
assert !pieces.isComplete()
|
||||
assert pieces.getRandomPiece() == 0
|
||||
assert pieces.claim() == 0
|
||||
pieces.markDownloaded(0)
|
||||
assert pieces.isComplete()
|
||||
}
|
||||
@@ -25,13 +25,28 @@ class PiecesTest {
|
||||
public void testTwoPieces() {
|
||||
pieces = new Pieces(2)
|
||||
assert !pieces.isComplete()
|
||||
int piece = pieces.getRandomPiece()
|
||||
int piece = pieces.claim()
|
||||
assert piece == 0 || piece == 1
|
||||
pieces.markDownloaded(piece)
|
||||
assert !pieces.isComplete()
|
||||
int piece2 = pieces.getRandomPiece()
|
||||
int piece2 = pieces.claim()
|
||||
assert piece != piece2
|
||||
pieces.markDownloaded(piece2)
|
||||
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() {
|
||||
assert 17 == FileHasher.getPieceSize(1000000)
|
||||
assert 17 == FileHasher.getPieceSize(100000000)
|
||||
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||
shouldFail IllegalArgumentException, {
|
||||
FileHasher.getPieceSize(Long.MAX_VALUE)
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ class HasherServiceTest {
|
||||
hasher = new FileHasher()
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||
eventBus.register(FileHashedEvent.class, listener)
|
||||
eventBus.register(FileSharedEvent.class, service)
|
||||
service.start()
|
||||
}
|
||||
|
||||
|
@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
|
||||
persisted.write json
|
||||
|
||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(2000)
|
||||
|
||||
assert listener.publishedFiles.size() == 1
|
||||
@@ -121,7 +121,7 @@ class PersisterServiceLoadingTest {
|
||||
persisted.write json
|
||||
|
||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(2000)
|
||||
|
||||
assert listener.publishedFiles.size() == 1
|
||||
@@ -163,7 +163,7 @@ class PersisterServiceLoadingTest {
|
||||
persisted.append "$json2\n"
|
||||
|
||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(2000)
|
||||
|
||||
assert listener.publishedFiles.size() == 2
|
||||
@@ -195,7 +195,7 @@ class PersisterServiceLoadingTest {
|
||||
persisted.write json1
|
||||
|
||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(2000)
|
||||
|
||||
assert listener.publishedFiles.size() == 1
|
||||
|
@@ -58,7 +58,7 @@ class PersisterServiceSavingTest {
|
||||
sf = new SharedFile(f, ih, 0)
|
||||
|
||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(1500)
|
||||
|
||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||
@@ -77,7 +77,7 @@ class PersisterServiceSavingTest {
|
||||
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
|
||||
|
||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||
ps.start()
|
||||
ps.onUILoadedEvent(null)
|
||||
Thread.sleep(1500)
|
||||
|
||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||
|
@@ -83,4 +83,11 @@ class SearchIndexTest {
|
||||
assert found.size() == 1
|
||||
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 {
|
||||
|
||||
Request request
|
||||
ContentRequest request
|
||||
|
||||
private void fromString(String requestString) {
|
||||
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) {
|
||||
try {
|
||||
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
|
||||
} catch (IOException expected) {}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ class UploaderTest {
|
||||
InputStream is
|
||||
OutputStream os
|
||||
|
||||
Request request
|
||||
ContentRequest request
|
||||
Uploader uploader
|
||||
|
||||
byte[] inFile
|
||||
@@ -52,7 +52,7 @@ class UploaderTest {
|
||||
}
|
||||
|
||||
private void startUpload() {
|
||||
uploader = new Uploader(file, request, endpoint)
|
||||
uploader = new ContentUploader(file, request, endpoint)
|
||||
uploadThread = new Thread(uploader.respond() as Runnable)
|
||||
uploadThread.setDaemon(true)
|
||||
uploadThread.start()
|
||||
@@ -77,7 +77,7 @@ class UploaderTest {
|
||||
@Test
|
||||
public void testSmallFile() {
|
||||
fillFile(20)
|
||||
request = new Request(range : new Range(0,19))
|
||||
request = new ContentRequest(range : new Range(0,19))
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 0-19" == readUntilRN()
|
||||
@@ -92,7 +92,7 @@ class UploaderTest {
|
||||
@Test
|
||||
public void testRequestMiddle() {
|
||||
fillFile(20)
|
||||
request = new Request(range : new Range(5,15))
|
||||
request = new ContentRequest(range : new Range(5,15))
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 5-15" == readUntilRN()
|
||||
@@ -108,7 +108,7 @@ class UploaderTest {
|
||||
@Test
|
||||
public void testOutOfRange() {
|
||||
fillFile(20)
|
||||
request = new Request(range : new Range(0,20))
|
||||
request = new ContentRequest(range : new Range(0,20))
|
||||
startUpload()
|
||||
assert "416 Range Not Satisfiable" == readUntilRN()
|
||||
assert "" == readUntilRN()
|
||||
@@ -118,7 +118,7 @@ class UploaderTest {
|
||||
public void testLargeFile() {
|
||||
final int length = 0x1 << 14
|
||||
fillFile(length)
|
||||
request = new Request(range : new Range(0, length - 1))
|
||||
request = new ContentRequest(range : new Range(0, length - 1))
|
||||
startUpload()
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
|
@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
|
||||
|
||||
### 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
|
||||
|
||||
|
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.
|
@@ -1,5 +1,8 @@
|
||||
group = com.muwire
|
||||
version = 0.1.11
|
||||
version = 0.4.8
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
|
34
gradlew
vendored
@@ -11,21 +11,21 @@
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
ls=$(ls -ld "$PRG")
|
||||
link=$(expr "$ls" : '.*-> \(.*\)$')
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
PRG=$(dirname "$PRG")"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
SAVED="$(pwd)"
|
||||
cd "$(dirname "$PRG")/" >/dev/null
|
||||
APP_HOME="$(pwd -P)"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=$(basename "$0")
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
@@ -49,7 +49,7 @@ cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
case "$(uname)" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
@@ -90,7 +90,7 @@ fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
MAX_FD_LIMIT=$(ulimit -H -n)
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
@@ -111,12 +111,12 @@ fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
APP_HOME=$(cygpath --path --mixed "$APP_HOME")
|
||||
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
|
||||
JAVACMD=$(cygpath --unix "$JAVACMD")
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
@@ -130,13 +130,13 @@ if $cygwin ; then
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
CHECK=$(echo "$arg"|egrep -c "$OURCYGPATTERN" -)
|
||||
CHECK2=$(echo "$arg"|egrep -c "^-") ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg")
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
eval $(echo args$i)="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
|
@@ -26,4 +26,24 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.OptionsView'
|
||||
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'
|
||||
}
|
||||
'trust-list' {
|
||||
model = 'com.muwire.gui.TrustListModel'
|
||||
view = 'com.muwire.gui.TrustListView'
|
||||
controller = 'com.muwire.gui.TrustListController'
|
||||
}
|
||||
'content-panel' {
|
||||
model = 'com.muwire.gui.ContentPanelModel'
|
||||
view = 'com.muwire.gui.ContentPanelView'
|
||||
controller = 'com.muwire.gui.ContentPanelController'
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,95 @@
|
||||
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
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.Match
|
||||
import com.muwire.core.content.Matcher
|
||||
import com.muwire.core.content.RegexMatcher
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ContentPanelController {
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelModel model
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelView view
|
||||
|
||||
Core core
|
||||
|
||||
@ControllerAction
|
||||
void addRule() {
|
||||
def term = view.ruleTextField.text
|
||||
|
||||
if (model.regex)
|
||||
core.muOptions.watchedRegexes.add(term)
|
||||
else
|
||||
core.muOptions.watchedKeywords.add(term)
|
||||
saveMuWireSettings()
|
||||
|
||||
core.eventBus.publish(new ContentControlEvent(term : term, regex : model.regex, add:true))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void deleteRule() {
|
||||
int rule = view.getSelectedRule()
|
||||
if (rule < 0)
|
||||
return
|
||||
Matcher matcher = model.rules[rule]
|
||||
String term = matcher.getTerm()
|
||||
if (matcher instanceof RegexMatcher)
|
||||
core.muOptions.watchedRegexes.remove(term)
|
||||
else
|
||||
core.muOptions.watchedKeywords.remove(term)
|
||||
saveMuWireSettings()
|
||||
|
||||
core.eventBus.publish(new ContentControlEvent(term : term, regex : (matcher instanceof RegexMatcher), add: false))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void keyword() {
|
||||
model.regex = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void regex() {
|
||||
model.regex = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void refresh() {
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
File f = new File(core.home, "MuWire.properties")
|
||||
f.withOutputStream {
|
||||
core.muOptions.write(it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
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.floodfill = router._context.netDb().floodfillEnabled()
|
||||
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()
|
||||
}
|
||||
}
|
@@ -11,16 +11,25 @@ import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
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.SearchEvent
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -30,6 +39,8 @@ class MainFrameController {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
MainFrameModel model
|
||||
@MVCMember @Nonnull
|
||||
MainFrameView view
|
||||
|
||||
private volatile Core core
|
||||
|
||||
@@ -39,6 +50,11 @@ class MainFrameController {
|
||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||
|
||||
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()
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = search
|
||||
@@ -46,14 +62,27 @@ class MainFrameController {
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
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
|
||||
if (model.hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
|
||||
} else {
|
||||
// this can be improved a lot
|
||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
|
||||
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,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
@@ -86,16 +115,17 @@ class MainFrameController {
|
||||
return
|
||||
def sortEvt = group.view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
row = sortEvt.convertPreviousRowIndexToModel(row)
|
||||
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||
}
|
||||
group.model.results[row]
|
||||
}
|
||||
|
||||
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
|
||||
if (sortEvt != null)
|
||||
selected = sortEvt.convertPreviousRowIndexToModel(selected)
|
||||
selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
selected
|
||||
}
|
||||
|
||||
@@ -103,7 +133,10 @@ class MainFrameController {
|
||||
void download() {
|
||||
def result = selectedResult()
|
||||
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)
|
||||
|
||||
@@ -111,8 +144,9 @@ class MainFrameController {
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
|
||||
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
|
||||
@@ -131,10 +165,29 @@ class MainFrameController {
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trustPersonaFromSearch() {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrustPersonaFromSearch() {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
downloader.cancel()
|
||||
model.downloadInfoHashes.remove(downloader.getInfoHash())
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
@@ -142,47 +195,133 @@ class MainFrameController {
|
||||
void resume() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
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) {
|
||||
int row = builder.getVariable(tableName).getSelectedRow()
|
||||
int row = view.getSelectedTrustTablesRow(tableName)
|
||||
if (row < 0)
|
||||
return
|
||||
builder.getVariable(tableName).model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markTrusted() {
|
||||
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
|
||||
model.markTrustedButtonEnabled = false
|
||||
model.markNeutralFromDistrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markNeutralFromDistrusted() {
|
||||
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
|
||||
model.markTrustedButtonEnabled = false
|
||||
model.markNeutralFromDistrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markDistrusted() {
|
||||
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markNeutralFromTrusted() {
|
||||
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void keywordSearch() {
|
||||
model.hashSearch = false
|
||||
void subscribe() {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row]
|
||||
core.muOptions.trustSubscriptions.add(p)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void hashSearch() {
|
||||
model.hashSearch = true
|
||||
void review() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
Map<String,Object> env = new HashMap<>()
|
||||
env["trustList"] = list
|
||||
env["trustService"] = core.trustService
|
||||
env["eventBus"] = core.eventBus
|
||||
mvcGroup.createMVCGroup("trust-list", env)
|
||||
|
||||
}
|
||||
|
||||
void unshareSelectedFiles() {
|
||||
println "unsharing selected files"
|
||||
@ControllerAction
|
||||
void update() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : true))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void unsubscribe() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
core.muOptions.trustSubscriptions.remove(list.persona)
|
||||
saveMuWireSettings()
|
||||
model.subscriptions.remove(list)
|
||||
JTable table = builder.getVariable("subscription-table")
|
||||
table.model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : false))
|
||||
}
|
||||
|
||||
private RemoteTrustList getSelectedTrustList() {
|
||||
int row = view.getSelectedTrustTablesRow("subscription-table")
|
||||
if (row < 0)
|
||||
return null
|
||||
model.subscriptions[row]
|
||||
}
|
||||
|
||||
void unshareSelectedFile() {
|
||||
SharedFile sf = view.selectedSharedFile()
|
||||
if (sf == null)
|
||||
return
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
}
|
||||
|
||||
void stopWatchingDirectory() {
|
||||
String directory = mvcGroup.view.getSelectedWatchedDirectory()
|
||||
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) {
|
||||
|
@@ -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.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JFileChooser
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class OptionsController {
|
||||
@@ -19,6 +25,7 @@ class OptionsController {
|
||||
void save() {
|
||||
String text
|
||||
Core core = application.context.get("core")
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
|
||||
def i2pProps = core.i2pOptions
|
||||
|
||||
@@ -38,6 +45,17 @@ class OptionsController {
|
||||
model.outboundLength = 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")
|
||||
i2pSettingsFile.withOutputStream {
|
||||
i2pProps.store(it,"")
|
||||
@@ -46,21 +64,46 @@ class OptionsController {
|
||||
text = view.retryField.text
|
||||
model.downloadRetryInterval = text
|
||||
|
||||
def settings = application.context.get("muwire-settings")
|
||||
settings.downloadRetryInterval = Integer.valueOf(text)
|
||||
|
||||
text = view.updateField.text
|
||||
model.updateCheckInterval = text
|
||||
settings.updateCheckInterval = Integer.valueOf(text)
|
||||
|
||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||
model.onlyTrusted = onlyTrusted
|
||||
settings.setAllowUntrusted(!onlyTrusted)
|
||||
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
|
||||
model.autoDownloadUpdate = autoDownloadUpdate
|
||||
settings.autoDownloadUpdate = autoDownloadUpdate
|
||||
|
||||
|
||||
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
||||
model.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)
|
||||
}
|
||||
|
||||
|
||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||
model.onlyTrusted = onlyTrusted
|
||||
settings.setAllowUntrusted(!onlyTrusted)
|
||||
|
||||
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
|
||||
model.trustLists = trustLists
|
||||
settings.allowTrustLists = trustLists
|
||||
|
||||
String trustListInterval = view.trustListIntervalField.text
|
||||
model.trustListInterval = trustListInterval
|
||||
settings.trustListInterval = Integer.parseInt(trustListInterval)
|
||||
|
||||
File settingsFile = new File(core.home, "MuWire.properties")
|
||||
settingsFile.withOutputStream {
|
||||
settings.write(it)
|
||||
@@ -77,9 +120,9 @@ class OptionsController {
|
||||
model.font = text
|
||||
uiSettings.font = text
|
||||
|
||||
boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||
model.showMonitor = showMonitor
|
||||
uiSettings.showMonitor = showMonitor
|
||||
// boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||
// model.showMonitor = showMonitor
|
||||
// uiSettings.showMonitor = showMonitor
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -93,6 +136,10 @@ class OptionsController {
|
||||
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)
|
||||
@@ -106,4 +153,15 @@ class OptionsController {
|
||||
view.d.setVisible(false)
|
||||
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()
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
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.EventBus
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class TrustListController {
|
||||
@MVCMember @Nonnull
|
||||
TrustListModel model
|
||||
@MVCMember @Nonnull
|
||||
TrustListView view
|
||||
|
||||
EventBus eventBus
|
||||
|
||||
@ControllerAction
|
||||
void trustFromTrusted() {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trustFromDistrusted() {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrustFromTrusted() {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrustFromDistrusted() {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
}
|
@@ -43,6 +43,8 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
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()) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.env.Metadata
|
||||
import groovy.util.logging.Log
|
||||
@@ -47,6 +48,8 @@ class Ready extends AbstractLifecycleHandler {
|
||||
} else {
|
||||
log.info("creating new properties")
|
||||
props = new MuWireSettings()
|
||||
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
def nickname
|
||||
while (true) {
|
||||
nickname = JOptionPane.showInputDialog(null,
|
||||
@@ -73,6 +76,7 @@ class Ready extends AbstractLifecycleHandler {
|
||||
props.downloadLocation = new File(portableDownloads)
|
||||
} else {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
chooser.setDialogTitle("Select a directory where downloads will be saved")
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
@@ -97,6 +101,9 @@ class Ready extends AbstractLifecycleHandler {
|
||||
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
||||
System.exit(0)
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook({
|
||||
core.shutdown()
|
||||
})
|
||||
core.startServices()
|
||||
application.context.put("muwire-settings", props)
|
||||
application.context.put("core",core)
|
||||
@@ -104,12 +111,6 @@ class Ready extends AbstractLifecycleHandler {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ContentPanelModel {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelView view
|
||||
|
||||
Core core
|
||||
|
||||
private ContentManager contentManager
|
||||
|
||||
def rules = []
|
||||
def hits = []
|
||||
|
||||
@Observable boolean regex
|
||||
@Observable boolean deleteButtonEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
contentManager = application.context.get("core").contentManager
|
||||
rules.addAll(contentManager.matchers)
|
||||
core.eventBus.register(ContentControlEvent.class, this)
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
core.eventBus.unregister(ContentControlEvent.class, this)
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
rules.clear()
|
||||
rules.addAll(contentManager.matchers)
|
||||
hits.clear()
|
||||
view.rulesTable.model.fireTableDataChanged()
|
||||
}
|
||||
|
||||
void onContentControlEvent(ContentControlEvent e) {
|
||||
runInsideUIAsync {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
28
gui/griffon-app/models/com/muwire/gui/I2PStatusModel.groovy
Normal file
@@ -0,0 +1,28 @@
|
||||
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 boolean floodfill
|
||||
@Observable int participatingTunnels
|
||||
@Observable int activePeers
|
||||
@Observable int receiveBps
|
||||
@Observable int sendBps
|
||||
@Observable int participatingBW
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
controller.refresh()
|
||||
}
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
@@ -9,21 +11,31 @@ import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.RouterDisconnectedEvent
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
|
||||
import com.muwire.core.update.UpdateAvailableEvent
|
||||
import com.muwire.core.update.UpdateDownloadedEvent
|
||||
import com.muwire.core.upload.UploadEvent
|
||||
import com.muwire.core.upload.UploadFinishedEvent
|
||||
|
||||
@@ -34,6 +46,7 @@ import griffon.core.mvc.MVCGroup
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.FXObservable
|
||||
import griffon.transform.Observable
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@@ -46,30 +59,48 @@ class MainFrameModel {
|
||||
MainFrameController controller
|
||||
@Inject @Nonnull GriffonApplication application
|
||||
@Observable boolean coreInitialized = false
|
||||
@Observable boolean routerPresent
|
||||
|
||||
def results = new ConcurrentHashMap<>()
|
||||
def downloads = []
|
||||
def uploads = []
|
||||
def shared = []
|
||||
def watched = []
|
||||
def connectionList = []
|
||||
def searches = new LinkedList()
|
||||
def trusted = []
|
||||
def distrusted = []
|
||||
|
||||
boolean hashSearch
|
||||
def subscriptions = []
|
||||
|
||||
@Observable int connections
|
||||
@Observable String me
|
||||
@Observable boolean searchButtonsEnabled
|
||||
@Observable int loadedFiles
|
||||
@Observable File hashingFile
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean cancelButtonEnabled
|
||||
@Observable boolean retryButtonEnabled
|
||||
@Observable boolean pauseButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||
@Observable boolean markDistrustedButtonEnabled
|
||||
@Observable boolean markNeutralFromDistrustedButtonEnabled
|
||||
@Observable boolean markTrustedButtonEnabled
|
||||
@Observable boolean reviewButtonEnabled
|
||||
@Observable boolean updateButtonEnabled
|
||||
@Observable boolean unsubscribeButtonEnabled
|
||||
|
||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||
|
||||
volatile Core core
|
||||
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||
|
||||
@Observable volatile Core core
|
||||
|
||||
private long lastRetryTime = System.currentTimeMillis()
|
||||
|
||||
UISettings uiSettings
|
||||
|
||||
void updateTablePreservingSelection(String tableName) {
|
||||
def downloadTable = builder.getVariable(tableName)
|
||||
int selectedRow = downloadTable.getSelectedRow()
|
||||
@@ -79,7 +110,7 @@ class MainFrameModel {
|
||||
|
||||
void mvcGroupInit(Map<String, Object> args) {
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
uiSettings = application.context.get("ui-settings")
|
||||
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
@@ -112,12 +143,15 @@ class MainFrameModel {
|
||||
application.addPropertyChangeListener("core", {e ->
|
||||
coreInitialized = (e.getNewValue() != null)
|
||||
core = e.getNewValue()
|
||||
routerPresent = core.router != null
|
||||
me = core.me.getHumanReadableName()
|
||||
core.eventBus.register(UIResultEvent.class, this)
|
||||
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||
core.eventBus.register(ConnectionEvent.class, this)
|
||||
core.eventBus.register(DisconnectionEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(FileHashingEvent.class, this)
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
@@ -125,9 +159,23 @@ class MainFrameModel {
|
||||
core.eventBus.register(QueryEvent.class, this)
|
||||
core.eventBus.register(UpdateAvailableEvent.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)
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
|
||||
core.muOptions.watchedKeywords.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
||||
}
|
||||
core.muOptions.watchedRegexes.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: true, add: true))
|
||||
}
|
||||
|
||||
timer.schedule({
|
||||
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
long now = System.currentTimeMillis()
|
||||
@@ -150,19 +198,46 @@ class MainFrameModel {
|
||||
runInsideUIAsync {
|
||||
trusted.addAll(core.trustService.good.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))) }
|
||||
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
MVCGroup resultsGroup = results.get(e.uuid)
|
||||
resultsGroup?.model.handleResult(e)
|
||||
}
|
||||
|
||||
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||
MVCGroup resultsGroup = results.get(e.uuid)
|
||||
resultsGroup?.model?.handleResultBatch(e.results)
|
||||
}
|
||||
|
||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||
runInsideUIAsync {
|
||||
downloads << e
|
||||
downloadInfoHashes.add(e.downloader.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +252,8 @@ class MainFrameModel {
|
||||
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")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
@@ -192,13 +268,24 @@ class MainFrameModel {
|
||||
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")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashingEvent(FileHashingEvent e) {
|
||||
runInsideUIAsync {
|
||||
loadedFiles = shared.size()
|
||||
hashingFile = e.hashingFile
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
runInsideUIAsync {
|
||||
hashingFile = null
|
||||
}
|
||||
if (e.error != null)
|
||||
return // TODO do something
|
||||
if (infoHashes.contains(e.sharedFile.infoHash))
|
||||
@@ -206,6 +293,7 @@ class MainFrameModel {
|
||||
infoHashes.add(e.sharedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.sharedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
@@ -217,6 +305,19 @@ class MainFrameModel {
|
||||
infoHashes.add(e.loadedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.loadedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
InfoHash infohash = e.unsharedFile.infoHash
|
||||
if (!infoHashes.remove(infohash))
|
||||
return
|
||||
runInsideUIAsync {
|
||||
shared.remove(e.unsharedFile)
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
@@ -249,28 +350,69 @@ class MainFrameModel {
|
||||
updateTablePreservingSelection("trusted-table")
|
||||
updateTablePreservingSelection("distrusted-table")
|
||||
|
||||
results.values().each {
|
||||
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
|
||||
results.values().each { MVCGroup group ->
|
||||
if (group.alive) {
|
||||
group.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
|
||||
runInsideUIAsync {
|
||||
if (!subscriptions.contains(e.trustList))
|
||||
subscriptions << e.trustList
|
||||
updateTablePreservingSelection("subscription-table")
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.replyTo == core.me.destination)
|
||||
return
|
||||
|
||||
def search
|
||||
if (e.searchEvent.searchHash != null) {
|
||||
if (!uiSettings.showSearchHashes) {
|
||||
return
|
||||
}
|
||||
search = Base64.encode(e.searchEvent.searchHash)
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder()
|
||||
e.searchEvent.searchTerms?.each {
|
||||
sb.append(it)
|
||||
sb.append(" ")
|
||||
}
|
||||
def search = sb.toString()
|
||||
search = sb.toString()
|
||||
if (search.trim().size() == 0)
|
||||
return
|
||||
}
|
||||
runInsideUIAsync {
|
||||
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
||||
JTable table = builder.getVariable("searches-table")
|
||||
|
||||
Boolean searchFound = false
|
||||
Iterator searchIter = searches.iterator()
|
||||
while ( searchIter.hasNext() ) {
|
||||
IncomingSearch searchEle = searchIter.next()
|
||||
if ( searchEle.search == search
|
||||
&& searchEle.originator == e.originator
|
||||
&& searchEle.uuid == e.searchEvent.getUuid() ) {
|
||||
searchIter.remove()
|
||||
table.model.fireTableDataChanged()
|
||||
searchFound = true
|
||||
searchEle.count++
|
||||
searchEle.timestamp = Calendar.getInstance()
|
||||
searches.addFirst(searchEle)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!searchFound) {
|
||||
searches.addFirst(new IncomingSearch(search, e.replyTo, e.originator, e.searchEvent.getUuid()))
|
||||
}
|
||||
|
||||
while(searches.size() > 200)
|
||||
searches.removeLast()
|
||||
JTable table = builder.getVariable("searches-table")
|
||||
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
@@ -279,6 +421,18 @@ class MainFrameModel {
|
||||
String search
|
||||
Destination replyTo
|
||||
Persona originator
|
||||
long count
|
||||
UUID uuid
|
||||
Calendar timestamp
|
||||
|
||||
IncomingSearch( String search, Destination replyTo, Persona originator, UUID uuid ) {
|
||||
this.search = search
|
||||
this.replyTo = replyTo
|
||||
this.originator = originator
|
||||
this.uuid = uuid
|
||||
this.count = 1
|
||||
this.timestamp = Calendar.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||
@@ -293,6 +447,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) {
|
||||
if (!core.muOptions.shareDownloadedFiles)
|
||||
return
|
||||
@@ -303,4 +465,26 @@ class MainFrameModel {
|
||||
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,14 +11,17 @@ import griffon.metadata.ArtifactProviderFor
|
||||
class OptionsModel {
|
||||
@Observable String downloadRetryInterval
|
||||
@Observable String updateCheckInterval
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean autoDownloadUpdate
|
||||
@Observable boolean shareDownloadedFiles
|
||||
@Observable String downloadLocation
|
||||
|
||||
// i2p options
|
||||
@Observable String inboundLength
|
||||
@Observable String inboundQuantity
|
||||
@Observable String outboundLength
|
||||
@Observable String outboundQuantity
|
||||
@Observable String i2pUDPPort
|
||||
@Observable String i2pNTCPPort
|
||||
|
||||
// gui options
|
||||
@Observable boolean showMonitor
|
||||
@@ -27,19 +30,33 @@ class OptionsModel {
|
||||
@Observable boolean clearCancelledDownloads
|
||||
@Observable boolean clearFinishedDownloads
|
||||
@Observable boolean excludeLocalResult
|
||||
@Observable boolean showSearchHashes
|
||||
|
||||
// bw options
|
||||
@Observable String inBw
|
||||
@Observable String outBw
|
||||
|
||||
// trust options
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean trustLists
|
||||
@Observable String trustListInterval
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
downloadRetryInterval = settings.downloadRetryInterval
|
||||
updateCheckInterval = settings.updateCheckInterval
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
autoDownloadUpdate = settings.autoDownloadUpdate
|
||||
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||
downloadLocation = settings.downloadLocation.getAbsolutePath()
|
||||
|
||||
Core core = application.context.get("core")
|
||||
inboundLength = core.i2pOptions["inbound.length"]
|
||||
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||
outboundLength = core.i2pOptions["outbound.length"]
|
||||
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||
i2pUDPPort = core.i2pOptions["i2np.udp.port"]
|
||||
i2pNTCPPort = core.i2pOptions["i2np.ntcp.port"]
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
showMonitor = uiSettings.showMonitor
|
||||
@@ -48,5 +65,15 @@ class OptionsModel {
|
||||
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)
|
||||
}
|
||||
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
trustLists = settings.allowTrustLists
|
||||
trustListInterval = String.valueOf(settings.trustListInterval)
|
||||
}
|
||||
}
|
@@ -23,6 +23,7 @@ class SearchTabModel {
|
||||
String uuid
|
||||
def results = []
|
||||
def hashBucket = [:]
|
||||
def sourcesBucket = [:]
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
@@ -37,7 +38,7 @@ class SearchTabModel {
|
||||
|
||||
void handleResult(UIResultEvent e) {
|
||||
if (uiSettings.excludeLocalResult &&
|
||||
e.sender == core.me)
|
||||
core.fileManager.rootToFiles.containsKey(e.infohash))
|
||||
return
|
||||
runInsideUIAsync {
|
||||
def bucket = hashBucket.get(e.infohash)
|
||||
@@ -47,9 +48,43 @@ class SearchTabModel {
|
||||
}
|
||||
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
|
||||
JTable table = builder.getVariable("results-table")
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
22
gui/griffon-app/models/com/muwire/gui/TrustListModel.groovy
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class TrustListModel {
|
||||
RemoteTrustList trustList
|
||||
TrustService trustService
|
||||
|
||||
def trusted
|
||||
def distrusted
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
trusted = new ArrayList<>(trustList.good)
|
||||
distrusted = new ArrayList<>(trustList.bad)
|
||||
}
|
||||
}
|
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 |