Compare commits
351 Commits
muwire-0.4
...
muwire-0.5
Author | SHA1 | Date | |
---|---|---|---|
94678bad3c | |||
e7072803e9 | |||
e9f7a51e16 | |||
916fad7d9b | |||
9feb891c51 | |||
b865376d24 | |||
8dcba7535c | |||
7e881f1fe6 | |||
a9aad7d9db | |||
e736b42751 | |||
acda64aea7 | |||
d82dc4ce90 | |||
f2ff90795d | |||
49f51a9f5f | |||
6fbd1267fa | |||
149568520f | |||
c672880db0 | |||
6cb1674d14 | |||
dba863a864 | |||
642044b7e2 | |||
47c14f109a | |||
36c1a1a288 | |||
5d51b1c580 | |||
bf3502220f | |||
ff1df88601 | |||
4ed572ba51 | |||
fd3f55ab4d | |||
1358e14467 | |||
e22d5fea11 | |||
7ade4aa10d | |||
a9f623a91a | |||
1ce410e943 | |||
27aad9d75d | |||
24591b10f2 | |||
e4f1ea5c10 | |||
c73c44c5f2 | |||
309cbcc580 | |||
86894f242b | |||
568255140f | |||
f6d2bac5bb | |||
1c396711ed | |||
c154d9538d | |||
8043782446 | |||
00c529cca1 | |||
094b9ac2b0 | |||
0dae0a561b | |||
82eaafc2c3 | |||
a3fc1a62e7 | |||
2fd8f45107 | |||
2429bbf59e | |||
f7e28e04f6 | |||
cc0188f20e | |||
af9b4f4679 | |||
625a559d02 | |||
6e20193d57 | |||
88ac267f99 | |||
9b3a7473d1 | |||
5b0180280e | |||
d0462034fc | |||
f3e4098107 | |||
26e7ca0b21 | |||
11007e5f19 | |||
ae651cb6bd | |||
cad3a88517 | |||
29c81646af | |||
8a0257927b | |||
3b882ae644 | |||
5b61738ca9 | |||
c77d79513e | |||
9f12442897 | |||
477b0a47ad | |||
7f1041dd96 | |||
99393c59bd | |||
a78d8c84ca | |||
fa9c697bfa | |||
e5b12701f5 | |||
f69727ab43 | |||
d7c7afe2c0 | |||
6c806c4441 | |||
c4095abdb4 | |||
8801546854 | |||
f6ee49c0f5 | |||
2320d650f6 | |||
e9e6e6920a | |||
87e5007f39 | |||
8df6715e24 | |||
6d587bf228 | |||
8684452848 | |||
7d652fabcb | |||
5eb8d75bba | |||
9ca8d1738c | |||
2bb9480137 | |||
7a6365f87a | |||
56540ca3ca | |||
eb5a5198b1 | |||
29562c42ea | |||
f5284f9483 | |||
9bd3c4f141 | |||
817dd68faf | |||
5954cdb342 | |||
56d44e6458 | |||
c6fb76610d | |||
5e329dfa2c | |||
742f6da870 | |||
7f46347c0f | |||
b308ac2f37 | |||
9cdabb51d1 | |||
45f0736a5e | |||
fe753ff978 | |||
ac717b5205 | |||
6f624e3afc | |||
623d675ed9 | |||
546b71b632 | |||
804113bb1b | |||
ab9e10f438 | |||
00520acdf0 | |||
8c44d196a7 | |||
9c5fa0a2ce | |||
d7bca05725 | |||
45fcb2209e | |||
7bf0373b80 | |||
5925b42597 | |||
13243b05ad | |||
43987be463 | |||
fcd3414e02 | |||
70913ea8fb | |||
b30e552498 | |||
bae66de4eb | |||
626e145e25 | |||
bf72c76f13 | |||
fce8bbfd97 | |||
1cc7925155 | |||
12b51ceb02 | |||
62811861a4 | |||
837aa6974b | |||
94e7c42d19 | |||
877bf12a93 | |||
224266b2dd | |||
8f16614dc3 | |||
b412f9fb0c | |||
b24d04811d | |||
771f645df0 | |||
b6483ad0f4 | |||
decb72c8ef | |||
439b3bf18b | |||
06679ffee0 | |||
1d5b12e2d7 | |||
4e6e1b6f5b | |||
f0b5361d7b | |||
e0c6bfbf51 | |||
2a0ecd8a47 | |||
fb1804e849 | |||
d4eaa0df8d | |||
ffde6ac86f | |||
7ad677ead2 | |||
ddb0568aab | |||
ff50a84a48 | |||
770396ba41 | |||
b55852e993 | |||
a6945275a4 | |||
7241809e55 | |||
54073af933 | |||
a32903fc8c | |||
e40520be46 | |||
97482b949a | |||
92ee107312 | |||
2e8082af64 | |||
8da5a428c9 | |||
fd46b3c7d6 | |||
eea3b2563b | |||
50719f3828 | |||
01a45a89a8 | |||
66bd249ed3 | |||
265cd6ee15 | |||
1dc88cb96b | |||
3e10d497b1 | |||
9a0b3bb9d6 | |||
a1fe3c01b9 | |||
ab323db62a | |||
d954387e41 | |||
ea9db21a18 | |||
136cf89c9b | |||
46de1baf88 | |||
13f7b8563c | |||
9c15208f3a | |||
a9ce9d96b3 | |||
4d2a5a8018 | |||
8395047386 | |||
cb23aa44f0 | |||
dbcb8508b8 | |||
47d406d93b | |||
e06f1805c2 | |||
2b04374e23 | |||
383addbc37 | |||
cc39cd7f8e | |||
83665d7524 | |||
94340480b4 | |||
8850d49c63 | |||
f0f9d840f0 | |||
7f4cd4f331 | |||
e6162503f6 | |||
7a5d71dc36 | |||
6fa39a5e35 | |||
c5ae804f61 | |||
d7695b448d | |||
946d9c8f32 | |||
02441ca1e3 | |||
5fa21b2360 | |||
d4c08f4fe6 | |||
942de287c6 | |||
d0299f80c6 | |||
1227cf9263 | |||
a05575485f | |||
f5bccd8126 | |||
70fb789abf | |||
feb712c253 | |||
d22b403e2a | |||
a24982e0df | |||
6c26019164 | |||
965fa79bbf | |||
60ddb85461 | |||
c7284623bc | |||
3e7f2aa70a | |||
4f436a636c | |||
b49dbc30c3 | |||
c25d314e1c | |||
b28587a275 | |||
8b8e5d59be | |||
70bbe1f636 | |||
337605dc0f | |||
14bdfa6b2e | |||
ed3f9da773 | |||
251080d08f | |||
f530ab999d | |||
4133384e48 | |||
600fc98868 | |||
129eeb3b88 | |||
20b51b78a0 | |||
33fe755b60 | |||
8b0668a134 | |||
730d2202fd | |||
69906a986d | |||
5bc8fa8633 | |||
7de7c9d8f3 | |||
e943f6019d | |||
2eec7bec5b | |||
c36110cf76 | |||
abe28517bc | |||
15bc4c064d | |||
91d771944b | |||
e09c456a13 | |||
d9c1067226 | |||
eda3e7ad3a | |||
e9798c7eaa | |||
66bb4eef5b | |||
55f260b3f4 | |||
32d4c3965e | |||
de1534d837 | |||
7b58e8a88a | |||
8a03b89985 | |||
1d97374857 | |||
549e8c2d98 | |||
b54d24db0d | |||
fa12e84345 | |||
6430ff2691 | |||
591313c81c | |||
ce7b6a0c65 | |||
5c4d4c4580 | |||
4cb864ff9f | |||
417675ad07 | |||
9513e5ba3c | |||
85610cf169 | |||
e8322384b8 | |||
179279ed30 | |||
ae79f0fded | |||
ed878b3762 | |||
623cca0ef2 | |||
eaa883c3ba | |||
7ae8076865 | |||
b1aa92661c | |||
9ed94c8376 | |||
fa6aea1abe | |||
0de84e704b | |||
a767dda044 | |||
56e9235d7b | |||
2fba9a74ce | |||
2bb6826906 | |||
9f339629a9 | |||
58d4207f94 | |||
32577a28dc | |||
f7b43304d4 | |||
dcbe09886d | |||
5a54b2dcda | |||
581293b24f | |||
cd072b9f76 | |||
6b74fc5956 | |||
3de2f872bb | |||
fcde917d08 | |||
4ded065010 | |||
18a1c7091a | |||
46aee19f80 | |||
92dd7064c6 | |||
b2e4dda677 | |||
e77a2c8961 | |||
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 |
32
README.md
32
README.md
@ -4,14 +4,14 @@ 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.4.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.5.5 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
|
||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
```
|
||||
./gradlew clean assemble
|
||||
./gradlew clean assemble
|
||||
```
|
||||
|
||||
If you want to run the unit tests, type
|
||||
@ -19,13 +19,29 @@ If you want to run the unit tests, type
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Some of the UI tests will fail because they haven't been written yet :-/
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
|
||||
|
||||
### Running
|
||||
### Running the GUI
|
||||
|
||||
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.
|
||||
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 gui-x.y.z.jar` in a terminal or command prompt.
|
||||
|
||||
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 `$HOME/.MuWire/i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there.
|
||||
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.
|
||||
[Default I2CP port]\: `7654`
|
||||
|
||||
### Running the CLI
|
||||
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
|
||||
The CLI is under active development and doesn't have all the features of the GUI.
|
||||
|
||||
### GPG Fingerprint
|
||||
|
||||
```
|
||||
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||
```
|
||||
|
||||
You can find the full key at https://keybase.io/zlatinb
|
||||
|
||||
|
||||
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
||||
|
7
TODO.md
7
TODO.md
@ -12,10 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
||||
|
||||
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
|
||||
@ -27,5 +23,4 @@ To enable parsing of metadata from known file types and the user editing it or a
|
||||
### Small Items
|
||||
|
||||
* Wrapper of some kind for in-place upgrades
|
||||
* Download file sequentially
|
||||
* Multiple-selection download, Ctrl-A
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
|
@ -2,7 +2,7 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'net.i2p:i2p:0.9.40'
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
27
cli-lanterna/build.gradle
Normal file
27
cli-lanterna/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin : 'application'
|
||||
application {
|
||||
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
|
||||
applicationName = 'MuWire-cli'
|
||||
}
|
||||
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
compile 'com.googlecode.lanterna:lanterna:3.0.1'
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class AddCommentView extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
AddCommentView(TextGUI textGUI, Core core, SharedFile sharedFile, TerminalSize terminalSize) {
|
||||
super("Add Comment To "+sharedFile.getFile().getName())
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
String oldComment = sharedFile.getComment()
|
||||
if (oldComment == null)
|
||||
oldComment = ""
|
||||
else
|
||||
oldComment = DataUtil.readi18nString(Base64.decode(oldComment))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize,oldComment,TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
Button saveButton = new Button("Save", {
|
||||
String newComment = textBox.getText()
|
||||
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
|
||||
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
|
||||
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
|
||||
} else {
|
||||
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
|
||||
String encodedOldComment = sharedFile.getComment()
|
||||
sharedFile.setComment(newComment)
|
||||
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
|
||||
close()
|
||||
}
|
||||
})
|
||||
Button cancelButton = new Button("Cancel", {close()})
|
||||
|
||||
buttonsPanel.addComponent(saveButton, layoutData)
|
||||
buttonsPanel.addComponent(cancelButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
import com.muwire.core.search.BrowseStatusEvent
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class BrowseModel {
|
||||
private final Persona persona
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
private final TableModel model = new TableModel("Name","Size","Hash","Comment")
|
||||
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
|
||||
|
||||
private int totalResults
|
||||
|
||||
private Label status
|
||||
private Label percentage
|
||||
|
||||
BrowseModel(Persona persona, Core core, TextGUIThread guiThread) {
|
||||
this.persona = persona
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
|
||||
core.eventBus.register(BrowseStatusEvent.class, this)
|
||||
core.eventBus.register(UIResultEvent.class, this)
|
||||
core.eventBus.publish(new UIBrowseEvent(host : persona))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
core.eventBus.unregister(UIResultEvent.class, this)
|
||||
}
|
||||
|
||||
void onBrowseStatusEvent(BrowseStatusEvent e) {
|
||||
guiThread.invokeLater {
|
||||
status.setText(e.status.toString())
|
||||
if (e.status == BrowseStatus.FETCHING)
|
||||
totalResults = e.totalResults
|
||||
}
|
||||
}
|
||||
|
||||
void onUIResultEvent(UIResultEvent e) {
|
||||
guiThread.invokeLater {
|
||||
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
|
||||
String infoHash = Base64.encode(e.infohash.getRoot())
|
||||
String comment = String.valueOf(e.comment != null)
|
||||
model.addRow(e.name, size, infoHash, comment)
|
||||
rootToResult.put(infoHash, e)
|
||||
|
||||
String percentageString = ""
|
||||
if (totalResults != 0) {
|
||||
double percentage = Math.round( (model.getRowCount() * 100 / totalResults).toDouble() )
|
||||
percentageString = String.valueOf(percentage)+"%"
|
||||
}
|
||||
percentage.setText(percentageString)
|
||||
}
|
||||
}
|
||||
|
||||
void setStatusLabel(Label status) {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
void setPercentageLabel(Label percentage) {
|
||||
this.percentage = percentage
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
|
||||
class BrowseView extends BasicWindow {
|
||||
private final BrowseModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
BrowseView(BrowseModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Browse "+model.persona.getHumanReadableName())
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label statusLabel = new Label("")
|
||||
Label percentageLabel = new Label("")
|
||||
model.setStatusLabel(statusLabel)
|
||||
model.setPercentageLabel(percentageLabel)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
topPanel.addComponent(statusLabel, layoutData)
|
||||
topPanel.addComponent(percentageLabel, layoutData)
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
|
||||
table = new Table("Name","Size","Hash","Comment")
|
||||
table.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.model)
|
||||
setVisibleRows(terminalSize.getRows())
|
||||
setSelectAction({rowSelected()})
|
||||
}
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
setComponent(contentPanel)
|
||||
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
String infoHash = row[2]
|
||||
boolean comment = Boolean.parseBoolean(row[3])
|
||||
if (comment) {
|
||||
Window prompt = new BasicWindow("Download Or View Comment")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
Button downloadButton = new Button("Download", {download(infoHash)})
|
||||
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(downloadButton, layoutData)
|
||||
addComponent(viewButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
downloadButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
} else {
|
||||
download(infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
private void download(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
def file = new File(core.muOptions.downloadLocation, result.name)
|
||||
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
|
||||
target : file, sequential : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download started", "Started download of "+result.name, MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void viewComment(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
ViewCommentView view = new ViewCommentView(result, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Border
|
||||
import com.googlecode.lanterna.gui2.BorderLayout
|
||||
import com.googlecode.lanterna.gui2.Borders
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.MultiWindowTextGUI
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.SeparateTextGUIThread
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.WindowBasedTextGUI
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.WaitingDialog
|
||||
import com.googlecode.lanterna.screen.Screen
|
||||
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
|
||||
import com.googlecode.lanterna.terminal.Terminal
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.5.8"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
private static WindowBasedTextGUI textGUI
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
|
||||
}
|
||||
}
|
||||
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
if (!home.exists())
|
||||
home.mkdirs()
|
||||
|
||||
def propsFile = new File(home,"MuWire.properties")
|
||||
|
||||
|
||||
DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory()
|
||||
Screen screen = terminalFactory.createScreen()
|
||||
textGUI = new MultiWindowTextGUI( new SeparateTextGUIThread.Factory(), screen)
|
||||
textGUI.getGUIThread().start()
|
||||
screen.startScreen()
|
||||
|
||||
def props
|
||||
if (!propsFile.exists()) {
|
||||
String nickname = TextInputDialog.showDialog(textGUI, "Select a nickname", "", "")
|
||||
String defaultDownloadLocation = System.getProperty("user.home")+File.separator+"Downloads"
|
||||
String downloadLocation = TextInputDialog.showDialog(textGUI, "Select download location", "", defaultDownloadLocation)
|
||||
String defaultIncompletesLocation = System.getProperty("user.home")+File.separator+".MuWire"+File.separator+"incompletes"
|
||||
String incompletesLocation = TextInputDialog.showDialog(textGUI, "Select incompletes location", "", defaultIncompletesLocation)
|
||||
|
||||
|
||||
File downloadLocationFile = new File(downloadLocation)
|
||||
if (!downloadLocationFile.exists())
|
||||
downloadLocationFile.mkdirs()
|
||||
File incompletesLocationFile = new File(incompletesLocation)
|
||||
if (!incompletesLocationFile.exists())
|
||||
incompletesLocationFile.mkdirs()
|
||||
|
||||
props = new MuWireSettings()
|
||||
props.setNickname(nickname)
|
||||
props.setDownloadLocation(downloadLocationFile)
|
||||
props.incompleteLocation = incompletesLocationFile
|
||||
|
||||
propsFile.withPrintWriter("UTF-8", {
|
||||
props.write(it)
|
||||
})
|
||||
} else {
|
||||
props = new Properties()
|
||||
propsFile.withReader("UTF-8", {
|
||||
props.load(it)
|
||||
})
|
||||
props = new MuWireSettings(props)
|
||||
}
|
||||
props.updateType = "cli-lanterna"
|
||||
|
||||
def i2pPropsFile = new File(home, "i2p.properties")
|
||||
if (!i2pPropsFile.exists()) {
|
||||
String i2pHost = TextInputDialog.showDialog(textGUI, "I2P router host", "Specifiy the host I2P router is on", "127.0.0.1")
|
||||
int i2pPort = TextInputDialog.showNumberDialog(textGUI, "I2CP port", "Specify the I2CP port", "7654").toInteger()
|
||||
|
||||
Properties i2pProps = new Properties()
|
||||
i2pProps["i2cp.tcp.host"] = i2pHost
|
||||
i2pProps["i2cp.tcp.port"] = String.valueOf(i2pPort)
|
||||
i2pPropsFile.withOutputStream { i2pProps.store(it, "") }
|
||||
}
|
||||
|
||||
def cliProps
|
||||
def cliPropsFile = new File(home, "cli.properties")
|
||||
if (cliPropsFile.exists()) {
|
||||
Properties p = new Properties()
|
||||
cliPropsFile.withInputStream {
|
||||
p.load(it)
|
||||
}
|
||||
cliProps = new CliSettings(p)
|
||||
} else
|
||||
cliProps = new CliSettings(new Properties())
|
||||
|
||||
|
||||
Window window = new BasicWindow("MuWire "+ MW_VERSION)
|
||||
window.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.withBorder(Borders.doubleLine())
|
||||
BorderLayout layout = new BorderLayout()
|
||||
contentPanel.setLayoutManager(layout)
|
||||
|
||||
Panel welcomeNamePanel = new Panel()
|
||||
contentPanel.addComponent(welcomeNamePanel, BorderLayout.Location.CENTER)
|
||||
welcomeNamePanel.setLayoutManager(new GridLayout(1))
|
||||
Label welcomeLabel = new Label("Welcome to MuWire "+ props.nickname)
|
||||
welcomeNamePanel.addComponent(welcomeLabel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
|
||||
Panel connectButtonPanel = new Panel()
|
||||
contentPanel.addComponent(connectButtonPanel, BorderLayout.Location.BOTTOM)
|
||||
connectButtonPanel.setLayoutManager(new GridLayout(1))
|
||||
Button connectButton = new Button("Connect", {
|
||||
|
||||
WaitingDialog waiting = new WaitingDialog("Connecting", "Please wait")
|
||||
waiting.showDialog(textGUI, false)
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1)
|
||||
Thread connector = new Thread({
|
||||
try {
|
||||
core = new Core(props, home, MW_VERSION)
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
})
|
||||
connector.start()
|
||||
while(latch.getCount() > 0) {
|
||||
textGUI.updateScreen()
|
||||
Thread.sleep(10)
|
||||
}
|
||||
waiting.close()
|
||||
window.close()
|
||||
} as Runnable)
|
||||
welcomeNamePanel.addComponent(connectButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
|
||||
window.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(window)
|
||||
|
||||
if (core == null) {
|
||||
MessageDialog.showMessageDialog(textGUI, "Failed", "MuWire failed to load", MessageDialogButton.Close)
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen, cliProps)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
textGUI.addWindowAndWait(window)
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1)
|
||||
Thread stopper = new Thread({
|
||||
core.shutdown()
|
||||
latch.countDown()
|
||||
} as Runnable)
|
||||
WaitingDialog waitingForShutdown = new WaitingDialog("MuWire is shutting down","Please wait")
|
||||
waitingForShutdown.setHints([Window.Hint.CENTERED])
|
||||
waitingForShutdown.showDialog(textGUI, false)
|
||||
stopper.start()
|
||||
while(latch.getCount() > 0) {
|
||||
textGUI.updateScreen()
|
||||
Thread.sleep(10)
|
||||
}
|
||||
waitingForShutdown.close()
|
||||
|
||||
screen.stopScreen()
|
||||
System.exit(0)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
class CliSettings {
|
||||
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean clearUploads
|
||||
|
||||
CliSettings(Properties props) {
|
||||
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
|
||||
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads", "false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads", "false"))
|
||||
}
|
||||
|
||||
void write(OutputStream os) {
|
||||
Properties props = new Properties()
|
||||
props.with {
|
||||
setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||
setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||
setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
|
||||
store(os, "CLI Properties")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
|
||||
class DownloadDetailsView extends BasicWindow {
|
||||
private final Downloader downloader
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
private Label knownSources, activeSources, donePieces
|
||||
DownloadDetailsView(Downloader downloader) {
|
||||
super("Download details for "+downloader.file.getName())
|
||||
this.downloader = downloader
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
knownSources = new Label("0")
|
||||
activeSources = new Label("0")
|
||||
donePieces = new Label("0")
|
||||
refresh()
|
||||
|
||||
Button refreshButton = new Button("Refresh",{refresh()})
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(new Label("Target Location"), layoutData)
|
||||
addComponent(new Label(downloader.file.getAbsolutePath()), layoutData)
|
||||
addComponent(new Label("Piece Size"), layoutData)
|
||||
addComponent(new Label(String.valueOf(downloader.pieceSize)), layoutData)
|
||||
addComponent(new Label("Total Pieces"), layoutData)
|
||||
addComponent(new Label(String.valueOf(downloader.nPieces)), layoutData)
|
||||
addComponent(new Label("Done Pieces"), layoutData)
|
||||
addComponent(donePieces, layoutData)
|
||||
addComponent(new Label("Known Sources"), layoutData)
|
||||
addComponent(knownSources, layoutData)
|
||||
addComponent(new Label("Active Sources"), layoutData)
|
||||
addComponent(activeSources, layoutData)
|
||||
addComponent(refreshButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
|
||||
setComponent(contentPanel)
|
||||
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
int done = downloader.donePieces()
|
||||
int known = downloader.activeWorkers.size()
|
||||
int active = downloader.activeWorkers()
|
||||
|
||||
knownSources.setText(String.valueOf(known))
|
||||
activeSources.setText(String.valueOf(active))
|
||||
donePieces.setText(String.valueOf(done))
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
class DownloaderWrapper {
|
||||
final Downloader downloader
|
||||
DownloaderWrapper(Downloader downloader) {
|
||||
this.downloader = downloader
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
downloader.file.getName()
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class DownloadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final CliSettings props
|
||||
private final List<Downloader> downloaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
|
||||
|
||||
|
||||
private long lastRetryTime
|
||||
|
||||
DownloadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||
Timer timer = new Timer(true)
|
||||
Runnable guiRunnable = {
|
||||
refreshModel()
|
||||
resumeDownloads()
|
||||
}
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
guiThread.invokeLater(guiRunnable)
|
||||
} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||
guiThread.invokeLater({
|
||||
downloaders.add(e.downloader)
|
||||
refreshModel()
|
||||
})
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
if (props.clearCancelledDownloads) {
|
||||
downloaders.removeAll { it.cancelled }
|
||||
}
|
||||
if (props.clearFinishedDownloads) {
|
||||
downloaders.removeAll { it.getCurrentState() == Downloader.DownloadState.FINISHED }
|
||||
}
|
||||
|
||||
downloaders.each {
|
||||
String status = it.getCurrentState().toString()
|
||||
int speedInt = it.speed()
|
||||
String speed = DataHelper.formatSize2Decimal(speedInt, false) + "B/sec"
|
||||
|
||||
int pieces = it.nPieces
|
||||
int done = it.donePieces()
|
||||
int percent = -1
|
||||
if (pieces != 0)
|
||||
percent = (done * 100 / pieces)
|
||||
String totalSize = DataHelper.formatSize2Decimal(it.length, false) + "B"
|
||||
String progress = (String.format("%2d", percent) + "% of ${totalSize}".toString())
|
||||
|
||||
String ETA
|
||||
if (speedInt == 0)
|
||||
ETA = "Unknown"
|
||||
else {
|
||||
long remaining = (pieces - done) * it.pieceSize / speedInt
|
||||
ETA = DataHelper.formatDuration(remaining * 1000)
|
||||
}
|
||||
|
||||
model.addRow([new DownloaderWrapper(it), status, progress, speed, ETA])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void resumeDownloads() {
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval == 0)
|
||||
return
|
||||
retryInterval *= 1000
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastRetryTime > retryInterval) {
|
||||
lastRetryTime = now
|
||||
downloaders.each {
|
||||
def state = it.getCurrentState()
|
||||
if (state == Downloader.DownloadState.FAILED || state == Downloader.DownloadState.DOWNLOADING)
|
||||
it.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
|
||||
class DownloadsView extends BasicWindow {
|
||||
private final Core core
|
||||
private final DownloadsModel model
|
||||
private final TextGUI textGUI
|
||||
private final Table table
|
||||
|
||||
DownloadsView(Core core, DownloadsModel model, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
table = new Table("Name","Status","Progress","Speed","ETA")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearButton = new Button("Clear Done",{clearDone()})
|
||||
buttonsPanel.addComponent(clearButton, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
Downloader downloader = row[0].downloader
|
||||
|
||||
Window prompt = new BasicWindow("Kill Download?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button killDownload = new Button("Kill Download", {
|
||||
downloader.cancel()
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download Killed", downloader.file.getName()+ " has been killed", MessageDialogButton.OK)
|
||||
})
|
||||
Button viewDetails = new Button("View Details", {
|
||||
textGUI.addWindowAndWait(new DownloadDetailsView(downloader))
|
||||
})
|
||||
Button close = new Button("Close", {
|
||||
prompt.close()
|
||||
})
|
||||
|
||||
contentPanel.addComponent(killDownload,layoutData)
|
||||
contentPanel.addComponent(viewDetails, layoutData)
|
||||
contentPanel.addComponent(close, layoutData)
|
||||
prompt.setComponent(contentPanel)
|
||||
close.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void clearDone() {
|
||||
model.downloaders.removeAll {
|
||||
def state = it.getCurrentState()
|
||||
state == Downloader.DownloadState.CANCELLED || state == Downloader.DownloadState.FINISHED
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class FilesModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final List<SharedFile> sharedFiles = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
|
||||
FilesModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
Timer timer = new Timer(true)
|
||||
timer.schedule({
|
||||
guiThread.invokeLater(refreshModel)
|
||||
} as TimerTask, 1000,1000)
|
||||
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.add(e.loadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
if (e.sharedFile != null)
|
||||
sharedFiles.add(e.sharedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.remove(e.unsharedFile)
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
String hits = String.valueOf(it.getHits())
|
||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.FileDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
|
||||
class FilesView extends BasicWindow {
|
||||
private final FilesModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
FilesView(FilesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Shared Files")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(4))
|
||||
|
||||
Button shareFile = new Button("Share File", {shareFile()})
|
||||
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
|
||||
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
|
||||
Button close = new Button("Close", {close()})
|
||||
|
||||
buttonsPanel.with {
|
||||
addComponent(shareFile, layoutData)
|
||||
addComponent(shareDirectory, layoutData)
|
||||
addComponent(unshareDirectory, layoutData)
|
||||
addComponent(close, layoutData)
|
||||
}
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
close.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
SharedFile sf = row[0].sharedFile
|
||||
|
||||
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
|
||||
Button unshareButton = new Button("Unshare", {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
||||
} )
|
||||
Button addCommentButton = new Button("Add Comment", {
|
||||
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
})
|
||||
Button certifyButton = new Button("Certify", {
|
||||
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : sf))
|
||||
MessageDialog.showMessageDialog(textGUI, "Certificate Created", "Certificate has been issued", MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(unshareButton, layoutData)
|
||||
contentPanel.addComponent(addCommentButton, layoutData)
|
||||
contentPanel.addComponent(certifyButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
|
||||
private void shareFile() {
|
||||
TerminalSize terminalSize = new TerminalSize(terminalSize.getColumns() - 10, terminalSize.getRows() - 10)
|
||||
FileDialog fileDialog = new FileDialog("Share File", "Select a file to share", "Share", terminalSize, false, null)
|
||||
File f = fileDialog.showDialog(textGUI)
|
||||
f = f.getCanonicalFile()
|
||||
core.eventBus.publish(new FileSharedEvent(file : f))
|
||||
MessageDialog.showMessageDialog(textGUI, "File Shared", f.getName()+" has been shared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void shareDirectory() {
|
||||
String directoryName = TextInputDialog.showDialog(textGUI, "Share a directory", "Enter the directory to share", "")
|
||||
if (directoryName == null)
|
||||
return
|
||||
File directory = new File(directoryName)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new FileSharedEvent(file : directory))
|
||||
MessageDialog.showMessageDialog(textGUI, "Directory Shared", directory.getName()+" has been shared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void unshareDirectory() {
|
||||
String directoryName = TextInputDialog.showDialog(textGUI, "Unshare a directory", "Enter the directory to unshare", "")
|
||||
if (directoryName == null)
|
||||
return
|
||||
File directory = new File(directoryName)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
|
||||
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
|
||||
}
|
||||
}
|
@ -0,0 +1,311 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalPosition
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.BorderLayout
|
||||
import com.googlecode.lanterna.gui2.Borders
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.Panels
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.screen.Screen
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.update.UpdateAvailableEvent
|
||||
import com.muwire.core.update.UpdateDownloadedEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class MainWindowView extends BasicWindow {
|
||||
|
||||
private final Core core
|
||||
private final TextGUI textGUI
|
||||
private final Screen screen
|
||||
|
||||
private final TextBox searchTextBox
|
||||
|
||||
private final DownloadsModel downloadsModel
|
||||
private final UploadsModel uploadsModel
|
||||
private final FilesModel filesModel
|
||||
private final TrustModel trustModel
|
||||
|
||||
private final Label connectionCount, incoming, outgoing
|
||||
private final Label known, failing, hopeless
|
||||
private final Label sharedFiles
|
||||
private final Label timesBrowsed
|
||||
private final Label updateStatus
|
||||
private final Label usedRam, totalRam, maxRam
|
||||
|
||||
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
|
||||
super(title);
|
||||
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.screen = screen
|
||||
|
||||
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core, props)
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
||||
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
||||
trustModel = new TrustModel(textGUI.getGUIThread(), core)
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
Panel contentPanel = new Panel()
|
||||
setComponent(contentPanel)
|
||||
|
||||
BorderLayout borderLayout = new BorderLayout()
|
||||
contentPanel.setLayoutManager(borderLayout)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
|
||||
|
||||
GridLayout gridLayout = new GridLayout(7)
|
||||
buttonsPanel.setLayoutManager(gridLayout)
|
||||
|
||||
searchTextBox = new TextBox(new TerminalSize(40, 1))
|
||||
Button searchButton = new Button("Search", { search() })
|
||||
Button downloadsButton = new Button("Downloads", {download()})
|
||||
Button uploadsButton = new Button("Uploads", {upload()})
|
||||
Button filesButton = new Button("Files", { files() })
|
||||
Button trustButton = new Button("Trust", {trust()})
|
||||
Button quitButton = new Button("Quit", {close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
buttonsPanel.with {
|
||||
addComponent(searchTextBox, layoutData)
|
||||
addComponent(searchButton, layoutData)
|
||||
addComponent(downloadsButton, layoutData)
|
||||
addComponent(uploadsButton, layoutData)
|
||||
addComponent(filesButton, layoutData)
|
||||
addComponent(trustButton, layoutData)
|
||||
addComponent(quitButton, layoutData)
|
||||
}
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
contentPanel.addComponent(bottomPanel, BorderLayout.Location.BOTTOM)
|
||||
BorderLayout bottomLayout = new BorderLayout()
|
||||
bottomPanel.setLayoutManager(bottomLayout)
|
||||
|
||||
Label persona = new Label(core.me.getHumanReadableName())
|
||||
bottomPanel.addComponent(persona, BorderLayout.Location.LEFT)
|
||||
|
||||
|
||||
Panel connectionsPanel = new Panel()
|
||||
connectionsPanel.setLayoutManager(new GridLayout(2))
|
||||
Label connections = new Label("Connections:")
|
||||
connectionsPanel.addComponent(connections, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
connectionCount = new Label("0")
|
||||
connectionsPanel.addComponent(connectionCount, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
bottomPanel.addComponent(connectionsPanel, BorderLayout.Location.RIGHT)
|
||||
|
||||
|
||||
Panel centralPanel = new Panel()
|
||||
centralPanel.setLayoutManager(new GridLayout(1))
|
||||
contentPanel.addComponent(centralPanel, BorderLayout.Location.CENTER)
|
||||
Panel statusPanel = new Panel()
|
||||
statusPanel.setLayoutManager(new GridLayout(2))
|
||||
statusPanel.withBorder(Borders.doubleLine("Stats"))
|
||||
centralPanel.addComponent(statusPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, true))
|
||||
|
||||
incoming = new Label("0")
|
||||
outgoing = new Label("0")
|
||||
known = new Label("0")
|
||||
failing = new Label("0")
|
||||
hopeless = new Label("0")
|
||||
sharedFiles = new Label("0")
|
||||
timesBrowsed = new Label("0")
|
||||
updateStatus = new Label("Unknown")
|
||||
usedRam = new Label("0")
|
||||
maxRam = new Label("0")
|
||||
totalRam = new Label("0")
|
||||
|
||||
statusPanel.with {
|
||||
addComponent(new Label("Incoming Connections: "), layoutData)
|
||||
addComponent(incoming, layoutData)
|
||||
addComponent(new Label("Outgoing Connections: "), layoutData)
|
||||
addComponent(outgoing, layoutData)
|
||||
addComponent(new Label("Known Hosts: "), layoutData)
|
||||
addComponent(known, layoutData)
|
||||
addComponent(new Label("Failing Hosts: "), layoutData)
|
||||
addComponent(failing, layoutData)
|
||||
addComponent(new Label("Hopeless Hosts: "), layoutData)
|
||||
addComponent(hopeless, layoutData)
|
||||
addComponent(new Label("Shared Files: "), layoutData)
|
||||
addComponent(sharedFiles, layoutData)
|
||||
addComponent(new Label("Times Browsed: "), layoutData)
|
||||
addComponent(timesBrowsed, layoutData)
|
||||
addComponent(new Label("Update Status: "), layoutData)
|
||||
addComponent(updateStatus, layoutData)
|
||||
addComponent(new Label("Java Version: "), layoutData)
|
||||
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
|
||||
addComponent(new Label("Used Memory: "), layoutData)
|
||||
addComponent(usedRam, layoutData)
|
||||
addComponent(new Label("Total Memory: "), layoutData)
|
||||
addComponent(totalRam, layoutData)
|
||||
addComponent(new Label("Maximum Memory: "), layoutData)
|
||||
addComponent(maxRam, layoutData)
|
||||
}
|
||||
|
||||
refreshStats()
|
||||
|
||||
searchButton.takeFocus()
|
||||
core.eventBus.register(ConnectionEvent.class, this)
|
||||
core.eventBus.register(HostDiscoveredEvent.class, this)
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileDownloadedEvent.class, this)
|
||||
core.eventBus.register(UpdateAvailableEvent.class, this)
|
||||
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||
}
|
||||
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
connectionCount.setText(String.valueOf(core.connectionManager.connections.size()))
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version is available with hash $e.infoHash"
|
||||
updateStatus.setText(label)
|
||||
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version downloaded"
|
||||
updateStatus.setText(label)
|
||||
String message = "MuWire version $e.version has been downloaded. Show details?."
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalSize sizeForTables() {
|
||||
TerminalSize full = screen.getTerminalSize()
|
||||
return new TerminalSize(full.getColumns(), full.getRows() - 10)
|
||||
}
|
||||
|
||||
private void search() {
|
||||
String query = searchTextBox.getText()
|
||||
query = query.trim()
|
||||
if (query.length() == 0)
|
||||
return
|
||||
if (query.length() > 128)
|
||||
query = query.substring(0, 128)
|
||||
|
||||
SearchModel model = new SearchModel(query, core, textGUI.getGUIThread())
|
||||
textGUI.addWindowAndWait(new SearchView(model,core, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
|
||||
private void download() {
|
||||
textGUI.addWindowAndWait(new DownloadsView(core, downloadsModel, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
private void upload() {
|
||||
textGUI.addWindowAndWait(new UploadsView(uploadsModel, sizeForTables()))
|
||||
}
|
||||
|
||||
private void files() {
|
||||
textGUI.addWindowAndWait(new FilesView(filesModel, textGUI, core, sizeForTables()))
|
||||
}
|
||||
|
||||
private void trust() {
|
||||
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
|
||||
}
|
||||
|
||||
private void refreshStats() {
|
||||
int inCon = 0
|
||||
int outCon = 0
|
||||
core.connectionManager.getConnections().each {
|
||||
if (it.isIncoming())
|
||||
inCon++
|
||||
else
|
||||
outCon++
|
||||
}
|
||||
int knownHosts = core.hostCache.hosts.size()
|
||||
int failingHosts = core.hostCache.countFailingHosts()
|
||||
int hopelessHosts = core.hostCache.countHopelessHosts()
|
||||
int shared = core.fileManager.fileToSharedFile.size()
|
||||
int browsed = core.connectionAcceptor.browsed
|
||||
long freeMemL = Runtime.getRuntime().freeMemory()
|
||||
long totalMemL = Runtime.getRuntime().totalMemory()
|
||||
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
|
||||
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
|
||||
String maxMem
|
||||
long maxMemL = Runtime.getRuntime().maxMemory()
|
||||
if (maxMemL >= Long.MAX_VALUE / 2)
|
||||
maxMem = "Unlimited"
|
||||
else
|
||||
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
|
||||
|
||||
|
||||
incoming.setText(String.valueOf(inCon))
|
||||
outgoing.setText(String.valueOf(outCon))
|
||||
known.setText(String.valueOf(knownHosts))
|
||||
failing.setText(String.valueOf(failingHosts))
|
||||
hopeless.setText(String.valueOf(hopelessHosts))
|
||||
sharedFiles.setText(String.valueOf(shared))
|
||||
timesBrowsed.setText(String.valueOf(browsed))
|
||||
usedRam.setText(usedMem)
|
||||
totalRam.setText(totalMem)
|
||||
maxRam.setText(maxMem)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class PersonaWrapper {
|
||||
private final Persona persona
|
||||
PersonaWrapper(Persona persona) {
|
||||
this.persona = persona
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
persona.getHumanReadableName()
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
class ResultsModel {
|
||||
private final UIResultBatchEvent results
|
||||
final TableModel model
|
||||
final Map<String, UIResultEvent> rootToResult = new HashMap<>()
|
||||
|
||||
ResultsModel(UIResultBatchEvent results) {
|
||||
this.results = results
|
||||
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
results.results.each {
|
||||
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
|
||||
String infoHash = Base64.encode(it.infohash.getRoot())
|
||||
String sources = String.valueOf(it.sources.size())
|
||||
String comment = String.valueOf(it.comment != null)
|
||||
model.addRow(it.name, size, infoHash, sources, comment, it.certificates)
|
||||
rootToResult.put(infoHash, it)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ResultsView extends BasicWindow {
|
||||
|
||||
private final ResultsModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
ResultsView(ResultsModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super(model.results.results[0].sender.getHumanReadableName() + " Results")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
table = new Table("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def rows = model.model.getRow(selectedRow)
|
||||
boolean comment = Boolean.parseBoolean(rows[4])
|
||||
boolean certificates = rows[5] > 0
|
||||
if (comment || certificates) {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Window prompt = new BasicWindow("Download Or View Comment/Certificates")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
Button downloadButton = new Button("Download", {download(rows[2])})
|
||||
contentPanel.addComponent(downloadButton, layoutData)
|
||||
|
||||
|
||||
if (comment) {
|
||||
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
|
||||
contentPanel.addComponent(viewButton, layoutData)
|
||||
}
|
||||
if (certificates) {
|
||||
Button certsButton = new Button("View Certificates", {viewCertificates(rows[2])})
|
||||
contentPanel.addComponent(certsButton, layoutData)
|
||||
}
|
||||
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
prompt.setComponent(contentPanel)
|
||||
downloadButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
} else {
|
||||
download(rows[2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void download(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
def file = new File(core.muOptions.downloadLocation, result.name)
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
|
||||
target : file, sequential : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download Started", "Started download of "+result.name, MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void viewComment(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCommentView view = new ViewCommentView(result, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void viewCertificates(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SplitPattern
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
class SearchModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final String query
|
||||
private final Core core
|
||||
final TableModel model
|
||||
|
||||
private final Map<Persona, UIResultBatchEvent> resultsPerSender = new HashMap<>()
|
||||
|
||||
SearchModel(String query, Core core, TextGUIThread guiThread) {
|
||||
this.query = query
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
this.model = new TableModel("Sender","Results","Browse","Trust")
|
||||
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||
|
||||
|
||||
boolean hashSearch = false
|
||||
byte [] root = null
|
||||
if (query.length() == 44 && query.indexOf(" ") < 0) {
|
||||
try {
|
||||
root = Base64.decode(query)
|
||||
hashSearch = true
|
||||
} catch (Exception e) {
|
||||
// not hash search
|
||||
}
|
||||
}
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||
payload = root
|
||||
} else {
|
||||
def nonEmpty = SplitPattern.termify(query)
|
||||
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig: sig.data))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(UIResultBatchEvent.class, this)
|
||||
}
|
||||
|
||||
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||
guiThread.invokeLater {
|
||||
Persona sender = e.results[0].sender
|
||||
|
||||
resultsPerSender.put(sender, e)
|
||||
|
||||
String browse = String.valueOf(e.results[0].browse)
|
||||
String results = String.valueOf(e.results.length)
|
||||
String trust = core.trustService.getLevel(sender.destination).toString()
|
||||
model.addRow([new PersonaWrapper(sender), results, browse, trust])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
class SearchView extends BasicWindow {
|
||||
private final Core core
|
||||
private final SearchModel model
|
||||
private final Table table
|
||||
private final TextGUI textGUI
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
SearchView(SearchModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super(model.query)
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
table = new Table("Sender","Results","Browse","Trust")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
Button closeButton = new Button("Close", {
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def rows = model.model.getRow(selectedRow)
|
||||
Persona persona = rows[0].persona
|
||||
boolean browse = Boolean.parseBoolean(rows[2])
|
||||
Window prompt = new BasicWindow("Show Or Browse "+rows[0]+"?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(6))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
Button showResults = new Button("Show Results", {
|
||||
showResults(persona)
|
||||
})
|
||||
Button browseHost = new Button("Browse Host", {
|
||||
BrowseModel model = new BrowseModel(persona, core, textGUI.getGUIThread())
|
||||
BrowseView view = new BrowseView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
})
|
||||
Button trustHost = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + " has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button neutralHost = new Button("Neutral",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + " has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustHost = new Button("Distrust", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + " has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closePrompt = new Button("Close", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(showResults, layoutData)
|
||||
if (browse)
|
||||
addComponent(browseHost, layoutData)
|
||||
addComponent(trustHost, layoutData)
|
||||
addComponent(neutralHost, layoutData)
|
||||
addComponent(distrustHost, layoutData)
|
||||
addComponent(closePrompt, layoutData)
|
||||
}
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
showResults.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void showResults(Persona persona) {
|
||||
def results = model.resultsPerSender.get(persona)
|
||||
ResultsModel resultsModel = new ResultsModel(results)
|
||||
ResultsView resultsView = new ResultsView(resultsModel, core, textGUI, terminalSize)
|
||||
textGUI.addWindowAndWait(resultsView)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class SharedFileWrapper {
|
||||
private final SharedFile sharedFile
|
||||
|
||||
SharedFileWrapper(SharedFile sharedFile) {
|
||||
this.sharedFile = sharedFile
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
sharedFile.getCachedPath()
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
|
||||
class TrustListModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final RemoteTrustList trustList
|
||||
private final Core core
|
||||
private final TableModel trustedTableModel, distrustedTableModel
|
||||
|
||||
TrustListModel(RemoteTrustList trustList, Core core) {
|
||||
this.trustList = trustList
|
||||
this.core = core
|
||||
|
||||
trustedTableModel = new TableModel("Trusted User","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
|
||||
refreshModels()
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModels() {
|
||||
int trustRows = trustedTableModel.getRowCount()
|
||||
trustRows.times { trustedTableModel.removeRow(0) }
|
||||
int distrustRows = distrustedTableModel.getRowCount()
|
||||
distrustRows.times { distrustedTableModel.removeRow(0) }
|
||||
|
||||
trustList.good.each {
|
||||
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
}
|
||||
trustList.bad.each {
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
}
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(TrustEvent.class, this)
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
class TrustListView extends BasicWindow {
|
||||
private final TrustListModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TerminalSize terminalSize
|
||||
private final Table trusted, distrusted
|
||||
|
||||
TrustListView(TrustListModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
int tableSize = terminalSize.getRows() - 10
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Label nameLabel = new Label("Trust list for "+model.trustList.persona.getHumanReadableName())
|
||||
Label lastUpdatedLabel = new Label("Last updated "+new Date(model.trustList.timestamp))
|
||||
contentPanel.addComponent(nameLabel, layoutData)
|
||||
contentPanel.addComponent(lastUpdatedLabel, layoutData)
|
||||
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted User","Your Trust")
|
||||
trusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.trustedTableModel)
|
||||
setVisibleRows(tableSize)
|
||||
}
|
||||
trusted.setSelectAction({ actionsForUser(true) })
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted User", "Your Trust")
|
||||
distrusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.distrustedTableModel)
|
||||
setVisibleRows(tableSize)
|
||||
}
|
||||
distrusted.setSelectAction({actionsForUser(false)})
|
||||
topPanel.addComponent(distrusted, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void actionsForUser(boolean trustedUser) {
|
||||
def table = trustedUser ? trusted : distrusted
|
||||
def model = trustedUser ? model.trustedTableModel : model.distrustedTableModel
|
||||
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.getRow(selectedRow)
|
||||
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Actions for "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button trustButton = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button neutralButton = new Button("Neutral",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustButton = new Button("Distrust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close",{prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(trustButton,layoutData)
|
||||
addComponent(neutralButton, layoutData)
|
||||
addComponent(distrustButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
|
||||
class TrustListWrapper {
|
||||
private final RemoteTrustList trustList
|
||||
TrustListWrapper(RemoteTrustList trustList) {
|
||||
this.trustList = trustList
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
trustList.persona.getHumanReadableName()
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
|
||||
|
||||
class TrustModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final TableModel modelTrusted, modelDistrusted, modelSubscriptions
|
||||
|
||||
TrustModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
modelTrusted = new TableModel("Trusted Users")
|
||||
modelDistrusted = new TableModel("Distrusted Users")
|
||||
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModels() {
|
||||
int trustedRows = modelTrusted.getRowCount()
|
||||
trustedRows.times { modelTrusted.removeRow(0) }
|
||||
int distrustedRows = modelDistrusted.getRowCount()
|
||||
distrustedRows.times { modelDistrusted.removeRow(0) }
|
||||
int subsRows = modelSubscriptions.getRowCount()
|
||||
subsRows.times { modelSubscriptions.removeRow(0) }
|
||||
|
||||
core.trustService.good.values().each {
|
||||
modelTrusted.addRow(new PersonaWrapper(it))
|
||||
}
|
||||
|
||||
core.trustService.bad.values().each {
|
||||
modelDistrusted.addRow(new PersonaWrapper(it))
|
||||
}
|
||||
|
||||
core.trustSubscriber.remoteTrustLists.values().each {
|
||||
def name = new TrustListWrapper(it)
|
||||
String trusted = String.valueOf(it.good.size())
|
||||
String distrusted = String.valueOf(it.bad.size())
|
||||
String status = it.status
|
||||
String lastUpdated = it.timestamp == 0 ? "Never" : new Date(it.timestamp)
|
||||
|
||||
modelSubscriptions.addRow(name, trusted, distrusted, status, lastUpdated)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
class TrustView extends BasicWindow {
|
||||
private final TrustModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TerminalSize terminalSize
|
||||
private final Table trusted, distrusted, subscriptions
|
||||
|
||||
TrustView(TrustModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
int tableSize = (terminalSize.getRows() / 2 - 10).toInteger()
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted Users")
|
||||
trusted.setCellSelection(false)
|
||||
trusted.setSelectAction({trustedActions()})
|
||||
trusted.setTableModel(model.modelTrusted)
|
||||
trusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted users")
|
||||
distrusted.setCellSelection(false)
|
||||
distrusted.setSelectAction({distrustedActions()})
|
||||
distrusted.setTableModel(model.modelDistrusted)
|
||||
distrusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(distrusted, layoutData)
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
bottomPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label tableName = new Label("Trust List Subscriptions")
|
||||
bottomPanel.addComponent(tableName, layoutData)
|
||||
|
||||
subscriptions = new Table("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
subscriptions.setCellSelection(false)
|
||||
subscriptions.setSelectAction({trustListActions()})
|
||||
subscriptions.setTableModel(model.modelSubscriptions)
|
||||
subscriptions.setVisibleRows(tableSize)
|
||||
bottomPanel.addComponent(subscriptions, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
contentPanel.addComponent(bottomPanel, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void trustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
def row = model.modelTrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button subscribe = new Button("Subscribe", {
|
||||
core.muOptions.trustSubscriptions.add(persona)
|
||||
saveMuSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
|
||||
MessageDialog.showMessageDialog(textGUI, "Subscribed", "Subscribed from trust list of " + persona.getHumanReadableName(),
|
||||
MessageDialogButton.OK)
|
||||
model.refreshModels()
|
||||
|
||||
})
|
||||
Button markNeutral = new Button("Mark Neutral", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Distrusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.with {
|
||||
addComponent(subscribe, layoutData)
|
||||
addComponent(markNeutral, layoutData)
|
||||
addComponent(markDistrusted, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void distrustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
def row = model.modelDistrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button markNeutral = new Button("Mark Neutral", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Trusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.with {
|
||||
addComponent(markDistrusted, layoutData)
|
||||
addComponent(markNeutral, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void trustListActions() {
|
||||
int selectedRow = subscriptions.getSelectedRow()
|
||||
def row = model.modelSubscriptions.getRow(selectedRow)
|
||||
|
||||
def trustList = row[0].trustList
|
||||
Persona persona = trustList.persona
|
||||
|
||||
Window prompt = new BasicWindow("Trust List Actions")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button reviewButton = new Button("Review",{review(trustList)})
|
||||
Button updateButton = new Button("Update",{
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
|
||||
MessageDialog.showMessageDialog(textGUI, "Updating...", "Trust list will update soon", MessageDialogButton.OK)
|
||||
})
|
||||
Button unsubscribeButton = new Button("Unsubscribe", {
|
||||
core.muOptions.trustSubscriptions.remove(persona)
|
||||
saveMuSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Unsubscribed", "Unsubscribed from trust list of " + persona.getHumanReadableName(),
|
||||
MessageDialogButton.OK)
|
||||
model.refreshModels()
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(reviewButton, layoutData)
|
||||
addComponent(updateButton, layoutData)
|
||||
addComponent(unsubscribeButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void review(def trustList) {
|
||||
TrustListModel model = new TrustListModel(trustList, core)
|
||||
TrustListView view = new TrustListView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
model.unregister()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File settingsFile = new File(core.home,"MuWire.properties")
|
||||
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
|
||||
class UpdateTextView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
UpdateTextView(String text, TerminalSize terminalSize) {
|
||||
super("Update Details")
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.upload.UploadEvent
|
||||
import com.muwire.core.upload.UploadFinishedEvent
|
||||
import com.muwire.core.upload.Uploader
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class UploadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private CliSettings props
|
||||
private final List<UploaderWrapper> uploaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces", "Speed")
|
||||
|
||||
UploadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
|
||||
Timer timer = new Timer(true)
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
timer.schedule({
|
||||
guiThread.invokeLater(refreshModel)
|
||||
} as TimerTask, 1000, 1000)
|
||||
|
||||
}
|
||||
|
||||
void onUploadEvent(UploadEvent e) {
|
||||
guiThread.invokeLater {
|
||||
UploaderWrapper found = null
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
found = it
|
||||
return
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
found.uploader = e.uploader
|
||||
found.finished = false
|
||||
} else
|
||||
uploaders << new UploaderWrapper(uploader : e.uploader)
|
||||
}
|
||||
}
|
||||
|
||||
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
it.finished = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int uploadersSize = model.getRowCount()
|
||||
uploadersSize.times { model.removeRow(0) }
|
||||
|
||||
if (props.clearUploads) {
|
||||
uploaders.removeAll { it.finished }
|
||||
}
|
||||
|
||||
uploaders.each {
|
||||
String name = it.uploader.getName()
|
||||
int percent = it.uploader.getProgress()
|
||||
String percentString = "$percent% of piece".toString()
|
||||
String downloader = it.uploader.getDownloader()
|
||||
|
||||
int pieces = it.uploader.getTotalPieces()
|
||||
int done = it.uploader.getDonePieces()
|
||||
if (percent == 100)
|
||||
done++
|
||||
int percentTotal = -1
|
||||
if (pieces != 0)
|
||||
percentTotal = (done * 100) / pieces
|
||||
long size = it.uploader.getTotalSize()
|
||||
String totalSize = ""
|
||||
if (size > 0)
|
||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
|
||||
|
||||
String speed = DataHelper.formatSize2Decimal(it.uploader.speed(), false) + "B/sec"
|
||||
|
||||
|
||||
model.addRow([name, percentString, downloader, remotePieces, speed])
|
||||
}
|
||||
}
|
||||
|
||||
private static class UploaderWrapper {
|
||||
Uploader uploader
|
||||
boolean finished
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
uploader.getName()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
class UploadsView extends BasicWindow {
|
||||
private final UploadsModel model
|
||||
private final Table table
|
||||
|
||||
UploadsView(UploadsModel model, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Progress","Downloader","Remote Pieces","Speed")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearDoneButton = new Button("Clear Finished",{
|
||||
model.uploaders.removeAll { it.finished }
|
||||
})
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
buttonsPanel.addComponent(clearDoneButton, layoutData)
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateFetchEvent
|
||||
import com.muwire.core.filecert.CertificateFetchStatus
|
||||
import com.muwire.core.filecert.CertificateFetchedEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ViewCertificatesModel {
|
||||
private final UIResultEvent result
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
|
||||
private final TableModel model = new TableModel("Issuer","File Name","Timestamp")
|
||||
private final Map<Persona, Set<Certificate>> byIssuer = new HashMap<>()
|
||||
|
||||
private int totalCerts
|
||||
|
||||
private Label status
|
||||
private Label percentage
|
||||
|
||||
ViewCertificatesModel(UIResultEvent result, Core core, TextGUIThread guiThread) {
|
||||
this.result = result
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
|
||||
core.eventBus.with {
|
||||
register(CertificateFetchEvent.class,this)
|
||||
register(CertificateFetchedEvent.class, this)
|
||||
publish(new UIFetchCertificatesEvent(host : result.sender, infoHash : result.infohash))
|
||||
}
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(CertificateFetchEvent.class, this)
|
||||
core.eventBus.unregister(CertificateFetchedEvent.class, this)
|
||||
}
|
||||
|
||||
void onCertificateFetchEvent(CertificateFetchEvent e) {
|
||||
guiThread.invokeLater {
|
||||
status.setText(e.status.toString())
|
||||
if (e.status == CertificateFetchStatus.FETCHING)
|
||||
totalCerts = e.count
|
||||
}
|
||||
}
|
||||
|
||||
void onCertificateFetchedEvent(CertificateFetchedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
Date date = new Date(e.certificate.timestamp)
|
||||
model.addRow(new PersonaWrapper(e.certificate.issuer), e.certificate.name.name, date)
|
||||
|
||||
Set<Certificate> set = byIssuer.get(e.certificate.issuer)
|
||||
if (set == null) {
|
||||
set = new HashSet<>()
|
||||
byIssuer.put(e.certificate.issuer, set)
|
||||
}
|
||||
set.add(e.certificate)
|
||||
|
||||
String percentageString = ""
|
||||
if (totalCerts > 0) {
|
||||
double percentage = Math.round((model.getRowCount() * 100 / totalCerts).toDouble())
|
||||
percentageString = String.valueOf(percentage) + "%"
|
||||
}
|
||||
percentage.setText(percentageString)
|
||||
}
|
||||
}
|
||||
|
||||
void setStatusLabel(Label status) {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
void setPercentageLabel(Label percentage) {
|
||||
this.percentage = percentage
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
|
||||
class ViewCertificatesView extends BasicWindow {
|
||||
private final ViewCertificatesModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
ViewCertificatesView(ViewCertificatesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Certificates")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label statusLabel = new Label("")
|
||||
Label percentageLabel = new Label("")
|
||||
model.setStatusLabel(statusLabel)
|
||||
model.setPercentageLabel(percentageLabel)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
topPanel.addComponent(statusLabel, layoutData)
|
||||
topPanel.addComponent(percentageLabel, layoutData)
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
|
||||
table = new Table("Issuer","File Name","Timestamp")
|
||||
table.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.model)
|
||||
setVisibleRows(terminalSize.getRows())
|
||||
setSelectAction({rowSelected()})
|
||||
}
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Import Certificate?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(2))
|
||||
Button importButton = new Button("Import", {importCert(persona)})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.addComponent(importButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
importButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void importCert(Persona persona) {
|
||||
Set<Certificate> certs = model.byIssuer.get(persona)
|
||||
certs.each {
|
||||
core.eventBus.publish(new UIImportCertificateEvent(certificate : it))
|
||||
}
|
||||
MessageDialog.showMessageDialog(textGUI, "Certificate(s) Imported", "", MessageDialogButton.OK)
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class ViewCommentView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
ViewCommentView(UIResultEvent result, TerminalSize terminalSize) {
|
||||
super("View Comments For "+result.getName())
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, result.comment, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -16,66 +16,66 @@ import com.muwire.core.upload.UploadEvent
|
||||
import com.muwire.core.upload.UploadFinishedEvent
|
||||
|
||||
class Cli {
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
if (!home.exists())
|
||||
home.mkdirs()
|
||||
|
||||
|
||||
def propsFile = new File(home,"MuWire.properties")
|
||||
if (!propsFile.exists()) {
|
||||
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
def props = new Properties()
|
||||
propsFile.withInputStream { props.load(it) }
|
||||
props = new MuWireSettings(props)
|
||||
|
||||
Core core
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.6")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def filesList
|
||||
if (args.length == 0) {
|
||||
println "Enter a file containing list of files to share"
|
||||
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||
filesList = reader.readLine()
|
||||
} else
|
||||
} else
|
||||
filesList = args[0]
|
||||
|
||||
|
||||
Thread.sleep(1000)
|
||||
println "loading shared files from $filesList"
|
||||
|
||||
|
||||
// listener for shared files
|
||||
def sharedListener = new SharedListener()
|
||||
core.eventBus.register(FileHashedEvent.class, sharedListener)
|
||||
core.eventBus.register(FileLoadedEvent.class, sharedListener)
|
||||
|
||||
|
||||
// for connections
|
||||
def connectionsListener = new ConnectionListener()
|
||||
core.eventBus.register(ConnectionEvent.class, connectionsListener)
|
||||
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
|
||||
|
||||
|
||||
// for uploads
|
||||
def uploadsListener = new UploadsListener()
|
||||
core.eventBus.register(UploadEvent.class, uploadsListener)
|
||||
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
|
||||
|
||||
|
||||
Timer timer = new Timer("status-printer", true)
|
||||
timer.schedule({
|
||||
println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
|
||||
} as TimerTask, 60000, 60000)
|
||||
|
||||
|
||||
def latch = new CountDownLatch(1)
|
||||
def fileLoader = new Object() {
|
||||
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
@ -85,14 +85,14 @@ class Cli {
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
println "waiting for files to load"
|
||||
latch.await()
|
||||
// now we begin
|
||||
println "MuWire is ready"
|
||||
|
||||
|
||||
filesList = new File(filesList)
|
||||
filesList.withReader {
|
||||
filesList.withReader {
|
||||
def toShare = it.readLine()
|
||||
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
||||
}
|
||||
@ -103,7 +103,7 @@ class Cli {
|
||||
})
|
||||
Thread.sleep(Integer.MAX_VALUE)
|
||||
}
|
||||
|
||||
|
||||
static class ConnectionListener {
|
||||
volatile int connections
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
@ -114,7 +114,7 @@ class Cli {
|
||||
connections--
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class UploadsListener {
|
||||
volatile int uploads
|
||||
public void onUploadEvent(UploadEvent e) {
|
||||
@ -126,7 +126,7 @@ class Cli {
|
||||
println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class SharedListener {
|
||||
volatile int shared
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
|
@ -17,31 +17,31 @@ import com.muwire.core.search.UIResultEvent
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class CliDownloader {
|
||||
|
||||
|
||||
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
|
||||
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
|
||||
|
||||
|
||||
public static void main(String []args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
if (!home.exists())
|
||||
home.mkdirs()
|
||||
|
||||
|
||||
def propsFile = new File(home,"MuWire.properties")
|
||||
if (!propsFile.exists()) {
|
||||
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
def props = new Properties()
|
||||
propsFile.withInputStream { props.load(it) }
|
||||
props = new MuWireSettings(props)
|
||||
|
||||
|
||||
def filesList
|
||||
int connections
|
||||
int resultWait
|
||||
if (args.length != 3) {
|
||||
println "Enter a file containing list of hashes of files to download, " +
|
||||
println "Enter a file containing list of hashes of files to download, " +
|
||||
"how many connections you want before searching" +
|
||||
"and how long to wait for results to arrive"
|
||||
System.exit(1)
|
||||
@ -53,24 +53,24 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.6")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def latch = new CountDownLatch(connections)
|
||||
def connectionListener = new ConnectionWaiter(latch : latch)
|
||||
core.eventBus.register(ConnectionEvent.class, connectionListener)
|
||||
|
||||
|
||||
core.startServices()
|
||||
println "starting to wait until there are $connections connections"
|
||||
latch.await()
|
||||
|
||||
|
||||
println "connected, searching for files"
|
||||
|
||||
|
||||
def file = new File(filesList)
|
||||
file.eachLine {
|
||||
String[] split = it.split(",")
|
||||
@ -79,22 +79,22 @@ class CliDownloader {
|
||||
def hash = Base64.decode(split[0])
|
||||
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
|
||||
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
|
||||
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
|
||||
}
|
||||
|
||||
|
||||
println "waiting for results to arrive"
|
||||
Thread.sleep(resultWait * 1000)
|
||||
|
||||
|
||||
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
|
||||
resultsListeners.each { uuid, resultsListener ->
|
||||
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
|
||||
File target = new File(resultsListener.fileName)
|
||||
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
|
||||
}
|
||||
|
||||
|
||||
Thread.sleep(1000)
|
||||
|
||||
|
||||
Timer timer = new Timer("stats-printer")
|
||||
timer.schedule({
|
||||
println "==== STATUS UPDATE ==="
|
||||
@ -109,7 +109,7 @@ class CliDownloader {
|
||||
}
|
||||
println "==== END ==="
|
||||
} as TimerTask, 60000, 60000)
|
||||
|
||||
|
||||
println "waiting for downloads to finish"
|
||||
while(true) {
|
||||
boolean allFinished = true
|
||||
@ -120,10 +120,10 @@ class CliDownloader {
|
||||
break
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
|
||||
|
||||
println "all downloads finished"
|
||||
}
|
||||
|
||||
|
||||
static class ResultsHolder {
|
||||
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
|
||||
String fileName
|
||||
@ -134,7 +134,7 @@ class CliDownloader {
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class ResultsListener {
|
||||
UUID uuid
|
||||
String fileName
|
||||
@ -148,7 +148,7 @@ class CliDownloader {
|
||||
listener.add(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class ConnectionWaiter {
|
||||
CountDownLatch latch
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
@ -156,7 +156,7 @@ class CliDownloader {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static class DownloadListener {
|
||||
public void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||
|
@ -11,10 +11,10 @@ class FileList {
|
||||
println "pass files.json as argument"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
def slurper = new JsonSlurper()
|
||||
File filesJson = new File(args[0])
|
||||
filesJson.eachLine {
|
||||
filesJson.eachLine {
|
||||
def json = slurper.parseText(it)
|
||||
String name = DataUtil.readi18nString(Base64.decode(json.file))
|
||||
println "$name,$json.length,$json.pieceSize,$json.infoHash"
|
||||
|
@ -2,9 +2,9 @@ apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile 'net.i2p:router:0.9.40'
|
||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
||||
compile 'net.i2p.client:streaming:0.9.40'
|
||||
compile "net.i2p:router:${i2pVersion}"
|
||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@ -1,13 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import net.i2p.crypto.SigType
|
||||
|
||||
class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||
public static final int MAX_HEADERS = 16
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||
}
|
@ -18,8 +18,14 @@ 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.filecert.CertificateClient
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
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
|
||||
@ -27,18 +33,24 @@ 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.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
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.BrowseManager
|
||||
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.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
@ -47,6 +59,8 @@ 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
|
||||
@ -68,13 +82,13 @@ import net.i2p.router.RouterContext
|
||||
|
||||
@Log
|
||||
public class Core {
|
||||
|
||||
|
||||
final EventBus eventBus
|
||||
final Persona me
|
||||
final File home
|
||||
final Properties i2pOptions
|
||||
final MuWireSettings muOptions
|
||||
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
@ -89,20 +103,24 @@ public class Core {
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
|
||||
final ContentManager contentManager
|
||||
final CertificateManager certificateManager
|
||||
|
||||
private final Router router
|
||||
|
||||
|
||||
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||
|
||||
|
||||
final SigningPrivateKey spk
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
this.home = home
|
||||
this.muOptions = props
|
||||
|
||||
|
||||
i2pOptions = new Properties()
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
@ -122,7 +140,7 @@ public class Core {
|
||||
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||
}
|
||||
|
||||
|
||||
if (!props.embeddedRouter) {
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
@ -131,6 +149,7 @@ public class Core {
|
||||
} else {
|
||||
log.info("launching embedded router")
|
||||
Properties routerProps = new Properties()
|
||||
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
||||
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||
routerProps.setProperty("router.excludePeerCaps", "KLM")
|
||||
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||
@ -140,48 +159,49 @@ public class Core {
|
||||
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
|
||||
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
|
||||
router = new Router(routerProps)
|
||||
I2PAppContext.getGlobalContext().metaClass = new RouterContextMetaClass()
|
||||
router.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
keyDat.withInputStream {
|
||||
destination.readBytes(it)
|
||||
def privateKey = new PrivateKey()
|
||||
privateKey.readBytes(it)
|
||||
spk.readBytes(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.write(Constants.PERSONA_VERSION)
|
||||
daos.writeShort((short)props.getNickname().length())
|
||||
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
|
||||
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
|
||||
daos.writeShort((short)name.length)
|
||||
daos.write(name)
|
||||
destination.writeBytes(daos)
|
||||
daos.flush()
|
||||
byte [] payload = baos.toByteArray()
|
||||
@ -193,66 +213,79 @@ public class Core {
|
||||
me = new Persona(new ByteArrayInputStream(baos.toByteArray()))
|
||||
log.info("Loaded myself as "+me.getHumanReadableName())
|
||||
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||
eventBus.register(TrustEvent.class, trustService)
|
||||
|
||||
|
||||
log.info "initializing file manager"
|
||||
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)
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing certificate manager")
|
||||
certificateManager = new CertificateManager(eventBus, home, me, spk)
|
||||
eventBus.register(UICreateCertificateEvent.class, certificateManager)
|
||||
eventBus.register(UIImportCertificateEvent.class, certificateManager)
|
||||
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
trustService = new TrustService(goodTrust, badTrust, 5000)
|
||||
eventBus.register(TrustEvent.class, trustService)
|
||||
|
||||
|
||||
log.info "initializing file manager"
|
||||
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)
|
||||
eventBus.register(UICommentEvent.class, fileManager)
|
||||
eventBus.register(SideCarFileEvent.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)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
|
||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||
eventBus.register(ConnectionEvent.class, hostCache)
|
||||
|
||||
log.info("initializing connection manager")
|
||||
connectionManager = props.isLeaf() ?
|
||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||
eventBus.register(ConnectionEvent.class, hostCache)
|
||||
|
||||
log.info("initializing connection manager")
|
||||
connectionManager = props.isLeaf() ?
|
||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||
eventBus.register(TrustEvent.class, connectionManager)
|
||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||
eventBus.register(TrustEvent.class, connectionManager)
|
||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||
eventBus.register(QueryEvent.class, connectionManager)
|
||||
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info("initializing certificate client")
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
eventBus.register(QueryEvent.class, searchManager)
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
eventBus.register(QueryEvent.class, searchManager)
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
@ -262,34 +295,47 @@ public class Core {
|
||||
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
eventBus.register(FileSharedEvent.class, hasherService)
|
||||
|
||||
eventBus.register(FileUnsharedEvent.class, hasherService)
|
||||
eventBus.register(DirectoryUnsharedEvent.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)
|
||||
|
||||
log.info("initializing browse manager")
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
hasherService.start()
|
||||
trustService.start()
|
||||
@ -302,12 +348,14 @@ public class Core {
|
||||
hostCache.waitForLoad()
|
||||
updateClient.start()
|
||||
}
|
||||
|
||||
|
||||
public void shutdown() {
|
||||
if (!shutdown.compareAndSet(false, true)) {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("saving settings")
|
||||
saveMuSettings()
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down download manageer")
|
||||
@ -318,27 +366,22 @@ public class Core {
|
||||
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 class RouterContextMetaClass extends DelegatingMetaClass {
|
||||
private final Object logManager = new MuWireLogManager()
|
||||
RouterContextMetaClass() {
|
||||
super(RouterContext.class)
|
||||
}
|
||||
|
||||
Object invokeMethod(Object object, String name, Object[] args) {
|
||||
if (name == "logManager")
|
||||
return logManager
|
||||
super.invokeMethod(object, name, args)
|
||||
}
|
||||
log.info("shutdown complete")
|
||||
}
|
||||
|
||||
public void saveMuSettings() {
|
||||
File f = new File(home, "MuWire.properties")
|
||||
f.withPrintWriter("UTF-8", { muOptions.write(it) })
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
@ -346,7 +389,7 @@ public class Core {
|
||||
log.info("creating home dir")
|
||||
home.mkdir()
|
||||
}
|
||||
|
||||
|
||||
def props = new Properties()
|
||||
def propsFile = new File(home, "MuWire.properties")
|
||||
if (propsFile.exists()) {
|
||||
@ -362,10 +405,10 @@ public class Core {
|
||||
props.write(it)
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.4.6")
|
||||
|
||||
Core core = new Core(props, home, "0.5.8")
|
||||
core.startServices()
|
||||
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
if (args.length == 0) {
|
||||
log.info("initialized everything, sleeping")
|
||||
|
@ -4,17 +4,17 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class Event {
|
||||
|
||||
private static final AtomicLong SEQ_NO = new AtomicLong();
|
||||
final long seqNo
|
||||
final long timestamp
|
||||
|
||||
Event() {
|
||||
seqNo = SEQ_NO.getAndIncrement()
|
||||
timestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"seqNo $seqNo timestamp $timestamp"
|
||||
}
|
||||
private static final AtomicLong SEQ_NO = new AtomicLong();
|
||||
final long seqNo
|
||||
final long timestamp
|
||||
|
||||
Event() {
|
||||
seqNo = SEQ_NO.getAndIncrement()
|
||||
timestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"seqNo $seqNo timestamp $timestamp"
|
||||
}
|
||||
}
|
||||
|
@ -10,42 +10,47 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import groovy.util.logging.Log
|
||||
@Log
|
||||
class EventBus {
|
||||
|
||||
private Map handlers = new HashMap()
|
||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("event-bus")
|
||||
rv
|
||||
}
|
||||
|
||||
void publish(Event e) {
|
||||
executor.execute({publishInternal(e)} as Runnable)
|
||||
}
|
||||
|
||||
private void publishInternal(Event e) {
|
||||
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
|
||||
def currentHandlers
|
||||
final def clazz = e.getClass()
|
||||
synchronized(this) {
|
||||
currentHandlers = handlers.getOrDefault(clazz, [])
|
||||
}
|
||||
currentHandlers.each {
|
||||
private Map handlers = new HashMap()
|
||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("event-bus")
|
||||
rv
|
||||
}
|
||||
|
||||
void publish(Event e) {
|
||||
executor.execute({publishInternal(e)} as Runnable)
|
||||
}
|
||||
|
||||
private void publishInternal(Event e) {
|
||||
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
|
||||
def currentHandlers
|
||||
final def clazz = e.getClass()
|
||||
synchronized(this) {
|
||||
currentHandlers = handlers.getOrDefault(clazz, [])
|
||||
}
|
||||
currentHandlers.each {
|
||||
try {
|
||||
it."on${clazz.getSimpleName()}"(e)
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.SEVERE, "exception dispatching event",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void register(Class<? extends Event> eventType, def handler) {
|
||||
log.info "Registering $handler for type $eventType"
|
||||
def currentHandlers = handlers.get(eventType)
|
||||
if (currentHandlers == null) {
|
||||
currentHandlers = new CopyOnWriteArrayList()
|
||||
handlers.put(eventType, currentHandlers)
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void register(Class<? extends Event> eventType, def handler) {
|
||||
log.info "Registering $handler for type $eventType"
|
||||
def currentHandlers = handlers.get(eventType)
|
||||
if (currentHandlers == null) {
|
||||
currentHandlers = new CopyOnWriteArrayList()
|
||||
handlers.put(eventType, currentHandlers)
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
|
||||
synchronized void unregister(Class<? extends Event> eventType, def handler) {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
}
|
||||
|
@ -13,5 +13,5 @@ class InvalidSignatureException extends Exception {
|
||||
public InvalidSignatureException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -6,126 +6,163 @@ import com.muwire.core.hostcache.CrawlerResponse
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class MuWireSettings {
|
||||
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
boolean searchExtraHop
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int totalUploadSlots
|
||||
int uploadSlotsPerUser
|
||||
int updateCheckInterval
|
||||
long lastUpdateCheck
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
File downloadLocation
|
||||
File incompleteLocation
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
int meshExpiration
|
||||
int speedSmoothSeconds
|
||||
boolean embeddedRouter
|
||||
int inBw, outBw
|
||||
|
||||
MuWireSettings() {
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
Set<String> negativeFileTree
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
}
|
||||
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
|
||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||
String incompleteLocationProp = props.getProperty("incompleteLocation")
|
||||
if (incompleteLocationProp != null)
|
||||
incompleteLocation = new File(incompleteLocationProp)
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
updateType = props.getProperty("updateType","jar")
|
||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
|
||||
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
|
||||
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
|
||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
|
||||
watchedDirectories = new HashSet<>()
|
||||
if (props.containsKey("watchedDirectories")) {
|
||||
String[] encoded = props.getProperty("watchedDirectories").split(",")
|
||||
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
|
||||
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
|
||||
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
|
||||
|
||||
trustSubscriptions = new HashSet<>()
|
||||
if (props.containsKey("trustSubscriptions")) {
|
||||
props.getProperty("trustSubscriptions").split(",").each {
|
||||
props.getProperty("trustSubscriptions").split(",").each {
|
||||
trustSubscriptions.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
|
||||
|
||||
}
|
||||
|
||||
void write(Writer out) throws IOException {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.toString())
|
||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
|
||||
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())
|
||||
if (incompleteLocation != null)
|
||||
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
props.setProperty("updateType",String.valueOf(updateType))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
|
||||
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
|
||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||
props.setProperty("inBw", String.valueOf(inBw))
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
|
||||
if (!watchedDirectories.isEmpty()) {
|
||||
String encoded = watchedDirectories.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty("watchedDirectories", encoded)
|
||||
}
|
||||
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
|
||||
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
|
||||
|
||||
if (!trustSubscriptions.isEmpty()) {
|
||||
String encoded = trustSubscriptions.stream().
|
||||
map({it.toBase64()}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
|
||||
props.store(out, "This file is UTF-8")
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
|
||||
boolean allowUntrusted() {
|
||||
allowUntrusted
|
||||
}
|
||||
|
||||
void setAllowUntrusted(boolean allowUntrusted) {
|
||||
this.allowUntrusted = allowUntrusted
|
||||
}
|
||||
|
||||
CrawlerResponse getCrawlerResponse() {
|
||||
crawlerResponse
|
||||
}
|
||||
|
||||
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
||||
this.crawlerResponse = crawlerResponse
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
|
||||
boolean allowUntrusted() {
|
||||
allowUntrusted
|
||||
}
|
||||
|
||||
void setAllowUntrusted(boolean allowUntrusted) {
|
||||
this.allowUntrusted = allowUntrusted
|
||||
}
|
||||
|
||||
CrawlerResponse getCrawlerResponse() {
|
||||
crawlerResponse
|
||||
}
|
||||
|
||||
void setCrawlerResponse(CrawlerResponse crawlerResponse) {
|
||||
this.crawlerResponse = crawlerResponse
|
||||
}
|
||||
|
||||
String getNickname() {
|
||||
nickname
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import java.nio.charset.StandardCharsets
|
||||
*/
|
||||
public class Name {
|
||||
final String name
|
||||
|
||||
|
||||
Name(String name) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
|
||||
Name(InputStream nameStream) throws IOException {
|
||||
DataInputStream dis = new DataInputStream(nameStream)
|
||||
int length = dis.readUnsignedShort()
|
||||
@ -19,22 +19,23 @@ public class Name {
|
||||
dis.readFully(nameBytes)
|
||||
this.name = new String(nameBytes, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
|
||||
public void write(OutputStream out) throws IOException {
|
||||
DataOutputStream dos = new DataOutputStream(out)
|
||||
dos.writeShort(name.length())
|
||||
dos.write(name.getBytes(StandardCharsets.UTF_8))
|
||||
byte [] bytes = name.getBytes(StandardCharsets.UTF_8)
|
||||
dos.writeShort(bytes.length)
|
||||
dos.write(bytes)
|
||||
}
|
||||
|
||||
|
||||
public getName() {
|
||||
name
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode()
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Name))
|
||||
|
@ -9,7 +9,7 @@ import net.i2p.data.SigningPublicKey
|
||||
|
||||
public class Persona {
|
||||
private static final int SIG_LEN = Constants.SIG_TYPE.getSigLen()
|
||||
|
||||
|
||||
private final byte version
|
||||
private final Name name
|
||||
private final Destination destination
|
||||
@ -17,12 +17,12 @@ public class Persona {
|
||||
private volatile String humanReadableName
|
||||
private volatile String base64
|
||||
private volatile byte[] payload
|
||||
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
|
||||
version = (byte) (personaStream.read() & 0xFF)
|
||||
if (version != Constants.PERSONA_VERSION)
|
||||
throw new IOException("Unknown version "+version)
|
||||
|
||||
|
||||
name = new Name(personaStream)
|
||||
destination = Destination.create(personaStream)
|
||||
sig = new byte[SIG_LEN]
|
||||
@ -31,7 +31,7 @@ public class Persona {
|
||||
if (!verify(version, name, destination, sig))
|
||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify")
|
||||
}
|
||||
|
||||
|
||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
baos.write(version)
|
||||
@ -42,7 +42,7 @@ public class Persona {
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
|
||||
public void write(OutputStream out) throws IOException {
|
||||
if (payload == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
@ -54,13 +54,13 @@ public class Persona {
|
||||
}
|
||||
out.write(payload)
|
||||
}
|
||||
|
||||
|
||||
public String getHumanReadableName() {
|
||||
if (humanReadableName == null)
|
||||
if (humanReadableName == null)
|
||||
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32)
|
||||
humanReadableName
|
||||
}
|
||||
|
||||
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
@ -69,12 +69,12 @@ public class Persona {
|
||||
}
|
||||
base64
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode() ^ destination.hashCode()
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Persona))
|
||||
@ -82,7 +82,7 @@ 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"
|
||||
|
@ -2,12 +2,12 @@ package com.muwire.core
|
||||
|
||||
abstract class Service {
|
||||
|
||||
volatile boolean loaded
|
||||
|
||||
abstract void load()
|
||||
|
||||
void waitForLoad() {
|
||||
while (!loaded)
|
||||
Thread.sleep(10)
|
||||
}
|
||||
volatile boolean loaded
|
||||
|
||||
abstract void load()
|
||||
|
||||
void waitForLoad() {
|
||||
while (!loaded)
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
|
91
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
91
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
@ -0,0 +1,91 @@
|
||||
package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
||||
|
||||
private static final Set<Character> SPLIT_CHARS = new HashSet<>()
|
||||
static {
|
||||
SPLIT_CHARS.with {
|
||||
add(' '.toCharacter())
|
||||
add('*'.toCharacter())
|
||||
add('+'.toCharacter())
|
||||
add('-'.toCharacter())
|
||||
add(','.toCharacter())
|
||||
add('.'.toCharacter())
|
||||
add(':'.toCharacter())
|
||||
add(';'.toCharacter())
|
||||
add('('.toCharacter())
|
||||
add(')'.toCharacter())
|
||||
add('='.toCharacter())
|
||||
add('_'.toCharacter())
|
||||
add('/'.toCharacter())
|
||||
add('\\'.toCharacter())
|
||||
add('!'.toCharacter())
|
||||
add('\''.toCharacter())
|
||||
add('$'.toCharacter())
|
||||
add('%'.toCharacter())
|
||||
add('|'.toCharacter())
|
||||
add('['.toCharacter())
|
||||
add(']'.toCharacter())
|
||||
add('{'.toCharacter())
|
||||
add('}'.toCharacter())
|
||||
add('?'.toCharacter())
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] termify(final String source) {
|
||||
String lowercase = source.toLowerCase().trim()
|
||||
|
||||
def rv = []
|
||||
int pos = 0
|
||||
int quote = -1
|
||||
|
||||
StringBuilder tmp = new StringBuilder()
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (quote < 0 && c == '"') {
|
||||
quote = pos - 1
|
||||
continue
|
||||
}
|
||||
if (quote >= 0) {
|
||||
if (c == '"') {
|
||||
quote = -1
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
} else if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append c
|
||||
}
|
||||
|
||||
// check if odd number of quotes and re-tokenize from last quote
|
||||
if (quote >= 0) {
|
||||
tmp = new StringBuilder()
|
||||
pos = quote + 1
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() > 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp.length() > 0)
|
||||
rv << tmp.toString()
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,17 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
@ -16,8 +23,10 @@ import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.Signature
|
||||
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
@ -25,105 +34,108 @@ 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
|
||||
final HostCache hostCache
|
||||
final EventBus eventBus
|
||||
final Endpoint endpoint
|
||||
final boolean incoming
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final MuWireSettings settings
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
|
||||
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
|
||||
|
||||
long lastPingSentTime, lastPongReceivedTime
|
||||
|
||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||
|
||||
protected final String name
|
||||
|
||||
long lastPingSentTime, lastPongReceivedTime
|
||||
|
||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.incoming = incoming
|
||||
this.endpoint = endpoint
|
||||
this.hostCache = hostCache
|
||||
this.eventBus = eventBus
|
||||
this.incoming = incoming
|
||||
this.endpoint = endpoint
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
|
||||
this.name = endpoint.destination.toBase32().substring(0,8)
|
||||
|
||||
this.reader = new Thread({readLoop()} as Runnable)
|
||||
this.reader.setName("reader-$name")
|
||||
this.reader.setDaemon(true)
|
||||
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-$name")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the connection threads
|
||||
*/
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"$name already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING, "$name already closed", new Exception() )
|
||||
return
|
||||
}
|
||||
|
||||
this.name = endpoint.destination.toBase32().substring(0,8)
|
||||
|
||||
this.reader = new Thread({readLoop()} as Runnable)
|
||||
this.reader.setName("reader-$name")
|
||||
this.reader.setDaemon(true)
|
||||
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-$name")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the connection threads
|
||||
*/
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"$name already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING, "$name already closed", new Exception() )
|
||||
return
|
||||
}
|
||||
log.info("closing $name")
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
|
||||
protected void readLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
log.info("closed $name")
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
|
||||
protected void readLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (SocketTimeoutException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void read()
|
||||
|
||||
protected void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
protected abstract void read()
|
||||
|
||||
protected void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void write(def message);
|
||||
|
||||
void sendPing() {
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract void write(def message);
|
||||
|
||||
void sendPing() {
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
void sendQuery(QueryEvent e) {
|
||||
def query = [:]
|
||||
query.type = "Search"
|
||||
@ -132,34 +144,38 @@ abstract class Connection implements Closeable {
|
||||
query.firstHop = e.firstHop
|
||||
query.keywords = e.searchEvent.getSearchTerms()
|
||||
query.oobInfohash = e.searchEvent.oobInfohash
|
||||
query.searchComments = e.searchEvent.searchComments
|
||||
query.compressedResults = e.searchEvent.compressedResults
|
||||
if (e.searchEvent.searchHash != null)
|
||||
query.infohash = Base64.encode(e.searchEvent.searchHash)
|
||||
query.replyTo = e.replyTo.toBase64()
|
||||
if (e.originator != null)
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
protected void handlePing() {
|
||||
log.fine("$name received ping")
|
||||
def pong = [:]
|
||||
pong.type = "Pong"
|
||||
pong.version = 1
|
||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||
messages.put(pong)
|
||||
}
|
||||
|
||||
protected void handlePong(def pong) {
|
||||
log.fine("$name received pong")
|
||||
lastPongReceivedTime = System.currentTimeMillis()
|
||||
if (pong.pongs == null)
|
||||
throw new Exception("Pong doesn't have pongs")
|
||||
pong.pongs.each {
|
||||
def dest = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void handlePing() {
|
||||
log.fine("$name received ping")
|
||||
def pong = [:]
|
||||
pong.type = "Pong"
|
||||
pong.version = 1
|
||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||
messages.put(pong)
|
||||
}
|
||||
|
||||
protected void handlePong(def pong) {
|
||||
log.fine("$name received pong")
|
||||
lastPongReceivedTime = System.currentTimeMillis()
|
||||
if (pong.pongs == null)
|
||||
throw new Exception("Pong doesn't have pongs")
|
||||
pong.pongs.each {
|
||||
def dest = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
||||
}
|
||||
}
|
||||
|
||||
private boolean throttleSearch() {
|
||||
final long now = System.currentTimeMillis()
|
||||
if (searchTimestamps.size() < SEARCHES) {
|
||||
@ -173,19 +189,19 @@ abstract class Connection implements Closeable {
|
||||
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) {
|
||||
search.keywords = null
|
||||
infohash = Base64.decode(search.infohash)
|
||||
}
|
||||
|
||||
|
||||
Destination replyTo = new Destination(search.replyTo)
|
||||
TrustLevel trustLevel = trustService.getLevel(replyTo)
|
||||
if (trustLevel == TrustLevel.DISTRUSTED) {
|
||||
@ -196,7 +212,7 @@ abstract class Connection implements Closeable {
|
||||
log.info("dropping search from neutral peer")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Persona originator = null
|
||||
if (search.originator != null) {
|
||||
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
|
||||
@ -205,21 +221,48 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
boolean oob = false
|
||||
if (search.oobInfohash != null)
|
||||
oob = search.oobInfohash
|
||||
|
||||
boolean searchComments = false
|
||||
if (search.searchComments != null)
|
||||
searchComments = search.searchComments
|
||||
boolean compressedResults = false
|
||||
if (search.compressedResults != null)
|
||||
compressedResults = search.compressedResults
|
||||
byte[] sig = null
|
||||
// TODO: make this mandatory at some point
|
||||
if (search.sig != null) {
|
||||
sig = Base64.decode(search.sig)
|
||||
byte [] payload
|
||||
if (infohash != null)
|
||||
payload = infohash
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else
|
||||
log.info("no signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
uuid : uuid,
|
||||
oobInfohash : oob)
|
||||
oobInfohash : oob,
|
||||
searchComments : searchComments,
|
||||
compressedResults : compressedResults)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop )
|
||||
firstHop : search.firstHop,
|
||||
sig : sig )
|
||||
eventBus.publish(event)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,23 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.attribute.DosFileAttributes
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
@ -17,6 +25,7 @@ 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.ResultsSender
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
@ -25,180 +34,197 @@ import com.muwire.core.search.UnexpectedResultsException
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class ConnectionAcceptor {
|
||||
|
||||
final EventBus eventBus
|
||||
final UltrapeerConnectionManager manager
|
||||
final MuWireSettings settings
|
||||
final I2PAcceptor acceptor
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final EventBus eventBus
|
||||
final UltrapeerConnectionManager manager
|
||||
final MuWireSettings settings
|
||||
final I2PAcceptor acceptor
|
||||
final HostCache hostCache
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
final CertificateManager certificateManager
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionEstablisher establisher) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
this.acceptor = acceptor
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
|
||||
private volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
this.acceptor = acceptor
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.searchManager = searchManager
|
||||
this.fileManager = fileManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor")
|
||||
rv
|
||||
}
|
||||
|
||||
handshakerThreads = Executors.newCachedThreadPool { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
acceptorThread.execute({acceptLoop()} as Runnable)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
this.establisher = establisher
|
||||
this.certificateManager = certificateManager
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor")
|
||||
rv
|
||||
}
|
||||
|
||||
handshakerThreads = Executors.newCachedThreadPool { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("acceptor-processor-${System.currentTimeMillis()}")
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
acceptorThread.execute({acceptLoop()} as Runnable)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
acceptorThread.shutdownNow()
|
||||
handshakerThreads.shutdownNow()
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
acceptorThread.shutdownNow()
|
||||
handshakerThreads.shutdownNow()
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
try {
|
||||
while(true) {
|
||||
def incoming = acceptor.accept()
|
||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||
switch(trustService.getLevel(incoming.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (settings.allowUntrusted())
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
log.info("Disallowing distrusted connection")
|
||||
incoming.close()
|
||||
continue
|
||||
}
|
||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||
}
|
||||
while(true) {
|
||||
def incoming = acceptor.accept()
|
||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||
switch(trustService.getLevel(incoming.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (settings.allowUntrusted())
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
log.info("Disallowing distrusted connection")
|
||||
incoming.close()
|
||||
continue
|
||||
}
|
||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "exception in accept loop",e)
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncoming(Endpoint e) {
|
||||
InputStream is = e.inputStream
|
||||
try {
|
||||
int read = is.read()
|
||||
switch(read) {
|
||||
case (byte)'M':
|
||||
}
|
||||
|
||||
private void processIncoming(Endpoint e) {
|
||||
InputStream is = e.inputStream
|
||||
try {
|
||||
int read = is.read()
|
||||
switch(read) {
|
||||
case (byte)'M':
|
||||
if (settings.isLeaf())
|
||||
throw new IOException("Incoming connection as leaf")
|
||||
processMuWire(e)
|
||||
break
|
||||
case (byte)'G':
|
||||
processGET(e)
|
||||
break
|
||||
processMuWire(e)
|
||||
break
|
||||
case (byte)'G':
|
||||
processGET(e)
|
||||
break
|
||||
case (byte)'H':
|
||||
processHashList(e)
|
||||
break
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
case (byte)'R':
|
||||
processRESULTS(e)
|
||||
break
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
private void processMuWire(Endpoint e) {
|
||||
byte[] uWire = "uWire ".bytes
|
||||
for (int i = 0; i < uWire.length; i++) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != uWire[i]) {
|
||||
throw new IOException("unexpected value $read at position $i")
|
||||
}
|
||||
}
|
||||
|
||||
byte[] type = new byte[4]
|
||||
DataInputStream dis = new DataInputStream(e.inputStream)
|
||||
dis.readFully(type)
|
||||
|
||||
case (byte)'B':
|
||||
processBROWSE(e)
|
||||
break
|
||||
case (byte)'C':
|
||||
processCERTIFICATES(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.getOutputStream().close()
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
private void processMuWire(Endpoint e) {
|
||||
byte[] uWire = "uWire ".bytes
|
||||
for (int i = 0; i < uWire.length; i++) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != uWire[i]) {
|
||||
throw new IOException("unexpected value $read at position $i")
|
||||
}
|
||||
}
|
||||
|
||||
byte[] type = new byte[4]
|
||||
DataInputStream dis = new DataInputStream(e.inputStream)
|
||||
dis.readFully(type)
|
||||
|
||||
if (type == "leaf".bytes)
|
||||
handleIncoming(e, true)
|
||||
else if (type == "peer".bytes)
|
||||
handleIncoming(e, false)
|
||||
else
|
||||
else
|
||||
throw new IOException("unknown connection type $type")
|
||||
}
|
||||
|
||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||
boolean accept = !manager.isConnected(e.destination) &&
|
||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||
boolean accept = !manager.isConnected(e.destination) &&
|
||||
!establisher.isInProgress(e.destination) &&
|
||||
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||
if (accept) {
|
||||
log.info("accepting connection, leaf:$leaf")
|
||||
e.outputStream.write("OK".bytes)
|
||||
e.outputStream.flush()
|
||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||
} else {
|
||||
log.info("rejecting connection, leaf:$leaf")
|
||||
e.outputStream.write("REJECT".bytes)
|
||||
def hosts = hostCache.getGoodHosts(10)
|
||||
if (!hosts.isEmpty()) {
|
||||
def json = [:]
|
||||
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
||||
json = JsonOutput.toJson(json)
|
||||
def os = new DataOutputStream(e.outputStream)
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void processGET(Endpoint e) {
|
||||
if (accept) {
|
||||
log.info("accepting connection, leaf:$leaf")
|
||||
e.outputStream.write("OK".bytes)
|
||||
e.outputStream.flush()
|
||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||
} else {
|
||||
log.info("rejecting connection, leaf:$leaf")
|
||||
e.outputStream.write("REJECT".bytes)
|
||||
def hosts = hostCache.getGoodHosts(10)
|
||||
if (!hosts.isEmpty()) {
|
||||
def json = [:]
|
||||
json.tryHosts = hosts.collect { d -> d.toBase64() }
|
||||
json = JsonOutput.toJson(json)
|
||||
def os = new DataOutputStream(e.outputStream)
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.close()
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void processGET(Endpoint e) {
|
||||
byte[] et = new byte[3]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(et)
|
||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid GET connection")
|
||||
uploadManager.processGET(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void processHashList(Endpoint e) {
|
||||
byte[] ashList = new byte[8]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
@ -207,7 +233,7 @@ class ConnectionAcceptor {
|
||||
throw new IOException("Invalid HASHLIST connection")
|
||||
uploadManager.processHashList(e)
|
||||
}
|
||||
|
||||
|
||||
private void processPOST(final Endpoint e) throws IOException {
|
||||
byte [] ost = new byte[4]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
@ -229,7 +255,7 @@ class ConnectionAcceptor {
|
||||
|
||||
Persona sender = new Persona(dis)
|
||||
if (sender.destination != e.getDestination())
|
||||
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
||||
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
|
||||
int nResults = dis.readUnsignedShort()
|
||||
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||
for (int i = 0; i < nResults; i++) {
|
||||
@ -247,43 +273,199 @@ 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()
|
||||
private void processRESULTS(Endpoint e) {
|
||||
InputStream is = e.getInputStream()
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
byte[] esults = new byte[7]
|
||||
dis.readFully(esults)
|
||||
if (esults != "ESULTS ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid RESULTS connection")
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
try {
|
||||
String uuid = DataUtil.readTillRN(dis)
|
||||
UUID resultsUUID = UUID.fromString(uuid)
|
||||
if (!searchManager.hasLocalSearch(resultsUUID))
|
||||
throw new UnexpectedResultsException(resultsUUID.toString())
|
||||
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.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()
|
||||
}
|
||||
|
||||
if (!headers.containsKey("Sender"))
|
||||
throw new IOException("No Sender header")
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No Count header")
|
||||
|
||||
byte [] personaBytes = Base64.decode(headers['Sender'])
|
||||
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
|
||||
if (sender.destination != e.getDestination())
|
||||
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
|
||||
|
||||
int nResults = Integer.parseInt(headers['Count'])
|
||||
if (nResults > Constants.MAX_RESULTS)
|
||||
throw new IOException("too many results $nResults")
|
||||
|
||||
dis = new DataInputStream(new GZIPInputStream(dis))
|
||||
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)
|
||||
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||
} catch (IOException bad) {
|
||||
log.log(Level.WARNING, "failed to process RESULTS", bad)
|
||||
} finally {
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void processBROWSE(Endpoint e) {
|
||||
try {
|
||||
byte [] rowse = new byte[7]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(rowse)
|
||||
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid BROWSE connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.browseFiles) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
browsed++
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
def sharedFiles = fileManager.getSharedFiles().values()
|
||||
|
||||
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
it.hit()
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.flush()
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processTRUST(Endpoint e) {
|
||||
try {
|
||||
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()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processCERTIFICATES(Endpoint e) {
|
||||
try {
|
||||
byte [] ERTIFICATES = new byte[12]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(ERTIFICATES)
|
||||
if (ERTIFICATES != "ERTIFICATES ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid CERTIFICATES connection")
|
||||
|
||||
byte [] infoHashStringBytes = new byte[44]
|
||||
dis.readFully(infoHashStringBytes)
|
||||
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||
|
||||
byte[] rn = new byte[2]
|
||||
dis.readFully(rn)
|
||||
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Malformed CERTIFICATES request")
|
||||
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
log.info("responding to certificates request for $infoHashString")
|
||||
byte [] root = Base64.decode(infoHashString)
|
||||
|
||||
Set<Certificate> certs = certificateManager.getByInfoHash(new InfoHash(root))
|
||||
if (certs.isEmpty()) {
|
||||
log.info("certs not found")
|
||||
e.getOutputStream().write("404 Certs Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
return
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${certs.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
certs.each {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
it.write(baos)
|
||||
byte [] payload = baos.toByteArray()
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,164 +21,171 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class ConnectionEstablisher {
|
||||
|
||||
private static final int CONCURRENT = 4
|
||||
|
||||
final EventBus eventBus
|
||||
final I2PConnector i2pConnector
|
||||
final MuWireSettings settings
|
||||
final ConnectionManager connectionManager
|
||||
final HostCache hostCache
|
||||
|
||||
final Timer timer
|
||||
final ExecutorService executor
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
private static final int CONCURRENT = 4
|
||||
|
||||
final EventBus eventBus
|
||||
final I2PConnector i2pConnector
|
||||
final MuWireSettings settings
|
||||
final ConnectionManager connectionManager
|
||||
final HostCache hostCache
|
||||
|
||||
final Timer timer
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
ConnectionEstablisher(){}
|
||||
|
||||
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
||||
ConnectionManager connectionManager, HostCache hostCache) {
|
||||
this.eventBus = eventBus
|
||||
this.i2pConnector = i2pConnector
|
||||
this.settings = settings
|
||||
this.connectionManager = connectionManager
|
||||
this.hostCache = hostCache
|
||||
timer = new Timer("connection-timer",true)
|
||||
executor = Executors.newFixedThreadPool(CONCURRENT, { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("connector-${System.currentTimeMillis()}")
|
||||
rv
|
||||
} as ThreadFactory)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({connectIfNeeded()} as TimerTask, 100, 1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
return
|
||||
|
||||
def toTry = null
|
||||
for (int i = 0; i < 5; i++) {
|
||||
toTry = hostCache.getHosts(1)
|
||||
if (toTry.isEmpty())
|
||||
return
|
||||
toTry = toTry[0]
|
||||
if (!connectionManager.isConnected(toTry) &&
|
||||
!inProgress.contains(toTry)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (toTry == null)
|
||||
return
|
||||
if (!connectionManager.isConnected(toTry) && inProgress.add(toTry))
|
||||
executor.execute({connect(toTry)} as Runnable)
|
||||
}
|
||||
|
||||
private void connect(Destination toTry) {
|
||||
log.info("starting connect to ${toTry.toBase32()}")
|
||||
try {
|
||||
def endpoint = i2pConnector.connect(toTry)
|
||||
log.info("successful transport connect to ${toTry.toBase32()}")
|
||||
|
||||
// outgoing handshake
|
||||
endpoint.outputStream.write("MuWire ".bytes)
|
||||
def type = settings.isLeaf() ? "leaf" : "peer"
|
||||
endpoint.outputStream.write(type.bytes)
|
||||
endpoint.outputStream.flush()
|
||||
|
||||
InputStream is = endpoint.inputStream
|
||||
int read = is.read()
|
||||
if (read == -1) {
|
||||
fail endpoint
|
||||
return
|
||||
}
|
||||
switch(read) {
|
||||
case (byte)'O': readK(endpoint); break
|
||||
case (byte)'R': readEJECT(endpoint); break
|
||||
default :
|
||||
log.warning("unknown response $read")
|
||||
fail endpoint
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Couldn't connect to ${toTry.toBase32()}", e)
|
||||
def endpoint = new Endpoint(toTry, null, null, null)
|
||||
fail(endpoint)
|
||||
} finally {
|
||||
inProgress.remove(toTry)
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
}
|
||||
|
||||
private void readK(Endpoint e) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != 'K') {
|
||||
log.warning("unknown response after O: $read")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
|
||||
log.info("connection to ${e.destination.toBase32()} established")
|
||||
|
||||
// wrap into deflater / inflater streams and publish
|
||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: false, leaf: false, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||
}
|
||||
|
||||
private void readEJECT(Endpoint e) {
|
||||
byte[] eject = "EJECT".bytes
|
||||
for (int i = 0; i < eject.length; i++) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != eject[i]) {
|
||||
log.warning("Unknown response after R at position $i")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
}
|
||||
log.info("connection to ${e.destination.toBase32()} rejected")
|
||||
|
||||
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: false, leaf: false, status: ConnectionAttemptStatus.REJECTED))
|
||||
try {
|
||||
DataInputStream dais = new DataInputStream(e.inputStream)
|
||||
int payloadSize = dais.readUnsignedShort()
|
||||
byte[] payload = new byte[payloadSize]
|
||||
dais.readFully(payload)
|
||||
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
||||
ConnectionManager connectionManager, HostCache hostCache) {
|
||||
this.eventBus = eventBus
|
||||
this.i2pConnector = i2pConnector
|
||||
this.settings = settings
|
||||
this.connectionManager = connectionManager
|
||||
this.hostCache = hostCache
|
||||
timer = new Timer("connection-timer",true)
|
||||
executor = Executors.newFixedThreadPool(CONCURRENT, { r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("connector-${System.currentTimeMillis()}")
|
||||
rv
|
||||
} as ThreadFactory)
|
||||
|
||||
closer = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
def json = new JsonSlurper()
|
||||
json = json.parse(payload)
|
||||
void start() {
|
||||
timer.schedule({connectIfNeeded()} as TimerTask, 100, 1000)
|
||||
}
|
||||
|
||||
if (json.tryHosts == null) {
|
||||
log.warning("post-rejection json didn't contain hosts to try")
|
||||
return
|
||||
}
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
return
|
||||
|
||||
def toTry = null
|
||||
for (int i = 0; i < 5; i++) {
|
||||
toTry = hostCache.getHosts(1)
|
||||
if (toTry.isEmpty())
|
||||
return
|
||||
toTry = toTry[0]
|
||||
if (!connectionManager.isConnected(toTry) &&
|
||||
!inProgress.contains(toTry)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (toTry == null)
|
||||
return
|
||||
if (!connectionManager.isConnected(toTry) && inProgress.add(toTry))
|
||||
executor.execute({connect(toTry)} as Runnable)
|
||||
}
|
||||
|
||||
private void connect(Destination toTry) {
|
||||
log.info("starting connect to ${toTry.toBase32()}")
|
||||
try {
|
||||
def endpoint = i2pConnector.connect(toTry)
|
||||
log.info("successful transport connect to ${toTry.toBase32()}")
|
||||
|
||||
// outgoing handshake
|
||||
endpoint.outputStream.write("MuWire ".bytes)
|
||||
def type = settings.isLeaf() ? "leaf" : "peer"
|
||||
endpoint.outputStream.write(type.bytes)
|
||||
endpoint.outputStream.flush()
|
||||
|
||||
InputStream is = endpoint.inputStream
|
||||
int read = is.read()
|
||||
if (read == -1) {
|
||||
fail endpoint
|
||||
return
|
||||
}
|
||||
switch(read) {
|
||||
case (byte)'O': readK(endpoint); break
|
||||
case (byte)'R': readEJECT(endpoint); break
|
||||
default :
|
||||
log.warning("unknown response $read")
|
||||
fail endpoint
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Couldn't connect to ${toTry.toBase32()}", e)
|
||||
def endpoint = new Endpoint(toTry, null, null, null)
|
||||
fail(endpoint)
|
||||
} finally {
|
||||
inProgress.remove(toTry)
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
if (!closer.isShutdown()) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
} as Runnable
|
||||
}
|
||||
}
|
||||
|
||||
private void readK(Endpoint e) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != 'K') {
|
||||
log.warning("unknown response after O: $read")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
|
||||
log.info("connection to ${e.destination.toBase32()} established")
|
||||
|
||||
// wrap into deflater / inflater streams and publish
|
||||
def wrapped = new Endpoint(e.destination, new InflaterInputStream(e.inputStream), new DeflaterOutputStream(e.outputStream, true), e.toClose)
|
||||
eventBus.publish(new ConnectionEvent(endpoint: wrapped, incoming: false, leaf: false, status: ConnectionAttemptStatus.SUCCESSFUL))
|
||||
}
|
||||
|
||||
private void readEJECT(Endpoint e) {
|
||||
byte[] eject = "EJECT".bytes
|
||||
for (int i = 0; i < eject.length; i++) {
|
||||
int read = e.inputStream.read()
|
||||
if (read != eject[i]) {
|
||||
log.warning("Unknown response after R at position $i")
|
||||
fail e
|
||||
return
|
||||
}
|
||||
}
|
||||
log.info("connection to ${e.destination.toBase32()} rejected")
|
||||
|
||||
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: false, leaf: false, status: ConnectionAttemptStatus.REJECTED))
|
||||
try {
|
||||
DataInputStream dais = new DataInputStream(e.inputStream)
|
||||
int payloadSize = dais.readUnsignedShort()
|
||||
byte[] payload = new byte[payloadSize]
|
||||
dais.readFully(payload)
|
||||
|
||||
def json = new JsonSlurper()
|
||||
json = json.parse(payload)
|
||||
|
||||
if (json.tryHosts == null) {
|
||||
log.warning("post-rejection json didn't contain hosts to try")
|
||||
return
|
||||
}
|
||||
|
||||
json.tryHosts.asList().each {
|
||||
Destination suggested = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: suggested))
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||
} finally {
|
||||
// the end
|
||||
closer.execute({e.close()} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
json.tryHosts.asList().each {
|
||||
Destination suggested = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: suggested))
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||
} finally {
|
||||
// the end
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInProgress(Destination d) {
|
||||
inProgress.contains(d)
|
||||
}
|
||||
|
@ -6,14 +6,14 @@ import net.i2p.data.Destination
|
||||
|
||||
class ConnectionEvent extends Event {
|
||||
|
||||
Endpoint endpoint
|
||||
boolean incoming
|
||||
Boolean leaf // can be null if uknown
|
||||
ConnectionAttemptStatus status
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
||||
}
|
||||
Endpoint endpoint
|
||||
boolean incoming
|
||||
Boolean leaf // can be null if uknown
|
||||
ConnectionAttemptStatus status
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"ConnectionEvent ${super.toString()} endpoint: $endpoint incoming: $incoming leaf : $leaf status : $status"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,64 +11,61 @@ import com.muwire.core.trust.TrustLevel
|
||||
import net.i2p.data.Destination
|
||||
|
||||
abstract class ConnectionManager {
|
||||
|
||||
private static final int PING_TIME = 20000
|
||||
|
||||
final EventBus eventBus
|
||||
|
||||
private final Timer timer
|
||||
|
||||
protected final HostCache hostCache
|
||||
private static final int PING_TIME = 20000
|
||||
|
||||
final EventBus eventBus
|
||||
|
||||
private final Timer timer
|
||||
|
||||
protected final HostCache hostCache
|
||||
protected final Persona me
|
||||
protected final MuWireSettings settings
|
||||
|
||||
ConnectionManager() {}
|
||||
|
||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
|
||||
ConnectionManager() {}
|
||||
|
||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.hostCache = hostCache
|
||||
this.hostCache = hostCache
|
||||
this.settings = settings
|
||||
this.timer = new Timer("connections-pinger",true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
getConnections().each { it.close() }
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.DISTRUSTED)
|
||||
drop(e.persona.destination)
|
||||
}
|
||||
|
||||
abstract void drop(Destination d)
|
||||
|
||||
abstract Collection<Connection> getConnections()
|
||||
|
||||
protected abstract int getDesiredConnections()
|
||||
|
||||
boolean needsConnections() {
|
||||
return getConnections().size() < getDesiredConnections()
|
||||
}
|
||||
|
||||
abstract boolean isConnected(Destination d)
|
||||
|
||||
abstract void onConnectionEvent(ConnectionEvent e)
|
||||
|
||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||
|
||||
abstract void shutdown()
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
if (now - it.lastPingSentTime > PING_TIME)
|
||||
it.sendPing()
|
||||
}
|
||||
}
|
||||
this.timer = new Timer("connections-pinger",true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.DISTRUSTED)
|
||||
drop(e.persona.destination)
|
||||
}
|
||||
|
||||
abstract void drop(Destination d)
|
||||
|
||||
abstract Collection<Connection> getConnections()
|
||||
|
||||
protected abstract int getDesiredConnections()
|
||||
|
||||
boolean needsConnections() {
|
||||
return getConnections().size() < getDesiredConnections()
|
||||
}
|
||||
|
||||
abstract boolean isConnected(Destination d)
|
||||
|
||||
abstract void onConnectionEvent(ConnectionEvent e)
|
||||
|
||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
if (now - it.lastPingSentTime > PING_TIME)
|
||||
it.sendPing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ import com.muwire.core.Event
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class DisconnectionEvent extends Event {
|
||||
|
||||
Destination destination
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
||||
}
|
||||
Destination destination
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"DisconnectionEvent ${super.toString()} destination:${destination.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -8,39 +8,36 @@ import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class Endpoint implements Closeable {
|
||||
final Destination destination
|
||||
final InputStream inputStream
|
||||
final OutputStream outputStream
|
||||
final Destination destination
|
||||
final InputStream inputStream
|
||||
final OutputStream outputStream
|
||||
final def toClose
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean()
|
||||
|
||||
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
||||
this.destination = destination
|
||||
this.inputStream = inputStream
|
||||
this.outputStream = outputStream
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean()
|
||||
|
||||
Endpoint(Destination destination, InputStream inputStream, OutputStream outputStream, def toClose) {
|
||||
this.destination = destination
|
||||
this.inputStream = inputStream
|
||||
this.outputStream = outputStream
|
||||
this.toClose = toClose
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||
return
|
||||
}
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {outputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||
return
|
||||
}
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (toClose != null) {
|
||||
try {toClose.reset()} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"destination: ${destination.toBase32()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"destination: ${destination.toBase32()}"
|
||||
}
|
||||
}
|
||||
|
@ -5,18 +5,18 @@ import net.i2p.client.streaming.I2PSocketManager
|
||||
|
||||
class I2PAcceptor {
|
||||
|
||||
final I2PSocketManager socketManager
|
||||
final I2PServerSocket serverSocket
|
||||
|
||||
I2PAcceptor() {}
|
||||
|
||||
I2PAcceptor(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
this.serverSocket = socketManager.getServerSocket()
|
||||
}
|
||||
|
||||
Endpoint accept() {
|
||||
def socket = serverSocket.accept()
|
||||
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
final I2PSocketManager socketManager
|
||||
final I2PServerSocket serverSocket
|
||||
|
||||
I2PAcceptor() {}
|
||||
|
||||
I2PAcceptor(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
this.serverSocket = socketManager.getServerSocket()
|
||||
}
|
||||
|
||||
Endpoint accept() {
|
||||
def socket = serverSocket.accept()
|
||||
new Endpoint(socket.getPeerDestination(), socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,18 @@ import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class I2PConnector {
|
||||
|
||||
final I2PSocketManager socketManager
|
||||
|
||||
I2PConnector() {}
|
||||
|
||||
I2PConnector(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
}
|
||||
|
||||
Endpoint connect(Destination dest) {
|
||||
def socket = socketManager.connect(dest)
|
||||
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
|
||||
final I2PSocketManager socketManager
|
||||
|
||||
I2PConnector() {}
|
||||
|
||||
I2PConnector(I2PSocketManager socketManager) {
|
||||
this.socketManager = socketManager
|
||||
}
|
||||
|
||||
Endpoint connect(Destination dest) {
|
||||
def socket = socketManager.connect(dest)
|
||||
new Endpoint(dest, socket.getInputStream(), socket.getOutputStream(), socket)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,27 +11,27 @@ import com.muwire.core.trust.TrustService
|
||||
import net.i2p.data.Destination
|
||||
|
||||
/**
|
||||
* Connection where the other side is a leaf.
|
||||
* Connection where the other side is a leaf.
|
||||
* Such connections can only be incoming.
|
||||
* @author zab
|
||||
*/
|
||||
class LeafConnection extends Connection {
|
||||
|
||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||
TrustService trustService, MuWireSettings settings) {
|
||||
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||
}
|
||||
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,68 +13,68 @@ import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class LeafConnectionManager extends ConnectionManager {
|
||||
|
||||
final int maxConnections
|
||||
|
||||
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
||||
|
||||
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
||||
|
||||
final int maxConnections
|
||||
|
||||
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
||||
|
||||
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
||||
HostCache hostCache, MuWireSettings settings) {
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxConnections = maxConnections
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxConnections = maxConnections
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (me.destination == e.receivedOn) {
|
||||
connections.values().each { it.sendQuery(e) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
connections.values()
|
||||
}
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
connections.values()
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxConnections;
|
||||
}
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxConnections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
connections.containsKey(d)
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
connections.containsKey(d)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (e.incoming || e.leaf) {
|
||||
log.severe("Got inconsistent event as a leaf! $e")
|
||||
return
|
||||
}
|
||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
return
|
||||
|
||||
Connection c = new UltrapeerConnection(eventBus, e.endpoint)
|
||||
connections.put(e.endpoint.destination, c)
|
||||
c.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
def removed = connections.remove(e.destination)
|
||||
if (removed == null)
|
||||
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (e.incoming || e.leaf) {
|
||||
log.severe("Got inconsistent event as a leaf! $e")
|
||||
return
|
||||
}
|
||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
return
|
||||
|
||||
Connection c = new UltrapeerConnection(eventBus, e.endpoint)
|
||||
connections.put(e.endpoint.destination, c)
|
||||
c.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
def removed = connections.remove(e.destination)
|
||||
if (removed == null)
|
||||
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -20,63 +20,63 @@ import net.i2p.data.Destination
|
||||
*/
|
||||
@Log
|
||||
class PeerConnection extends Connection {
|
||||
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
|
||||
private final byte[] readHeader = new byte[3]
|
||||
private final byte[] writeHeader = new byte[3]
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
|
||||
private final byte[] readHeader = new byte[3]
|
||||
private final byte[] writeHeader = new byte[3]
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||
this.dis = new DataInputStream(endpoint.inputStream)
|
||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||
}
|
||||
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||
this.dis = new DataInputStream(endpoint.inputStream)
|
||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
dis.readFully(readHeader)
|
||||
int length = DataUtil.readLength(readHeader)
|
||||
log.fine("$name read length $length")
|
||||
|
||||
byte[] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
|
||||
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
||||
// TODO process binary
|
||||
} else {
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : handlePing(); break;
|
||||
case "Pong" : handlePong(json); break;
|
||||
@Override
|
||||
protected void read() {
|
||||
dis.readFully(readHeader)
|
||||
int length = DataUtil.readLength(readHeader)
|
||||
log.fine("$name read length $length")
|
||||
|
||||
byte[] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
|
||||
if ((readHeader[0] & (byte)0x80) == 0x80) {
|
||||
// TODO process binary
|
||||
} else {
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : handlePing(); break;
|
||||
case "Pong" : handlePong(json); break;
|
||||
case "Search": handleSearch(json); break
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
byte[] payload
|
||||
if (message instanceof Map) {
|
||||
payload = JsonOutput.toJson(message).bytes
|
||||
DataUtil.packHeader(payload.length, writeHeader)
|
||||
log.fine "$name writing message type ${message.type} length $payload.length"
|
||||
writeHeader[0] &= (byte)0x7F
|
||||
} else {
|
||||
// TODO: write binary
|
||||
}
|
||||
|
||||
dos.write(writeHeader)
|
||||
dos.write(payload)
|
||||
dos.flush()
|
||||
}
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
byte[] payload
|
||||
if (message instanceof Map) {
|
||||
payload = JsonOutput.toJson(message).bytes
|
||||
DataUtil.packHeader(payload.length, writeHeader)
|
||||
log.fine "$name writing message type ${message.type} length $payload.length"
|
||||
writeHeader[0] &= (byte)0x7F
|
||||
} else {
|
||||
// TODO: write binary
|
||||
}
|
||||
|
||||
dos.write(writeHeader)
|
||||
dos.write(payload)
|
||||
dos.flush()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,30 +17,30 @@ import net.i2p.data.Destination
|
||||
*/
|
||||
class UltrapeerConnection extends Connection {
|
||||
|
||||
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
||||
super(eventBus, endpoint, false, hostCache, trustService)
|
||||
}
|
||||
public UltrapeerConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
||||
super(eventBus, endpoint, false, hostCache, trustService)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
@Override
|
||||
protected void read() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
if (message instanceof Map) {
|
||||
writeJsonMessage(message)
|
||||
} else {
|
||||
writeBinaryMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeJsonMessage(def message) {
|
||||
|
||||
}
|
||||
|
||||
private void writeBinaryMessage(def message) {
|
||||
|
||||
}
|
||||
@Override
|
||||
protected void write(Object message) {
|
||||
if (message instanceof Map) {
|
||||
writeJsonMessage(message)
|
||||
} else {
|
||||
writeBinaryMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private void writeJsonMessage(def message) {
|
||||
|
||||
}
|
||||
|
||||
private void writeBinaryMessage(def message) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -15,28 +15,28 @@ import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
final int maxPeers, maxLeafs
|
||||
final TrustService trustService
|
||||
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
|
||||
UltrapeerConnectionManager() {}
|
||||
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
final int maxPeers, maxLeafs
|
||||
final TrustService trustService
|
||||
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
|
||||
UltrapeerConnectionManager() {}
|
||||
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxPeers = maxPeers
|
||||
this.maxLeafs = maxLeafs
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxPeers = maxPeers
|
||||
this.maxLeafs = maxLeafs
|
||||
this.trustService = trustService
|
||||
}
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
peerConnections.get(d)?.close()
|
||||
}
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
peerConnections.get(d)?.close()
|
||||
leafConnections.get(d)?.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
forwardQueryToLeafs(e)
|
||||
if (!e.firstHop)
|
||||
@ -50,67 +50,68 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
||||
rv.addAll(peerConnections.values())
|
||||
rv.addAll(leafConnections.values())
|
||||
rv
|
||||
}
|
||||
|
||||
boolean hasLeafSlots() {
|
||||
leafConnections.size() < maxLeafs
|
||||
}
|
||||
|
||||
boolean hasPeerSlots() {
|
||||
peerConnections.size() < maxPeers
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxPeers / 2;
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
||||
}
|
||||
@Override
|
||||
public Collection<Connection> getConnections() {
|
||||
def rv = new ArrayList(peerConnections.size() + leafConnections.size())
|
||||
rv.addAll(peerConnections.values())
|
||||
rv.addAll(leafConnections.values())
|
||||
rv
|
||||
}
|
||||
|
||||
boolean hasLeafSlots() {
|
||||
leafConnections.size() < maxLeafs
|
||||
}
|
||||
|
||||
boolean hasPeerSlots() {
|
||||
peerConnections.size() < maxPeers
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDesiredConnections() {
|
||||
return maxPeers / 2;
|
||||
}
|
||||
@Override
|
||||
public boolean isConnected(Destination d) {
|
||||
peerConnections.containsKey(d) || leafConnections.containsKey(d)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (!e.incoming && e.leaf) {
|
||||
log.severe("Inconsistent event $e")
|
||||
return
|
||||
}
|
||||
|
||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
return
|
||||
|
||||
Connection c = e.leaf ?
|
||||
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
||||
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
||||
def map = e.leaf ? leafConnections : peerConnections
|
||||
map.put(e.endpoint.destination, c)
|
||||
c.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
def removed = peerConnections.remove(e.destination)
|
||||
if (removed == null)
|
||||
removed = leafConnections.remove(e.destination)
|
||||
if (removed == null)
|
||||
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionEvent(ConnectionEvent e) {
|
||||
if (!e.incoming && e.leaf) {
|
||||
log.severe("Inconsistent event $e")
|
||||
return
|
||||
}
|
||||
|
||||
if (e.status != ConnectionAttemptStatus.SUCCESSFUL)
|
||||
return
|
||||
|
||||
Connection c = e.leaf ?
|
||||
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
||||
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
||||
def map = e.leaf ? leafConnections : peerConnections
|
||||
map.put(e.endpoint.destination, c)
|
||||
c.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||
def removed = peerConnections.remove(e.destination)
|
||||
if (removed == null)
|
||||
removed = leafConnections.remove(e.destination)
|
||||
if (removed == null)
|
||||
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
|
||||
}
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
super.shutdown()
|
||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
peerConnections.clear()
|
||||
leafConnections.clear()
|
||||
}
|
||||
|
||||
void forwardQueryToLeafs(QueryEvent e) {
|
||||
|
||||
}
|
||||
|
||||
void forwardQueryToLeafs(QueryEvent e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
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()
|
||||
}
|
||||
}
|
@ -21,5 +21,5 @@ class BadHashException extends Exception {
|
||||
public BadHashException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import com.muwire.core.util.DataUtil
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
@ -25,20 +26,22 @@ import com.muwire.core.UILoadedEvent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
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 File home
|
||||
private final Persona me
|
||||
|
||||
|
||||
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||
|
||||
|
||||
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
|
||||
I2PConnector connector, File home, Persona me) {
|
||||
this.eventBus = eventBus
|
||||
@ -46,12 +49,9 @@ public class DownloadManager {
|
||||
this.meshManager = meshManager
|
||||
this.muSettings = muSettings
|
||||
this.connector = connector
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
this.me = me
|
||||
|
||||
incompletes.mkdir()
|
||||
|
||||
|
||||
this.executor = Executors.newCachedThreadPool({ r ->
|
||||
Thread rv = new Thread(r)
|
||||
rv.setName("download-worker")
|
||||
@ -59,23 +59,28 @@ public class DownloadManager {
|
||||
rv
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
def size = e.result[0].size
|
||||
def infohash = e.result[0].infohash
|
||||
def pieceSize = e.result[0].pieceSize
|
||||
|
||||
|
||||
Set<Destination> destinations = new HashSet<>()
|
||||
e.result.each {
|
||||
e.result.each {
|
||||
destinations.add(it.sender.destination)
|
||||
}
|
||||
destinations.addAll(e.sources)
|
||||
destinations.remove(me.destination)
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize)
|
||||
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
@ -84,24 +89,24 @@ public class DownloadManager {
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
|
||||
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
|
||||
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
|
||||
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
|
||||
void resume(Downloader downloader) {
|
||||
executor.execute({downloader.download() as Runnable})
|
||||
}
|
||||
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
File downloadsFile = new File(home, "downloads.json")
|
||||
if (!downloadsFile.exists())
|
||||
@ -111,7 +116,7 @@ public class DownloadManager {
|
||||
def json = slurper.parseText(it)
|
||||
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
def destinations = new HashSet<>()
|
||||
json.destinations.each { destination ->
|
||||
json.destinations.each { destination ->
|
||||
destinations.add new Destination(destination)
|
||||
}
|
||||
InfoHash infoHash
|
||||
@ -123,29 +128,50 @@ public class DownloadManager {
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||
|
||||
boolean sequential = false
|
||||
if (json.sequential != null)
|
||||
sequential = json.sequential
|
||||
|
||||
File incompletes
|
||||
if (json.incompletes != null)
|
||||
incompletes = new File(DataUtil.readi18nString(Base64.decode(json.incompletes)))
|
||||
else
|
||||
incompletes = new File(home, "incompletes")
|
||||
|
||||
if (json.pieceSizePow2 == null || json.pieceSizePow2 == 0) {
|
||||
log.warning("Skipping $file because pieceSizePow2=$json.pieceSizePow2")
|
||||
return // skip this download as it's corrupt anyway
|
||||
}
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
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))
|
||||
|
||||
try {
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
downloaders.put(infoHash, downloader)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING,"cannot start downloader, skipping", bad)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||
int pieceSize = 0x1 << pieceSizePow2
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||
long pieceSize = 0x1L << pieceSizePow2
|
||||
int nPieces = (int)(length / pieceSize)
|
||||
if (length % pieceSize != 0)
|
||||
nPieces++
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
|
||||
mesh.pieces
|
||||
}
|
||||
|
||||
|
||||
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||
Downloader downloader = downloaders.get(e.infoHash)
|
||||
if (downloader == null)
|
||||
@ -156,19 +182,19 @@ public class DownloadManager {
|
||||
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.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
|
||||
private void persistDownloaders() {
|
||||
File downloadsFile = new File(home,"downloads.json")
|
||||
downloadsFile.withPrintWriter { writer ->
|
||||
downloadsFile.withPrintWriter { writer ->
|
||||
downloaders.values().each { downloader ->
|
||||
if (!downloader.cancelled) {
|
||||
def json = [:]
|
||||
@ -180,20 +206,25 @@ public class DownloadManager {
|
||||
destinations << it.toBase64()
|
||||
}
|
||||
json.destinations = destinations
|
||||
|
||||
|
||||
InfoHash infoHash = downloader.getInfoHash()
|
||||
if (infoHash.hashList != null)
|
||||
json.hashList = Base64.encode(infoHash.hashList)
|
||||
else
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
|
||||
|
||||
json.paused = downloader.paused
|
||||
|
||||
json.sequential = downloader.pieces.ratio == 0f
|
||||
|
||||
json.incompletes = Base64.encode(DataUtil.encodei18nString(downloader.incompletes.getAbsolutePath()))
|
||||
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void shutdown() {
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
|
@ -14,17 +14,19 @@ 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.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
class DownloadSession {
|
||||
|
||||
|
||||
private final EventBus eventBus
|
||||
private final String meB64
|
||||
private final Pieces pieces
|
||||
@ -36,13 +38,12 @@ class DownloadSession {
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
|
||||
private ByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
private final AtomicLong dataSinceLastRead
|
||||
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
@ -52,6 +53,7 @@ class DownloadSession {
|
||||
this.pieceSize = pieceSize
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
this.dataSinceLastRead = dataSinceLastRead
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@ -59,7 +61,7 @@ class DownloadSession {
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return if the request will proceed. The only time it may not
|
||||
* is if all the pieces have been claimed by other sessions.
|
||||
@ -68,44 +70,46 @@ class DownloadSession {
|
||||
public boolean request() throws IOException {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
|
||||
int piece
|
||||
|
||||
int[] pieceAndPosition
|
||||
if (available.isEmpty())
|
||||
piece = pieces.claim()
|
||||
pieceAndPosition = pieces.claim()
|
||||
else
|
||||
piece = pieces.claim(new HashSet<>(available))
|
||||
if (piece == -1)
|
||||
pieceAndPosition = pieces.claim(new HashSet<>(available))
|
||||
if (pieceAndPosition == null)
|
||||
return false
|
||||
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())
|
||||
|
||||
|
||||
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".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.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
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 false
|
||||
}
|
||||
|
||||
|
||||
if (!(code == 200 || code == 416)) {
|
||||
log.warning("unknown code $code")
|
||||
endpoint.close()
|
||||
@ -120,10 +124,10 @@ class DownloadSession {
|
||||
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)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
|
||||
|
||||
// prase X-Alt if present
|
||||
if (headers.containsKey("X-Alt")) {
|
||||
headers["X-Alt"].split(",").each {
|
||||
@ -136,8 +140,10 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
// parse X-Have if present
|
||||
if (headers.containsKey("X-Have")) {
|
||||
if (headers.containsKey("X-Have")) {
|
||||
DataUtil.decodeXHave(headers["X-Have"]).each {
|
||||
if (it >= pieces.nPieces)
|
||||
throw new IOException("Invalid X-Have header, available piece $it/$pieces.nPieces")
|
||||
available.add(it)
|
||||
}
|
||||
if (!available.contains(piece))
|
||||
@ -147,16 +153,16 @@ class DownloadSession {
|
||||
throw new IOException("Code $code but no X-Have")
|
||||
available.clear()
|
||||
}
|
||||
|
||||
|
||||
if (code != 200)
|
||||
return true
|
||||
|
||||
|
||||
String range = headers["Content-Range"]
|
||||
if (range == null)
|
||||
if (range == null)
|
||||
throw new IOException("Code 200 but no Content-Range")
|
||||
|
||||
|
||||
def group = (range =~ /^(\d+)-(\d+)$/)
|
||||
if (group.size() != 1)
|
||||
if (group.size() != 1)
|
||||
throw new IOException("invalid Content-Range header $range")
|
||||
|
||||
long receivedStart = Long.parseLong(group[0][1])
|
||||
@ -167,13 +173,14 @@ class DownloadSession {
|
||||
endpoint.close()
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// 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()) {
|
||||
@ -184,42 +191,36 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead += read
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
||||
mapped.clear()
|
||||
digest.update(mapped)
|
||||
DataUtil.tryUnmap(mapped)
|
||||
byte [] hash = digest.digest()
|
||||
byte [] expected = new byte[32]
|
||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||
if (hash != expected)
|
||||
throw new BadHashException()
|
||||
if (hash != expected) {
|
||||
pieces.markPartial(piece, 0)
|
||||
throw new BadHashException("bad hash on piece $piece")
|
||||
}
|
||||
} finally {
|
||||
try { channel?.close() } catch (IOException ignore) {}
|
||||
DataUtil.tryUnmap(mapped)
|
||||
}
|
||||
pieces.markDownloaded(piece)
|
||||
unclaim = false
|
||||
} finally {
|
||||
if (unclaim)
|
||||
if (unclaim && !steal)
|
||||
pieces.unclaim(piece)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
synchronized int positionInPiece() {
|
||||
if (mapped == null)
|
||||
return 0
|
||||
mapped.position()
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
@ -27,9 +28,10 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
public class Downloader {
|
||||
|
||||
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 ->
|
||||
Thread rv = new Thread(r)
|
||||
rv.setName("download worker")
|
||||
@ -38,8 +40,8 @@ public class Downloader {
|
||||
})
|
||||
|
||||
private final EventBus eventBus
|
||||
private final DownloadManager downloadManager
|
||||
private final Persona me
|
||||
private final DownloadManager downloadManager
|
||||
private final Persona me
|
||||
private final File file
|
||||
private final Pieces pieces
|
||||
private final long length
|
||||
@ -48,24 +50,26 @@ public class Downloader {
|
||||
private final I2PConnector connector
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File incompletes
|
||||
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, paused
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
|
||||
private volatile long lastSpeedRead = System.currentTimeMillis()
|
||||
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,
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||
File incompletes, Pieces pieces) {
|
||||
this.eventBus = eventBus
|
||||
@ -76,26 +80,23 @@ public class Downloader {
|
||||
this.length = length
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.incompletes = incompletes
|
||||
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
|
||||
|
||||
// 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() {
|
||||
infoHash
|
||||
}
|
||||
|
||||
|
||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||
this.infoHash = infoHash
|
||||
}
|
||||
|
||||
|
||||
void download() {
|
||||
readPieces()
|
||||
destinations.each {
|
||||
@ -106,40 +107,51 @@ public class Downloader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void readPieces() {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
pieces.markDownloaded(piece)
|
||||
piecesFile.eachLine {
|
||||
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 ->
|
||||
pieces.getDownloaded().each { piece ->
|
||||
writer.println(piece)
|
||||
}
|
||||
pieces.write(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public long donePieces() {
|
||||
pieces.donePieces()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public int speed() {
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
|
||||
speedArr.clear()
|
||||
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
|
||||
speedPos = 0
|
||||
}
|
||||
|
||||
// normalize to speedArr.size
|
||||
@ -164,15 +176,15 @@ public class Downloader {
|
||||
|
||||
speedAvg
|
||||
}
|
||||
|
||||
|
||||
public DownloadState getCurrentState() {
|
||||
if (cancelled)
|
||||
return DownloadState.CANCELLED
|
||||
if (paused)
|
||||
return DownloadState.PAUSED
|
||||
|
||||
|
||||
boolean allFinished = true
|
||||
activeWorkers.values().each {
|
||||
activeWorkers.values().each {
|
||||
allFinished &= it.currentState == WorkerState.FINISHED
|
||||
}
|
||||
if (allFinished) {
|
||||
@ -180,22 +192,22 @@ public class Downloader {
|
||||
return DownloadState.FINISHED
|
||||
return DownloadState.FAILED
|
||||
}
|
||||
|
||||
|
||||
// if at least one is downloading...
|
||||
boolean oneDownloading = false
|
||||
activeWorkers.values().each {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING) {
|
||||
oneDownloading = true
|
||||
return
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (oneDownloading)
|
||||
return DownloadState.DOWNLOADING
|
||||
|
||||
|
||||
// at least one is requesting hashlist
|
||||
boolean oneHashlist = false
|
||||
activeWorkers.values().each {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.HASHLIST) {
|
||||
oneHashlist = true
|
||||
return
|
||||
@ -203,10 +215,10 @@ public class Downloader {
|
||||
}
|
||||
if (oneHashlist)
|
||||
return DownloadState.HASHLIST
|
||||
|
||||
|
||||
return DownloadState.CONNECTING
|
||||
}
|
||||
|
||||
|
||||
public void cancel() {
|
||||
cancelled = true
|
||||
stop()
|
||||
@ -217,27 +229,27 @@ public class Downloader {
|
||||
incompleteFile.delete()
|
||||
pieces.clearAll()
|
||||
}
|
||||
|
||||
|
||||
public void pause() {
|
||||
paused = true
|
||||
stop()
|
||||
}
|
||||
|
||||
|
||||
void stop() {
|
||||
activeWorkers.values().each {
|
||||
activeWorkers.values().each {
|
||||
it.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int activeWorkers() {
|
||||
int active = 0
|
||||
activeWorkers.values().each {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState != WorkerState.FINISHED)
|
||||
active++
|
||||
}
|
||||
active
|
||||
}
|
||||
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
readPieces()
|
||||
@ -256,7 +268,7 @@ public class Downloader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void addSource(Destination d) {
|
||||
if (activeWorkers.containsKey(d))
|
||||
return
|
||||
@ -264,7 +276,7 @@ public class Downloader {
|
||||
activeWorkers.put(d, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
}
|
||||
|
||||
|
||||
class DownloadWorker implements Runnable {
|
||||
private final Destination destination
|
||||
private volatile WorkerState currentState
|
||||
@ -272,11 +284,11 @@ public class Downloader {
|
||||
private Endpoint endpoint
|
||||
private volatile DownloadSession currentSession
|
||||
private final Set<Integer> available = new HashSet<>()
|
||||
|
||||
|
||||
DownloadWorker(Destination destination) {
|
||||
this.destination = destination
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
downloadThread = Thread.currentThread()
|
||||
currentState = WorkerState.CONNECTING
|
||||
@ -292,8 +304,8 @@ public class Downloader {
|
||||
currentState = WorkerState.DOWNLOADING
|
||||
boolean requestPerformed
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available)
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@ -303,12 +315,17 @@ public class Downloader {
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||
} finally {
|
||||
writePieces()
|
||||
currentState = WorkerState.FINISHED
|
||||
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
}
|
||||
activeWorkers.values().each {
|
||||
if (it.destination != destination)
|
||||
it.cancel()
|
||||
}
|
||||
try {
|
||||
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
@ -317,20 +334,14 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
endpoint?.close()
|
||||
}
|
||||
}
|
||||
|
||||
int speed() {
|
||||
if (currentSession == null)
|
||||
return 0
|
||||
currentSession.speed()
|
||||
}
|
||||
|
||||
|
||||
void cancel() {
|
||||
downloadThread?.interrupt()
|
||||
}
|
||||
|
@ -20,32 +20,32 @@ class HashListSession {
|
||||
private final String meB64
|
||||
private final InfoHash infoHash
|
||||
private final Endpoint endpoint
|
||||
|
||||
|
||||
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||
this.meB64 = meB64
|
||||
this.infoHash = infoHash
|
||||
this.endpoint = endpoint
|
||||
}
|
||||
|
||||
|
||||
InfoHash request() throws IOException {
|
||||
InputStream is = endpoint.getInputStream()
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
|
||||
|
||||
String root = Base64.encode(infoHash.getRoot())
|
||||
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
|
||||
String code = readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("unknown code $code")
|
||||
|
||||
|
||||
// parse all headers
|
||||
Set<String> headers = new HashSet<>()
|
||||
String header
|
||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||
headers.add(header)
|
||||
|
||||
|
||||
long receivedStart = -1
|
||||
long receivedEnd = -1
|
||||
for (String receivedHeader : headers) {
|
||||
@ -58,10 +58,10 @@ class HashListSession {
|
||||
receivedStart = Long.parseLong(group[0][1])
|
||||
receivedEnd = Long.parseLong(group[0][2])
|
||||
}
|
||||
|
||||
|
||||
if (receivedStart != 0)
|
||||
throw new IOException("hashlist started at $receivedStart")
|
||||
|
||||
|
||||
byte[] hashList = new byte[receivedEnd]
|
||||
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
@ -73,7 +73,7 @@ class HashListSession {
|
||||
throw new IOException()
|
||||
hashListBuf.put(tmp, 0, read)
|
||||
}
|
||||
|
||||
|
||||
InfoHash received = InfoHash.fromHashList(hashList)
|
||||
if (received.getRoot() != infoHash.getRoot())
|
||||
throw new IOException("fetched list doesn't match root")
|
||||
|
@ -5,11 +5,12 @@ class Pieces {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Pieces(int nPieces, float ratio) {
|
||||
this.nPieces = nPieces
|
||||
this.ratio = ratio
|
||||
@ -17,39 +18,54 @@ class Pieces {
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
synchronized int claim() {
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces)
|
||||
return -1
|
||||
|
||||
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 * claimedCardinality) / nPieces > ratio) {
|
||||
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
|
||||
int rv = claimed.nextClearBit(0)
|
||||
claimed.set(rv)
|
||||
return rv
|
||||
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
|
||||
while(true) {
|
||||
int start = random.nextInt(nPieces)
|
||||
if (claimed.get(start))
|
||||
continue
|
||||
claimed.set(start)
|
||||
return start
|
||||
return [start, partials.getOrDefault(start,0), 0]
|
||||
}
|
||||
}
|
||||
|
||||
synchronized int claim(Set<Integer> available) {
|
||||
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||
|
||||
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 -1
|
||||
List<Integer> toList = available.toList()
|
||||
Collections.shuffle(toList)
|
||||
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()
|
||||
if (ratio > 0f)
|
||||
Collections.shuffle(toList)
|
||||
int rv = toList[0]
|
||||
claimed.set(rv)
|
||||
rv
|
||||
[rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
|
||||
synchronized def getDownloaded() {
|
||||
def rv = []
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
@ -57,30 +73,47 @@ class Pieces {
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
|
||||
synchronized void markDownloaded(int piece) {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
|
||||
synchronized void markPartial(int piece, int position) {
|
||||
partials.put(piece, position)
|
||||
}
|
||||
|
||||
synchronized void unclaim(int piece) {
|
||||
claimed.clear(piece)
|
||||
}
|
||||
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
done.cardinality() == nPieces
|
||||
}
|
||||
|
||||
|
||||
synchronized int donePieces() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ import com.muwire.core.search.UIResultEvent
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class UIDownloadEvent extends Event {
|
||||
|
||||
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
||||
|
119
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal file
119
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal file
@ -0,0 +1,119 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.Name
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.data.SigningPublicKey
|
||||
|
||||
class Certificate {
|
||||
private final byte version
|
||||
private final InfoHash infoHash
|
||||
private final Name name
|
||||
private final long timestamp
|
||||
private final Persona issuer
|
||||
private final byte[] sig
|
||||
|
||||
private volatile byte [] payload
|
||||
|
||||
Certificate(InputStream is) {
|
||||
version = (byte) (is.read() & 0xFF)
|
||||
if (version != Constants.FILE_CERT_VERSION)
|
||||
throw new IOException("Unknown version $version")
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
timestamp = dis.readLong()
|
||||
|
||||
byte [] root = new byte[InfoHash.SIZE]
|
||||
dis.readFully(root)
|
||||
infoHash = new InfoHash(root)
|
||||
|
||||
name = new Name(dis)
|
||||
|
||||
issuer = new Persona(is)
|
||||
|
||||
sig = new byte[Constants.SIG_TYPE.getSigLen()]
|
||||
dis.readFully(sig)
|
||||
|
||||
if (!verify(version, infoHash, name, timestamp, issuer, sig))
|
||||
throw new InvalidSignatureException("certificate for $name.name from ${issuer.getHumanReadableName()} didn't verify")
|
||||
}
|
||||
|
||||
Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, SigningPrivateKey spk) {
|
||||
this.version = Constants.FILE_CERT_VERSION
|
||||
this.infoHash = infoHash
|
||||
this.name = new Name(name)
|
||||
this.timestamp = timestamp
|
||||
this.issuer = issuer
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
this.name.write(daos)
|
||||
issuer.write(daos)
|
||||
daos.close()
|
||||
|
||||
byte[] payload = baos.toByteArray()
|
||||
Signature signature = DSAEngine.getInstance().sign(payload, spk)
|
||||
this.sig = signature.getData()
|
||||
}
|
||||
|
||||
private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, byte[] sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
name.write(daos)
|
||||
issuer.write(daos)
|
||||
daos.close()
|
||||
|
||||
byte [] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
public void write(OutputStream os) {
|
||||
if (payload == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
name.write(daos)
|
||||
issuer.write(daos)
|
||||
daos.write(sig)
|
||||
daos.close()
|
||||
|
||||
payload = baos.toByteArray()
|
||||
}
|
||||
os.write(payload)
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Certificate))
|
||||
return false
|
||||
Certificate other = (Certificate)o
|
||||
|
||||
version == other.version &&
|
||||
infoHash == other.infoHash &&
|
||||
timestamp == other.timestamp &&
|
||||
name == other.name &&
|
||||
issuer == other.issuer
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class CertificateClient {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector connector
|
||||
|
||||
private final ExecutorService fetcherThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
CertificateClient(EventBus eventBus, I2PConnector connector) {
|
||||
this.eventBus = eventBus
|
||||
this.connector = connector
|
||||
}
|
||||
|
||||
void onUIFetchCertificatesEvent(UIFetchCertificatesEvent e) {
|
||||
fetcherThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
|
||||
String infoHashString = Base64.encode(e.infoHash.getRoot())
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("CERTIFICATES ${infoHashString}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("invalid code $code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.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()
|
||||
}
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
||||
int count = Integer.parseInt(headers['Count'])
|
||||
|
||||
// start pulling the certs
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count))
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
for (int i = 0; i < count; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
Certificate cert = null
|
||||
try {
|
||||
cert = new Certificate(new ByteArrayInputStream(tmp))
|
||||
} catch (IOException | InvalidSignatureException ignore) {
|
||||
continue
|
||||
}
|
||||
if (cert.infoHash == e.infoHash)
|
||||
eventBus.publish(new CertificateFetchedEvent(certificate : cert))
|
||||
}
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Fetching certificates failed", bad)
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateCreatedEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateFetchEvent extends Event {
|
||||
CertificateFetchStatus status
|
||||
int count
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.muwire.core.filecert;
|
||||
|
||||
public enum CertificateFetchStatus {
|
||||
CONNECTING, FETCHING, DONE, FAILED
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateFetchedEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.Name
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class CertificateManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final File certDir
|
||||
private final Persona me
|
||||
private final SigningPrivateKey spk
|
||||
|
||||
final Map<InfoHash, Set<Certificate>> byInfoHash = new ConcurrentHashMap()
|
||||
final Map<Persona, Set<Certificate>> byIssuer = new ConcurrentHashMap()
|
||||
|
||||
CertificateManager(EventBus eventBus, File home, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
this.certDir = new File(home, "filecerts")
|
||||
if (!certDir.exists())
|
||||
certDir.mkdirs()
|
||||
else
|
||||
loadCertificates()
|
||||
}
|
||||
|
||||
private void loadCertificates() {
|
||||
certDir.listFiles({ dir, name ->
|
||||
name.endsWith("mwcert")
|
||||
} as FilenameFilter).each { certFile ->
|
||||
Certificate cert = null
|
||||
try {
|
||||
certFile.withInputStream {
|
||||
cert = new Certificate(it)
|
||||
}
|
||||
} catch (IOException | InvalidSignatureException ignore) {
|
||||
log.log(Level.WARNING, "Certificate failed to load from $certFile", ignore)
|
||||
return
|
||||
}
|
||||
|
||||
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byInfoHash.put(cert.infoHash, existing)
|
||||
}
|
||||
existing.add(cert)
|
||||
|
||||
existing = byIssuer.get(cert.issuer)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byIssuer.put(cert.issuer, existing)
|
||||
}
|
||||
existing.add(cert)
|
||||
|
||||
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
|
||||
}
|
||||
}
|
||||
|
||||
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
||||
InfoHash infoHash = e.sharedFile.getInfoHash()
|
||||
String name = e.sharedFile.getFile().getName()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
Certificate cert = new Certificate(infoHash, name, timestamp, me, spk)
|
||||
|
||||
|
||||
if (addToMaps(cert)) {
|
||||
saveCert(cert)
|
||||
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
|
||||
}
|
||||
}
|
||||
|
||||
void onUIImportCertificateEvent(UIImportCertificateEvent e) {
|
||||
Certificate cert = e.certificate
|
||||
if (!addToMaps(cert))
|
||||
return
|
||||
saveCert(cert)
|
||||
}
|
||||
|
||||
private void saveCert(Certificate cert) {
|
||||
String infoHashString = Base64.encode(cert.infoHash.getRoot())
|
||||
File certFile = new File(certDir, "${infoHashString}_${cert.issuer.getHumanReadableName()}.mwcert")
|
||||
certFile.withOutputStream { cert.write(it) }
|
||||
}
|
||||
|
||||
private boolean addToMaps(Certificate cert) {
|
||||
boolean added = true
|
||||
|
||||
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byInfoHash.put(cert.infoHash, existing)
|
||||
}
|
||||
added &= existing.add(cert)
|
||||
|
||||
existing = byIssuer.get(cert.issuer)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byIssuer.put(cert.issuer, existing)
|
||||
}
|
||||
added &= existing.add(cert)
|
||||
added
|
||||
}
|
||||
|
||||
boolean hasLocalCertificate(InfoHash infoHash) {
|
||||
if (!byInfoHash.containsKey(infoHash))
|
||||
return false
|
||||
Set<Certificate> set = byInfoHash.get(infoHash)
|
||||
for (Certificate cert : set) {
|
||||
if (cert.issuer == me)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Set<Certificate> getByInfoHash(InfoHash infoHash) {
|
||||
Set<Certificate> rv = new HashSet<>()
|
||||
if (byInfoHash.containsKey(infoHash))
|
||||
rv.addAll(byInfoHash.get(infoHash))
|
||||
rv
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UICreateCertificateEvent extends Event {
|
||||
SharedFile sharedFile
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFetchCertificatesEvent extends Event {
|
||||
Persona host
|
||||
InfoHash infoHash
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIImportCertificateEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@ -4,4 +4,8 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DirectoryWatchedEvent extends Event {
|
||||
File directory
|
||||
}
|
@ -13,6 +13,7 @@ import java.nio.file.WatchService
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import groovy.util.logging.Log
|
||||
@ -20,9 +21,9 @@ 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())
|
||||
@ -30,7 +31,9 @@ class DirectoryWatcher {
|
||||
else
|
||||
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||
}
|
||||
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
@ -38,8 +41,10 @@ class DirectoryWatcher {
|
||||
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
@ -47,32 +52,43 @@ class DirectoryWatcher {
|
||||
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()
|
||||
|
||||
void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
|
||||
File canonical = e.directory.getCanonicalFile()
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.file, wk)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
})
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
@ -93,7 +109,7 @@ class DirectoryWatcher {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void processCreated(Path parent, Path path) {
|
||||
File f= join(parent, path)
|
||||
@ -103,26 +119,27 @@ class DirectoryWatcher {
|
||||
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())
|
||||
if (!fileManager.getNegativeTree().fileToNode.containsKey(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))
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
|
@ -8,5 +8,5 @@ import net.i2p.data.Destination
|
||||
|
||||
class FileDownloadedEvent extends Event {
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
DownloadedFile downloadedFile
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashedEvent extends Event {
|
||||
|
||||
SharedFile sharedFile
|
||||
String error
|
||||
|
||||
SharedFile sharedFile
|
||||
String error
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -13,75 +13,78 @@ import java.security.NoSuchAlgorithmException
|
||||
|
||||
class FileHasher {
|
||||
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << 37
|
||||
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
* @return the size of each piece in power of 2
|
||||
public static final int MIN_PIECE_SIZE_POW2 = 17
|
||||
public static final int MAX_PIECE_SIZE_POW2 = 37
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << MAX_PIECE_SIZE_POW2
|
||||
|
||||
/**
|
||||
* @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)
|
||||
return 17
|
||||
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-13
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("File too large $size")
|
||||
}
|
||||
|
||||
final MessageDigest digest
|
||||
|
||||
FileHasher() {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
digest = null
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final int size = 0x1 << getPieceSize(length)
|
||||
int numPieces = (int) (length / size)
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
try {
|
||||
MappedByteBuffer buf
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
digest.update buf
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 30)
|
||||
return MIN_PIECE_SIZE_POW2
|
||||
|
||||
for (int i = 31; i <= MAX_PIECE_SIZE_POW2; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-13
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("File too large $size")
|
||||
}
|
||||
|
||||
final MessageDigest digest
|
||||
|
||||
FileHasher() {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
digest = null
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final long size = 0x1L << getPieceSize(length)
|
||||
int numPieces = (length / size).toInteger()
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
MappedByteBuffer buf = null
|
||||
try {
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size.toInteger())
|
||||
digest.update buf
|
||||
DataUtil.tryUnmap(buf)
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
} finally {
|
||||
raf.close()
|
||||
}
|
||||
|
||||
byte [] hashList = output.toByteArray()
|
||||
InfoHash.fromHashList(hashList)
|
||||
}
|
||||
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
long lastPieceLength = length - (numPieces - 1) * size
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength.toInteger())
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
} finally {
|
||||
raf.close()
|
||||
DataUtil.tryUnmap(buf)
|
||||
}
|
||||
|
||||
byte [] hashList = output.toByteArray()
|
||||
InfoHash.fromHashList(hashList)
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
println "This utility computes an infohash of a file"
|
||||
println "Pass absolute path to a file as an argument"
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
|
||||
def file = new File(args[0])
|
||||
file = file.getAbsoluteFile()
|
||||
def hasher = new FileHasher()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -5,5 +5,5 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class FileLoadedEvent extends Event {
|
||||
|
||||
SharedFile loadedFile
|
||||
SharedFile loadedFile
|
||||
}
|
||||
|
@ -8,128 +8,227 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchIndex
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class FileManager {
|
||||
|
||||
|
||||
final EventBus eventBus
|
||||
final EventBus eventBus
|
||||
final MuWireSettings settings
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree negativeTree = new FileTree()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
this.settings = settings
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
this.eventBus = eventBus
|
||||
|
||||
for (String negative : settings.negativeFileTree) {
|
||||
negativeTree.add(new File(negative))
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
addToIndex(e.loadedFile)
|
||||
}
|
||||
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile == null)
|
||||
return
|
||||
File f = e.sharedFile.getFile()
|
||||
if (sideCarFiles.remove(f)) {
|
||||
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
|
||||
if (sideCar.exists())
|
||||
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
|
||||
}
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
addToIndex(e.loadedFile)
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (settings.shareDownloadedFiles) {
|
||||
addToIndex(e.downloadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
|
||||
void onSideCarFileEvent(SideCarFileEvent e) {
|
||||
String name = e.file.getName()
|
||||
name = name.substring(0, name.length() - ".mwcomment".length())
|
||||
File target = new File(e.file.getParentFile(), name)
|
||||
SharedFile existing = fileToSharedFile.get(target)
|
||||
if (existing == null) {
|
||||
sideCarFiles.add(target)
|
||||
return
|
||||
}
|
||||
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
|
||||
String oldComment = existing.getComment()
|
||||
existing.setComment(comment)
|
||||
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
log.info("adding new root")
|
||||
existing = new HashSet<>()
|
||||
rootToFiles.put(infoHash, existing);
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles == null) {
|
||||
existingFiles = new HashSet<>()
|
||||
nameToFiles.put(name, existingFiles)
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
|
||||
index.add(name)
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
if (existing.isEmpty()) {
|
||||
rootToFiles.remove(infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles != null) {
|
||||
existingFiles.remove(sf.file)
|
||||
if (existingFiles.isEmpty()) {
|
||||
nameToFiles.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
index.remove(name)
|
||||
}
|
||||
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
existing = new HashSet<>()
|
||||
rootToFiles.put(infoHash, existing);
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
||||
negativeTree.add(sf.file.getParentFile())
|
||||
}
|
||||
saveNegativeTree()
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles == null) {
|
||||
existingFiles = new HashSet<>()
|
||||
nameToFiles.put(name, existingFiles)
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(sf.getFile())
|
||||
}
|
||||
|
||||
index.add(name)
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
if (existing.isEmpty()) {
|
||||
rootToFiles.remove(infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file)
|
||||
saveNegativeTree()
|
||||
}
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
if (existingFiles != null) {
|
||||
existingFiles.remove(sf.file)
|
||||
if (existingFiles.isEmpty()) {
|
||||
nameToFiles.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
if (existingComment.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index.remove(name)
|
||||
}
|
||||
|
||||
void onUICommentEvent(UICommentEvent e) {
|
||||
if (e.oldComment != null) {
|
||||
def comment = DataUtil.readi18nString(Base64.decode(e.oldComment))
|
||||
Set<File> existingFiles = commentToFile.get(comment)
|
||||
existingFiles.remove(e.sharedFile.getFile())
|
||||
if (existingFiles.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
|
||||
String comment = e.sharedFile.getComment()
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
if (comment != null) {
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(e.sharedFile.getFile())
|
||||
}
|
||||
}
|
||||
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
synchronized(fileToSharedFile) {
|
||||
return new HashMap<>(fileToSharedFile)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Set<SharedFile> getSharedFiles(byte []root) {
|
||||
return rootToFiles.get(new InfoHash(root))
|
||||
}
|
||||
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
ResultsEvent re = null
|
||||
if (e.searchHash != null) {
|
||||
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
ResultsEvent re = null
|
||||
if (e.searchHash != null) {
|
||||
Set<SharedFile> found
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
if (found != null && !found.isEmpty()) {
|
||||
found.each { it.hit() }
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each {
|
||||
files.addAll nameToFiles.getOrDefault(it, [])
|
||||
if (e.searchComments)
|
||||
files.addAll commentToFile.getOrDefault(it, [])
|
||||
}
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
|
||||
}
|
||||
|
||||
if (re != null)
|
||||
eventBus.publish(re)
|
||||
}
|
||||
|
||||
|
||||
if (!sharedFiles.isEmpty()) {
|
||||
sharedFiles.each { it.hit() }
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (re != null)
|
||||
eventBus.publish(re)
|
||||
}
|
||||
|
||||
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
|
||||
if (!oob)
|
||||
return files
|
||||
Set<SharedFile> rv = new HashSet<>()
|
||||
files.each {
|
||||
files.each {
|
||||
if (it.getPieceSize() != 0)
|
||||
rv.add(it)
|
||||
}
|
||||
@ -137,7 +236,9 @@ class FileManager {
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
e.directory.listFiles().each {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
@ -147,4 +248,9 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveNegativeTree() {
|
||||
settings.negativeFileTree.clear()
|
||||
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import com.muwire.core.Event
|
||||
|
||||
class FileSharedEvent extends Event {
|
||||
|
||||
File file
|
||||
|
||||
File file
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
|
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
64
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
@ -0,0 +1,64 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FileTree {
|
||||
|
||||
private final TreeNode root = new TreeNode()
|
||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||
|
||||
void add(File file) {
|
||||
List<File> path = new ArrayList<>()
|
||||
path.add(file)
|
||||
while (file.getParentFile() != null) {
|
||||
path.add(file.getParentFile())
|
||||
file = file.getParentFile()
|
||||
}
|
||||
|
||||
Collections.reverse(path)
|
||||
|
||||
TreeNode current = root
|
||||
for (File element : path) {
|
||||
TreeNode existing = fileToNode.get(element)
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
}
|
||||
current = existing
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(File file) {
|
||||
TreeNode node = fileToNode.remove(file)
|
||||
if (node == null) {
|
||||
return false
|
||||
}
|
||||
node.parent.children.remove(node)
|
||||
if (node.parent.children.isEmpty() && node.parent != root)
|
||||
remove(node.parent.file)
|
||||
def copy = new ArrayList(node.children)
|
||||
for (TreeNode child : copy)
|
||||
remove(child.file)
|
||||
true
|
||||
}
|
||||
|
||||
public static class TreeNode {
|
||||
TreeNode parent
|
||||
File file
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
public int hashCode() {
|
||||
file.hashCode()
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TreeNode))
|
||||
return false
|
||||
TreeNode other = (TreeNode)o
|
||||
file == other.file
|
||||
}
|
||||
}
|
||||
}
|
@ -4,5 +4,6 @@ import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileUnsharedEvent extends Event {
|
||||
SharedFile unsharedFile
|
||||
SharedFile unsharedFile
|
||||
boolean deleted
|
||||
}
|
||||
|
@ -3,45 +3,70 @@ package com.muwire.core.files
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class HasherService {
|
||||
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileManager fileManager
|
||||
Executor executor
|
||||
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
final Set<File> hashed = new HashSet<>()
|
||||
final MuWireSettings settings
|
||||
Executor executor
|
||||
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
|
||||
void start() {
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void start() {
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
File canonical = evt.file.getCanonicalFile()
|
||||
if (!settings.shareHiddenFiles && canonical.isHidden())
|
||||
return
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
}
|
||||
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
if (f.isDirectory()) {
|
||||
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 {
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileManager.fileToSharedFile.containsKey(canonical))
|
||||
return
|
||||
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
|
||||
return
|
||||
if (canonical.getName().endsWith(".mwcomment")) {
|
||||
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
|
||||
eventBus.publish(new SideCarFileEvent(file : canonical))
|
||||
} else if (hashed.add(canonical))
|
||||
executor.execute( { -> process(canonical) } as Runnable)
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent evt) {
|
||||
hashed.remove(evt.unsharedFile.file)
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent evt) {
|
||||
hashed.remove(evt.directory)
|
||||
}
|
||||
|
||||
private void process(File f) {
|
||||
if (f.isDirectory()) {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : f))
|
||||
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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ package com.muwire.core.files
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
@ -23,135 +26,156 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
class PersisterService extends Service {
|
||||
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
this.fileManager = fileManager
|
||||
timer = new Timer("file persister", true)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
this.fileManager = fileManager
|
||||
timer = new Timer("file persister timer", true)
|
||||
}
|
||||
|
||||
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()
|
||||
try {
|
||||
location.eachLine {
|
||||
if (it.trim().length() > 0) {
|
||||
def parsed = slurper.parseText it
|
||||
def event = fromJson parsed
|
||||
if (event != null) {
|
||||
|
||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
||||
persistFiles()
|
||||
}
|
||||
|
||||
void load() {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
|
||||
if (location.exists() && location.isFile()) {
|
||||
int loaded = 0
|
||||
def slurper = new JsonSlurper()
|
||||
try {
|
||||
location.eachLine {
|
||||
if (it.trim().length() > 0) {
|
||||
def parsed = slurper.parseText it
|
||||
def event = fromJson parsed
|
||||
if (event != null) {
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
listener.publish event
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
tmp.deleteOnExit()
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
||||
json.length = f.length()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = Base64.encode ih.getRoot()
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
int hits = 0
|
||||
if (json.hits != null)
|
||||
hits = json.hits
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
sf.hits = hits
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
persisterExecutor.submit( {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
tmp.deleteOnExit()
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
byte [] tmp = new byte [32]
|
||||
json.hashList = []
|
||||
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
||||
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
|
||||
json.hashList.add Base64.encode(tmp)
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class SideCarFileEvent extends Event {
|
||||
File file
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UICommentEvent extends Event {
|
||||
SharedFile sharedFile
|
||||
String oldComment
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user