Compare commits
199 Commits
muwire-0.4
...
muwire-0.5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
26
README.md
26
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.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.5.3 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,17 +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 `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.
|
||||
|
||||
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.41'
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
23
cli-lanterna/build.gradle
Normal file
23
cli-lanterna/build.gradle
Normal file
@@ -0,0 +1,23 @@
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
compile 'com.googlecode.lanterna:lanterna:3.0.1'
|
||||
}
|
||||
|
@@ -0,0 +1,65 @@
|
||||
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.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.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()
|
||||
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,182 @@
|
||||
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.4"
|
||||
|
||||
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.withOutputStream {
|
||||
props.write(it)
|
||||
}
|
||||
} else {
|
||||
props = new Properties()
|
||||
propsFile.withInputStream {
|
||||
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, "") }
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
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,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,90 @@
|
||||
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 List<Downloader> downloaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
|
||||
|
||||
|
||||
private long lastRetryTime
|
||||
|
||||
DownloadsModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
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) }
|
||||
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,78 @@
|
||||
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])
|
||||
|
||||
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, 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 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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
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.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")
|
||||
|
||||
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
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
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.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")
|
||||
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(3))
|
||||
|
||||
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 closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(unshareButton, layoutData)
|
||||
contentPanel.addComponent(addCommentButton, 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,272 @@
|
||||
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
|
||||
|
||||
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 updateStatus
|
||||
|
||||
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen) {
|
||||
super(title);
|
||||
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.screen = screen
|
||||
|
||||
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core)
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core)
|
||||
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")
|
||||
updateStatus = new Label("Unknown")
|
||||
|
||||
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("Update Status: "), layoutData)
|
||||
addComponent(updateStatus, 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 from $e.signer, search for $e.infoHash"
|
||||
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version downloaded"
|
||||
updateStatus.setText(label)
|
||||
String message = "Version $e.version from $e.signer has been downloaded. You can update now."
|
||||
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
@@ -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")
|
||||
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)
|
||||
rootToResult.put(infoHash, it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.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")
|
||||
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])
|
||||
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(rows[2])})
|
||||
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(downloadButton, layoutData)
|
||||
contentPanel.addComponent(viewButton, layoutData)
|
||||
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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
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.data.Base64
|
||||
|
||||
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
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||
} else {
|
||||
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
def nonEmpty = []
|
||||
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
}
|
||||
|
||||
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.withOutputStream { core.muOptions.write(it) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
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 final List<Uploader> uploaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces")
|
||||
|
||||
UploadsModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
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({uploaders.add(e.uploader)})
|
||||
}
|
||||
|
||||
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||
guiThread.invokeLater({uploaders.remove(e.uploader)})
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int uploadersSize = model.getRowCount()
|
||||
uploadersSize.times { model.removeRow(0) }
|
||||
|
||||
uploaders.each {
|
||||
String name = it.getName()
|
||||
int percent = it.getProgress()
|
||||
String percentString = "$percent% of piece".toString()
|
||||
String downloader = it.getDownloader()
|
||||
|
||||
int pieces = it.getTotalPieces()
|
||||
int done = it.getDonePieces()
|
||||
int percentTotal = -1
|
||||
if (pieces != 0)
|
||||
percentTotal = (done * 100) / pieces
|
||||
long size = it.getTotalSize()
|
||||
String totalSize = ""
|
||||
if (size > 0)
|
||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
|
||||
|
||||
model.addRow([name, percentString, downloader, remotePieces])
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.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")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.9")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.9")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -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.41'
|
||||
compile 'net.i2p.client:mstreaming:0.9.41'
|
||||
compile 'net.i2p.client:streaming:0.9.41'
|
||||
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 = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||
}
|
@@ -28,6 +28,8 @@ 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.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatcher
|
||||
@@ -35,11 +37,13 @@ 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
|
||||
@@ -135,6 +139,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))
|
||||
@@ -214,14 +219,16 @@ public class Core {
|
||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||
eventBus.register(UICommentEvent.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)
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@@ -250,7 +257,7 @@ public class Core {
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
@@ -276,17 +283,19 @@ public class Core {
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
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)
|
||||
@@ -297,6 +306,11 @@ public class Core {
|
||||
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() {
|
||||
@@ -361,7 +375,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.4.9")
|
||||
Core core = new Core(props, home, "0.5.3")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -6,11 +6,13 @@ 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
|
||||
@@ -20,12 +22,17 @@ class MuWireSettings {
|
||||
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
|
||||
Set<String> watchedKeywords
|
||||
@@ -38,23 +45,33 @@ class MuWireSettings {
|
||||
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"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
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"))
|
||||
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"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
|
||||
watchedDirectories = readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||
@@ -74,22 +91,31 @@ class MuWireSettings {
|
||||
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("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))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
|
||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
@@ -106,7 +132,7 @@ class MuWireSettings {
|
||||
}
|
||||
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new HashSet<>()
|
||||
Set<String> rv = new ConcurrentHashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
|
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
||||
|
||||
}
|
@@ -132,6 +132,8 @@ 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()
|
||||
@@ -209,11 +211,19 @@ abstract class Connection implements Closeable {
|
||||
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
|
||||
|
||||
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,
|
||||
|
@@ -5,11 +5,15 @@ 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.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
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 +21,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,6 +30,7 @@ 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 {
|
||||
@@ -37,6 +43,7 @@ class ConnectionAcceptor {
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
@@ -47,7 +54,7 @@ class ConnectionAcceptor {
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionEstablisher establisher) {
|
||||
FileManager fileManager, ConnectionEstablisher establisher) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
@@ -55,6 +62,7 @@ class ConnectionAcceptor {
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.searchManager = searchManager
|
||||
this.fileManager = fileManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
|
||||
@@ -126,9 +134,15 @@ class ConnectionAcceptor {
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
case (byte)'R':
|
||||
processRESULTS(e)
|
||||
break
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
case (byte)'B':
|
||||
processBROWSE(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
@@ -229,7 +243,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++) {
|
||||
@@ -246,44 +260,147 @@ class ConnectionAcceptor {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
def obj = ResultsSender.sharedFileToObj(it, false)
|
||||
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) {
|
||||
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
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ class ConnectionEstablisher {
|
||||
final HostCache hostCache
|
||||
|
||||
final Timer timer
|
||||
final ExecutorService executor
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
@@ -51,6 +51,8 @@ class ConnectionEstablisher {
|
||||
rv.setName("connector-${System.currentTimeMillis()}")
|
||||
rv
|
||||
} as ThreadFactory)
|
||||
|
||||
closer = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void start() {
|
||||
@@ -60,6 +62,7 @@ class ConnectionEstablisher {
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
@@ -120,8 +123,12 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
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) {
|
||||
@@ -175,7 +182,7 @@ class ConnectionEstablisher {
|
||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||
} finally {
|
||||
// the end
|
||||
e.close()
|
||||
closer.execute({e.close()} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -34,7 +34,7 @@ public class DownloadManager {
|
||||
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<>()
|
||||
@@ -46,12 +46,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")
|
||||
@@ -63,6 +60,11 @@ public class DownloadManager {
|
||||
|
||||
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
|
||||
@@ -74,7 +76,7 @@ public class DownloadManager {
|
||||
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,
|
||||
@@ -122,8 +124,18 @@ public class DownloadManager {
|
||||
byte [] root = Base64.decode(json.hashRoot)
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||
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)
|
||||
@@ -137,12 +149,12 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||
int pieceSize = 0x1 << 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
|
||||
}
|
||||
|
||||
@@ -188,6 +200,11 @@ public class DownloadManager {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ 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}
|
||||
|
||||
@@ -48,6 +49,7 @@ 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
|
||||
@@ -76,16 +78,13 @@ 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() {
|
||||
@@ -145,6 +144,12 @@ public class Downloader {
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
}
|
||||
|
||||
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
|
||||
speedArr.clear()
|
||||
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
|
||||
speedPos = 0
|
||||
}
|
||||
|
||||
// normalize to speedArr.size
|
||||
currSpeed /= speedArr.size()
|
||||
@@ -326,7 +331,7 @@ 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))
|
||||
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ class Pieces {
|
||||
done = new BitSet(nPieces)
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces) {
|
||||
@@ -30,7 +30,7 @@ class Pieces {
|
||||
}
|
||||
|
||||
// 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, partials.getOrDefault(rv, 0), 0]
|
||||
@@ -59,7 +59,8 @@ class Pieces {
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
List<Integer> toList = availableCopy.toList()
|
||||
Collections.shuffle(toList)
|
||||
if (ratio > 0f)
|
||||
Collections.shuffle(toList)
|
||||
int rv = toList[0]
|
||||
claimed.set(rv)
|
||||
[rv, partials.getOrDefault(rv, 0), 0]
|
||||
|
@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -31,6 +32,8 @@ class DirectoryWatcher {
|
||||
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
|
||||
@@ -39,7 +42,9 @@ class DirectoryWatcher {
|
||||
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")
|
||||
@@ -64,15 +69,28 @@ class DirectoryWatcher {
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
Path path = e.file.getCanonicalFile().toPath()
|
||||
File canonical = e.file.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.withOutputStream {
|
||||
muOptions.write(it)
|
||||
}
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
|
@@ -8,8 +8,10 @@ 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 {
|
||||
@@ -20,6 +22,7 @@ class FileManager {
|
||||
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()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@@ -62,6 +65,18 @@ class FileManager {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -86,9 +101,45 @@ class FileManager {
|
||||
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) {
|
||||
@@ -112,10 +163,15 @@ class FileManager {
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
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)
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class HasherService {
|
||||
@@ -11,12 +12,15 @@ class HasherService {
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileManager fileManager
|
||||
final Set<File> hashed = new HashSet<>()
|
||||
final MuWireSettings settings
|
||||
Executor executor
|
||||
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void start() {
|
||||
@@ -24,13 +28,24 @@ class HasherService {
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||
File canonical = evt.file.getCanonicalFile()
|
||||
if (!settings.shareHiddenFiles && canonical.isHidden())
|
||||
return
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
if (fileManager.fileToSharedFile.containsKey(canonical))
|
||||
return
|
||||
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) {
|
||||
f = f.getCanonicalFile()
|
||||
if (f.isDirectory()) {
|
||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||
} else {
|
||||
|
@@ -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
|
||||
|
||||
@@ -28,13 +31,16 @@ class PersisterService extends Service {
|
||||
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", true)
|
||||
timer = new Timer("file persister timer", true)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
@@ -44,9 +50,16 @@ class PersisterService extends Service {
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -56,6 +69,9 @@ class PersisterService extends Service {
|
||||
if (event != null) {
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,44 +125,44 @@ class PersisterService extends Service {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
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
|
||||
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()
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
||||
json.length = f.length()
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = Base64.encode ih.getRoot()
|
||||
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)
|
||||
}
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
|
@@ -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
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIPersistFilesEvent extends Event {
|
||||
}
|
@@ -6,8 +6,12 @@ class CacheServers {
|
||||
|
||||
private static final int TO_GIVE = 3
|
||||
private static Set<Destination> CACHES = [
|
||||
// zlatinb
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA==")
|
||||
// sNL
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
|
||||
// dark_trion
|
||||
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -7,21 +7,35 @@ class Host {
|
||||
private static final int MAX_FAILURES = 3
|
||||
|
||||
final Destination destination
|
||||
private final int clearInterval
|
||||
private final int clearInterval, hopelessInterval, rejectionInterval
|
||||
int failures,successes
|
||||
long lastAttempt
|
||||
long lastSuccessfulAttempt
|
||||
long lastRejection
|
||||
|
||||
public Host(Destination destination, int clearInterval) {
|
||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
|
||||
this.destination = destination
|
||||
this.clearInterval = clearInterval
|
||||
this.hopelessInterval = hopelessInterval
|
||||
this.rejectionInterval = rejectionInterval
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
|
||||
private void connectSuccessful() {
|
||||
failures = 0
|
||||
successes++
|
||||
lastAttempt = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
connectSuccessful()
|
||||
lastSuccessfulAttempt = lastAttempt
|
||||
}
|
||||
|
||||
synchronized void onReject() {
|
||||
connectSuccessful()
|
||||
lastRejection = lastAttempt;
|
||||
}
|
||||
|
||||
synchronized void onFailure() {
|
||||
failures++
|
||||
successes = 0
|
||||
@@ -40,7 +54,17 @@ class Host {
|
||||
failures = 0
|
||||
}
|
||||
|
||||
synchronized void canTryAgain() {
|
||||
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||
synchronized boolean canTryAgain() {
|
||||
lastSuccessfulAttempt > 0 &&
|
||||
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isHopeless() {
|
||||
isFailed() &&
|
||||
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isRecentlyRejected() {
|
||||
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ class HostCache extends Service {
|
||||
hosts.get(e.destination).clearFailures()
|
||||
return
|
||||
}
|
||||
Host host = new Host(e.destination, settings.hostClearInterval)
|
||||
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
if (allowHost(host)) {
|
||||
hosts.put(e.destination, host)
|
||||
}
|
||||
@@ -64,15 +64,17 @@ class HostCache extends Service {
|
||||
Destination dest = e.endpoint.destination
|
||||
Host host = hosts.get(dest)
|
||||
if (host == null) {
|
||||
host = new Host(dest, settings.hostClearInterval)
|
||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
|
||||
switch(e.status) {
|
||||
case ConnectionAttemptStatus.SUCCESSFUL:
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onConnect()
|
||||
break
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onReject()
|
||||
break
|
||||
case ConnectionAttemptStatus.FAILED:
|
||||
host.onFailure()
|
||||
break
|
||||
@@ -82,6 +84,10 @@ class HostCache extends Service {
|
||||
List<Destination> getHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {allowHost(hosts[it])}
|
||||
rv.removeAll {
|
||||
def h = hosts[it];
|
||||
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
@@ -99,6 +105,22 @@ class HostCache extends Service {
|
||||
Collections.shuffle(rv)
|
||||
rv[0..n-1]
|
||||
}
|
||||
|
||||
int countFailingHosts() {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {
|
||||
hosts[it].isFailed()
|
||||
}
|
||||
rv.size()
|
||||
}
|
||||
|
||||
int countHopelessHosts() {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {
|
||||
hosts[it].isHopeless()
|
||||
}
|
||||
rv.size()
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (storage.exists()) {
|
||||
@@ -106,12 +128,16 @@ class HostCache extends Service {
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
Destination dest = new Destination(entry.destination)
|
||||
Host host = new Host(dest, settings.hostClearInterval)
|
||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
if (entry.lastAttempt != null)
|
||||
host.lastAttempt = entry.lastAttempt
|
||||
if (allowHost(host))
|
||||
if (entry.lastSuccessfulAttempt != null)
|
||||
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
|
||||
if (entry.lastRejection != null)
|
||||
host.lastRejection = entry.lastRejection
|
||||
if (allowHost(host))
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +146,6 @@ class HostCache extends Service {
|
||||
}
|
||||
|
||||
private boolean allowHost(Host host) {
|
||||
if (host.isFailed() && !host.canTryAgain())
|
||||
return false
|
||||
if (host.destination == myself)
|
||||
return false
|
||||
TrustLevel trust = trustService.getLevel(host.destination)
|
||||
@@ -140,12 +164,14 @@ class HostCache extends Service {
|
||||
storage.delete()
|
||||
storage.withPrintWriter { writer ->
|
||||
hosts.each { dest, host ->
|
||||
if (allowHost(host)) {
|
||||
if (allowHost(host) && !host.isHopeless()) {
|
||||
def map = [:]
|
||||
map.destination = dest.toBase64()
|
||||
map.failures = host.failures
|
||||
map.successes = host.successes
|
||||
map.lastAttempt = host.lastAttempt
|
||||
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
|
||||
map.lastRejection = host.lastRejection
|
||||
def json = JsonOutput.toJson(map)
|
||||
writer.println json
|
||||
}
|
||||
|
@@ -33,11 +33,12 @@ class MeshManager {
|
||||
meshes.get(infoHash)
|
||||
}
|
||||
|
||||
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
|
||||
Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
|
||||
synchronized(meshes) {
|
||||
if (meshes.containsKey(infoHash))
|
||||
return meshes.get(infoHash)
|
||||
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio)
|
||||
float ratio = sequential ? 0f : settings.downloadSequentialRatio
|
||||
Pieces pieces = new Pieces(nPieces, ratio)
|
||||
if (fileManager.rootToFiles.containsKey(infoHash)) {
|
||||
for (int i = 0; i < nPieces; i++)
|
||||
pieces.markDownloaded(i)
|
||||
|
@@ -0,0 +1,87 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
@Log
|
||||
class BrowseManager {
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
|
||||
private final Executor browserThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
BrowseManager(I2PConnector connector, EventBus eventBus) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
void onUIBrowseEvent(UIBrowseEvent e) {
|
||||
browserThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("BROWSE\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 results = Integer.parseInt(headers['Count'])
|
||||
|
||||
// at this stage, start pulling the results
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING, totalResults : results))
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
||||
UUID uuid = UUID.randomUUID()
|
||||
for (int i = 0; i < results; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
def json = slurper.parse(tmp)
|
||||
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
|
||||
eventBus.publish(result)
|
||||
}
|
||||
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
|
||||
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING, "browse failed", bad)
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
} as Runnable)
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.muwire.core.search;
|
||||
|
||||
public enum BrowseStatus {
|
||||
CONNECTING, FETCHING, FINISHED, FAILED
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class BrowseStatusEvent extends Event {
|
||||
BrowseStatus status
|
||||
int totalResults
|
||||
}
|
@@ -90,6 +90,14 @@ class ResultsParser {
|
||||
Set<Destination> sources = Collections.emptySet()
|
||||
if (json.sources != null)
|
||||
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
|
||||
|
||||
String comment = null
|
||||
if (json.comment != null)
|
||||
comment = DataUtil.readi18nString(Base64.decode(json.comment))
|
||||
|
||||
boolean browse = false
|
||||
if (json.browse != null)
|
||||
browse = json.browse
|
||||
|
||||
return new UIResultEvent( sender : p,
|
||||
name : name,
|
||||
@@ -97,6 +105,8 @@ class ResultsParser {
|
||||
infohash : new InfoHash(infoHash),
|
||||
pieceSize : pieceSize,
|
||||
sources : sources,
|
||||
comment : comment,
|
||||
browse : browse,
|
||||
uuid: uuid)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
|
@@ -4,6 +4,7 @@ import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -13,10 +14,12 @@ import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.util.logging.Log
|
||||
@@ -42,16 +45,19 @@ class ResultsSender {
|
||||
private final I2PConnector connector
|
||||
private final Persona me
|
||||
private final EventBus eventBus
|
||||
private final MuWireSettings settings
|
||||
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me) {
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) {
|
||||
this.connector = connector;
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
|
||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
|
||||
if (target.equals(me.destination)) {
|
||||
def uiResultEvents = []
|
||||
results.each {
|
||||
long length = it.getFile().length()
|
||||
int pieceSize = it.getPieceSize()
|
||||
@@ -60,19 +66,25 @@ class ResultsSender {
|
||||
Set<Destination> suggested = Collections.emptySet()
|
||||
if (it instanceof DownloadedFile)
|
||||
suggested = it.sources
|
||||
def comment = null
|
||||
if (it.getComment() != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||
}
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
sources : suggested
|
||||
sources : suggested,
|
||||
comment : comment
|
||||
)
|
||||
eventBus.publish(uiResultEvent)
|
||||
uiResultEvents << uiResultEvent
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid : uuid, results : uiResultEvents))
|
||||
} else {
|
||||
executor.execute(new ResultSendJob(uuid : uuid, results : results,
|
||||
target: target, oobInfohash : oobInfohash))
|
||||
target: target, oobInfohash : oobInfohash, compressedResults : compressedResults))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,58 +93,79 @@ class ResultsSender {
|
||||
SharedFile [] results
|
||||
Destination target
|
||||
boolean oobInfohash
|
||||
boolean compressedResults
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = oobInfohash ? 2 : 1
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
if (!oobInfohash) {
|
||||
byte [] hashList = it.getInfoHash().getHashList()
|
||||
def hashListB64 = []
|
||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||
hashListB64 << Base64.encode(tmp)
|
||||
}
|
||||
obj.hashList = hashListB64
|
||||
if (!compressedResults) {
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
|
||||
if (it instanceof DownloadedFile)
|
||||
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
results.each {
|
||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.close()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
os.flush()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "problem sending results",e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
|
||||
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = 2
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
||||
obj.size = sf.getCachedLength()
|
||||
obj.pieceSize = sf.getPieceSize()
|
||||
|
||||
if (sf instanceof DownloadedFile)
|
||||
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
if (sf.getComment() != null)
|
||||
obj.comment = sf.getComment()
|
||||
|
||||
obj.browse = browseFiles
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
@@ -9,11 +9,13 @@ class SearchEvent extends Event {
|
||||
byte [] searchHash
|
||||
UUID uuid
|
||||
boolean oobInfohash
|
||||
boolean searchComments
|
||||
boolean compressedResults
|
||||
|
||||
String toString() {
|
||||
def infoHash = null
|
||||
if (searchHash != null)
|
||||
infoHash = new InfoHash(searchHash)
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments compressedResults:$compressedResults"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.SplitPattern
|
||||
|
||||
class SearchIndex {
|
||||
|
||||
@@ -32,7 +32,7 @@ class SearchIndex {
|
||||
}
|
||||
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
|
||||
String [] split = source.split(" ")
|
||||
def rv = []
|
||||
split.each { if (it.length() > 0) rv << it }
|
||||
|
@@ -44,7 +44,7 @@ public class SearchManager {
|
||||
log.info("No results for search uuid $event.uuid")
|
||||
return
|
||||
}
|
||||
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
|
||||
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash, event.searchEvent.compressedResults)
|
||||
}
|
||||
|
||||
boolean hasLocalSearch(UUID uuid) {
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIBrowseEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -14,6 +14,8 @@ class UIResultEvent extends Event {
|
||||
long size
|
||||
InfoHash infohash
|
||||
int pieceSize
|
||||
String comment
|
||||
boolean browse
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@@ -3,5 +3,5 @@ package com.muwire.core.update
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class UpdateServers {
|
||||
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
|
||||
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
|
||||
}
|
||||
|
@@ -92,7 +92,7 @@ public class UploadManager {
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
@@ -217,7 +217,7 @@ public class UploadManager {
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
|
13
core/src/main/java/com/muwire/core/Constants.java
Normal file
13
core/src/main/java/com/muwire/core/Constants.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import net.i2p.crypto.SigType;
|
||||
|
||||
public 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 int MAX_RESULTS = 0x1 << 16;
|
||||
}
|
@@ -2,6 +2,12 @@ package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class SharedFile {
|
||||
|
||||
@@ -11,6 +17,12 @@ public class SharedFile {
|
||||
|
||||
private final String cachedPath;
|
||||
private final long cachedLength;
|
||||
|
||||
private final String b64EncodedFileName;
|
||||
private final String b64EncodedHashRoot;
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
@@ -18,6 +30,16 @@ public class SharedFile {
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
||||
|
||||
List<String> b64List = new ArrayList<String>();
|
||||
byte[] tmp = new byte[32];
|
||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
||||
b64List.add(Base64.encode(tmp));
|
||||
}
|
||||
this.b64EncodedHashList = b64List;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
@@ -40,6 +62,18 @@ public class SharedFile {
|
||||
rv++;
|
||||
return rv;
|
||||
}
|
||||
|
||||
public String getB64EncodedFileName() {
|
||||
return b64EncodedFileName;
|
||||
}
|
||||
|
||||
public String getB64EncodedHashRoot() {
|
||||
return b64EncodedHashRoot;
|
||||
}
|
||||
|
||||
public List<String> getB64EncodedHashList() {
|
||||
return b64EncodedHashList;
|
||||
}
|
||||
|
||||
public String getCachedPath() {
|
||||
return cachedPath;
|
||||
@@ -48,6 +82,14 @@ public class SharedFile {
|
||||
public long getCachedLength() {
|
||||
return cachedLength;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
@@ -1,122 +1,134 @@
|
||||
package com.muwire.core.util
|
||||
package com.muwire.core.util;
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
class DataUtil {
|
||||
public class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1;
|
||||
|
||||
static void writeUnsignedShort(int value, OutputStream os) {
|
||||
static void writeUnsignedShort(int value, OutputStream os) throws IOException {
|
||||
if (value > MAX_SHORT || value < 0)
|
||||
throw new IllegalArgumentException("$value invalid")
|
||||
throw new IllegalArgumentException("$value invalid");
|
||||
|
||||
byte lsb = (byte) (value & 0xFF)
|
||||
byte msb = (byte) (value >> 8)
|
||||
byte lsb = (byte) (value & 0xFF);
|
||||
byte msb = (byte) (value >> 8);
|
||||
|
||||
os.write(msb)
|
||||
os.write(lsb)
|
||||
os.write(msb);
|
||||
os.write(lsb);
|
||||
}
|
||||
|
||||
private final static int MAX_HEADER = 0x7FFFFF
|
||||
private final static int MAX_HEADER = 0x7FFFFF;
|
||||
|
||||
static void packHeader(int length, byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
if (length < 0 || length > MAX_HEADER)
|
||||
throw new IllegalArgumentException("length $length")
|
||||
throw new IllegalArgumentException("length $length");
|
||||
|
||||
header[2] = (byte) (length & 0xFF)
|
||||
header[1] = (byte) ((length >> 8) & 0xFF)
|
||||
header[0] = (byte) ((length >> 16) & 0x7F)
|
||||
header[2] = (byte) (length & 0xFF);
|
||||
header[1] = (byte) ((length >> 8) & 0xFF);
|
||||
header[0] = (byte) ((length >> 16) & 0x7F);
|
||||
}
|
||||
|
||||
static int readLength(byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF)
|
||||
((int)header[2] & 0xFF);
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length")
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length");
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
||||
if (encoded.length != length + 2)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length")
|
||||
byte [] string = new byte[length]
|
||||
System.arraycopy(encoded, 2, string, 0, length)
|
||||
new String(string, StandardCharsets.UTF_8)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
|
||||
byte [] string = new byte[length];
|
||||
System.arraycopy(encoded, 2, string, 0, length);
|
||||
return new String(string, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8)
|
||||
public static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
|
||||
if (utf8.length > Short.MAX_VALUE)
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length")
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) utf8.length)
|
||||
daos.write(utf8)
|
||||
daos.close()
|
||||
baos.toByteArray()
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length");
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DataOutputStream daos = new DataOutputStream(baos);
|
||||
try {
|
||||
daos.writeShort((short) utf8.length);
|
||||
daos.write(utf8);
|
||||
daos.close();
|
||||
} catch (IOException impossible) {
|
||||
throw new IllegalStateException(impossible);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static String readTillRN(InputStream is) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
public static String readTillRN(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||
byte read = is.read()
|
||||
int read = is.read();
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
throw new IOException();
|
||||
if (read == '\r') {
|
||||
if (is.read() != '\n')
|
||||
throw new IOException("invalid header")
|
||||
break
|
||||
throw new IOException("invalid header");
|
||||
break;
|
||||
}
|
||||
baos.write(read)
|
||||
baos.write(read);
|
||||
}
|
||||
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
||||
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8
|
||||
int bytes = totalPieces / 8;
|
||||
if (totalPieces % 8 != 0)
|
||||
bytes++
|
||||
byte[] raw = new byte[bytes]
|
||||
pieces.each {
|
||||
int byteIdx = it / 8
|
||||
int offset = it % 8
|
||||
int mask = 0x80 >>> offset
|
||||
raw[byteIdx] |= mask
|
||||
bytes++;
|
||||
byte[] raw = new byte[bytes];
|
||||
for (int it : pieces) {
|
||||
int byteIdx = it / 8;
|
||||
int offset = it % 8;
|
||||
int mask = 0x80 >>> offset;
|
||||
raw[byteIdx] |= mask;
|
||||
}
|
||||
Base64.encode(raw)
|
||||
return Base64.encode(raw);
|
||||
}
|
||||
|
||||
public static List<Integer> decodeXHave(String xHave) {
|
||||
byte [] availablePieces = Base64.decode(xHave)
|
||||
List<Integer> available = new ArrayList<>()
|
||||
availablePieces.eachWithIndex {b, i ->
|
||||
byte [] availablePieces = Base64.decode(xHave);
|
||||
List<Integer> available = new ArrayList<>();
|
||||
for (int i = 0; i < availablePieces.length; i ++) {
|
||||
byte b = availablePieces[i];
|
||||
for (int j = 0; j < 8 ; j++) {
|
||||
byte mask = 0x80 >>> j
|
||||
byte mask = (byte) (0x80 >>> j);
|
||||
if ((b & mask) == mask) {
|
||||
available.add(i * 8 + j)
|
||||
available.add(i * 8 + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
available
|
||||
return available;
|
||||
}
|
||||
|
||||
public static Exception findRoot(Exception e) {
|
||||
public static Throwable findRoot(Throwable e) {
|
||||
while(e.getCause() != null)
|
||||
e = e.getCause()
|
||||
e
|
||||
e = e.getCause();
|
||||
return e;
|
||||
}
|
||||
|
||||
public static void tryUnmap(ByteBuffer cb) {
|
@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
|
||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||
|
||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher)
|
||||
acceptor.start()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import static org.junit.Assert.fail
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
@@ -180,10 +181,11 @@ class DownloadSessionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // this needs to be rewritten with stealing in mind
|
||||
public void testSmallFileClaimed() {
|
||||
initSession(20, [0])
|
||||
long now = System.currentTimeMillis()
|
||||
downloadThread.join(100)
|
||||
downloadThread.join(150)
|
||||
assert 100 >= (System.currentTimeMillis() - now)
|
||||
assert !performed
|
||||
assert available.isEmpty()
|
||||
|
@@ -16,7 +16,7 @@ class PiecesTest {
|
||||
public void testSinglePiece() {
|
||||
pieces = new Pieces(1)
|
||||
assert !pieces.isComplete()
|
||||
assert pieces.claim() == 0
|
||||
assert pieces.claim() == [0,0,0]
|
||||
pieces.markDownloaded(0)
|
||||
assert pieces.isComplete()
|
||||
}
|
||||
@@ -25,28 +25,28 @@ class PiecesTest {
|
||||
public void testTwoPieces() {
|
||||
pieces = new Pieces(2)
|
||||
assert !pieces.isComplete()
|
||||
int piece = pieces.claim()
|
||||
assert piece == 0 || piece == 1
|
||||
pieces.markDownloaded(piece)
|
||||
int[] piece = pieces.claim()
|
||||
assert piece[0] == 0 || piece[0] == 1
|
||||
pieces.markDownloaded(piece[0])
|
||||
assert !pieces.isComplete()
|
||||
int piece2 = pieces.claim()
|
||||
assert piece != piece2
|
||||
pieces.markDownloaded(piece2)
|
||||
int[] piece2 = pieces.claim()
|
||||
assert piece[0] != piece2[0]
|
||||
pieces.markDownloaded(piece2[0])
|
||||
assert pieces.isComplete()
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClaimAvailable() {
|
||||
pieces = new Pieces(2)
|
||||
int claimed = pieces.claim([0].toSet())
|
||||
assert claimed == 0
|
||||
assert -1 == pieces.claim([0].toSet())
|
||||
int[] claimed = pieces.claim([0].toSet())
|
||||
assert claimed == [0,0,0]
|
||||
assert [0,0,1] == pieces.claim([0].toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClaimNoneAvailable() {
|
||||
pieces = new Pieces(20)
|
||||
int claimed = pieces.claim()
|
||||
assert -1 == pieces.claim([claimed].toSet())
|
||||
int[] claimed = pieces.claim()
|
||||
assert [0,0,0] == pieces.claim(claimed.toSet())
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,8 @@ class HasherServiceTest {
|
||||
void before() {
|
||||
eventBus = new EventBus()
|
||||
hasher = new FileHasher()
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||
def props = new MuWireSettings()
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, props), props)
|
||||
eventBus.register(FileHashedEvent.class, listener)
|
||||
eventBus.register(FileSharedEvent.class, service)
|
||||
service.start()
|
||||
|
@@ -72,6 +72,9 @@ class HostCacheTest {
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
settingsMock.ignore.allowUntrusted { true }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -91,6 +94,10 @@ class HostCacheTest {
|
||||
TrustLevel.DISTRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -104,6 +111,9 @@ class HostCacheTest {
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
settingsMock.ignore.allowUntrusted { false }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -123,6 +133,9 @@ class HostCacheTest {
|
||||
}
|
||||
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
|
||||
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -139,7 +152,15 @@ class HostCacheTest {
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
trustMock.demand.getLevel { d ->
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 100 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
|
||||
@@ -158,6 +179,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
|
||||
@@ -183,6 +208,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
|
||||
@@ -214,6 +243,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -229,6 +262,11 @@ class HostCacheTest {
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
Thread.sleep(150)
|
||||
@@ -260,6 +298,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
def rv = cache.getHosts(5)
|
||||
assert rv.size() == 1
|
||||
|
@@ -9,6 +9,9 @@ import org.junit.Test
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.mesh.Mesh
|
||||
|
||||
class UploaderTest {
|
||||
|
||||
@@ -52,7 +55,13 @@ class UploaderTest {
|
||||
}
|
||||
|
||||
private void startUpload() {
|
||||
uploader = new ContentUploader(file, request, endpoint)
|
||||
def hasher = new FileHasher()
|
||||
InfoHash infoHash = hasher.hashFile(file)
|
||||
Pieces pieces = new Pieces(FileHasher.getPieceSize(file.length()))
|
||||
for (int i = 0; i < pieces.nPieces; i++)
|
||||
pieces.markDownloaded(i)
|
||||
Mesh mesh = new Mesh(infoHash, pieces)
|
||||
uploader = new ContentUploader(file, request, endpoint, mesh, FileHasher.getPieceSize(file.length()))
|
||||
uploadThread = new Thread(uploader.respond() as Runnable)
|
||||
uploadThread.setDaemon(true)
|
||||
uploadThread.start()
|
||||
@@ -81,6 +90,7 @@ class UploaderTest {
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 0-19" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
|
||||
byte [] data = new byte[20]
|
||||
@@ -96,6 +106,7 @@ class UploaderTest {
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 5-15" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
|
||||
byte [] data = new byte[11]
|
||||
@@ -111,6 +122,7 @@ class UploaderTest {
|
||||
request = new ContentRequest(range : new Range(0,20))
|
||||
startUpload()
|
||||
assert "416 Range Not Satisfiable" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
}
|
||||
|
||||
@@ -123,6 +135,7 @@ class UploaderTest {
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
|
||||
byte [] data = new byte[length]
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
|
@@ -1,8 +1,20 @@
|
||||
group = com.muwire
|
||||
version = 0.4.9
|
||||
version = 0.5.4
|
||||
i2pVersion = 0.9.43
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
grailsVersion=4.0.0
|
||||
gorm.version=7.0.2.RELEASE
|
||||
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
|
||||
# plugin properties
|
||||
author = zab@mail.i2p
|
||||
signer = zab@mail.i2p
|
||||
keystorePassword=changeit
|
||||
websiteURL=http://muwire.i2p
|
||||
updateURLsu3=http://muwire.i2p/MuWire.su3
|
||||
|
||||
pack200=true
|
||||
|
@@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
|
||||
apply from: 'gradle/publishing.gradle'
|
||||
apply from: 'gradle/code-coverage.gradle'
|
||||
apply from: 'gradle/code-quality.gradle'
|
||||
apply from: 'gradle/integration-test.gradle'
|
||||
// apply from: 'gradle/code-coverage.gradle'
|
||||
// apply from: 'gradle/code-quality.gradle'
|
||||
// apply from: 'gradle/integration-test.gradle'
|
||||
// apply from: 'gradle/package.gradle'
|
||||
apply from: 'gradle/docs.gradle'
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
@@ -58,7 +58,11 @@ dependencies {
|
||||
compile project(":core")
|
||||
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
|
||||
|
||||
runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
|
||||
// runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
|
||||
|
||||
runtime group: 'org.slf4j', name: 'slf4j-jdk14', version: "${slf4jVersion}"
|
||||
runtime group: 'org.slf4j', name: 'slf4j-api', version: "${slf4jVersion}"
|
||||
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
|
||||
runtime "javax.annotation:javax.annotation-api:1.3.2"
|
||||
|
||||
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
|
||||
@@ -119,6 +123,7 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
|
||||
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
|
||||
destinationFile = file("${buildDir}/jacoco/root.exec")
|
||||
@@ -138,4 +143,5 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
|
||||
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
|
||||
application {
|
||||
title = 'MuWire'
|
||||
startupGroups = ['EventList', 'MainFrame']
|
||||
autoShutdown = true
|
||||
startupGroups = ['EventList', 'MainFrame', 'ShutdownWindow']
|
||||
autoShutdown = false
|
||||
}
|
||||
|
||||
mvcGroups {
|
||||
@@ -16,6 +16,11 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.MainFrameView'
|
||||
controller = 'com.muwire.gui.MainFrameController'
|
||||
}
|
||||
'ShutdownWindow' {
|
||||
model = 'com.muwire.gui.ShutdownWindowModel'
|
||||
view = 'com.muwire.gui.ShutdownWindowView'
|
||||
controller = 'com.muwire.gui.ShutdownWindowController'
|
||||
}
|
||||
'SearchTab' {
|
||||
model = 'com.muwire.gui.SearchTabModel'
|
||||
view = 'com.muwire.gui.SearchTabView'
|
||||
@@ -46,4 +51,24 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.ContentPanelView'
|
||||
controller = 'com.muwire.gui.ContentPanelController'
|
||||
}
|
||||
'show-comment' {
|
||||
model = 'com.muwire.gui.ShowCommentModel'
|
||||
view = 'com.muwire.gui.ShowCommentView'
|
||||
controller = 'com.muwire.gui.ShowCommentController'
|
||||
}
|
||||
'add-comment' {
|
||||
model = 'com.muwire.gui.AddCommentModel'
|
||||
view = 'com.muwire.gui.AddCommentView'
|
||||
controller = 'com.muwire.gui.AddCommentController'
|
||||
}
|
||||
'browse' {
|
||||
model = 'com.muwire.gui.BrowseModel'
|
||||
view = 'com.muwire.gui.BrowseView'
|
||||
controller = 'com.muwire.gui.BrowseController'
|
||||
}
|
||||
'close-warning' {
|
||||
model = 'com.muwire.gui.CloseWarningModel'
|
||||
view = 'com.muwire.gui.CloseWarningView'
|
||||
controller = 'com.muwire.gui.CloseWarningController'
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class AddCommentController {
|
||||
@MVCMember @Nonnull
|
||||
AddCommentModel model
|
||||
@MVCMember @Nonnull
|
||||
AddCommentView view
|
||||
|
||||
Core core
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
String comment = view.textarea.getText()
|
||||
if (comment.trim().length() == 0)
|
||||
comment = null
|
||||
else
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
model.selectedFiles.each {
|
||||
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
|
||||
it.setComment(comment)
|
||||
core.eventBus.publish(event)
|
||||
}
|
||||
mvcGroup.parentGroup.view.refreshSharedFiles()
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
import com.muwire.core.search.BrowseStatusEvent
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class BrowseController {
|
||||
@MVCMember @Nonnull
|
||||
BrowseModel model
|
||||
@MVCMember @Nonnull
|
||||
BrowseView view
|
||||
|
||||
EventBus eventBus
|
||||
|
||||
|
||||
void register() {
|
||||
eventBus.register(BrowseStatusEvent.class, this)
|
||||
eventBus.register(UIResultEvent.class, this)
|
||||
eventBus.publish(new UIBrowseEvent(host : model.host))
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
eventBus.unregister(UIResultEvent.class, this)
|
||||
}
|
||||
|
||||
void onBrowseStatusEvent(BrowseStatusEvent e) {
|
||||
runInsideUIAsync {
|
||||
model.status = e.status
|
||||
if (e.status == BrowseStatus.FETCHING)
|
||||
model.totalResults = e.totalResults
|
||||
}
|
||||
}
|
||||
|
||||
void onUIResultEvent(UIResultEvent e) {
|
||||
runInsideUIAsync {
|
||||
model.results << e
|
||||
model.resultCount = model.results.size()
|
||||
view.resultsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void dismiss() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.isEmpty())
|
||||
return
|
||||
selectedResults.removeAll {
|
||||
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
selectedResults.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
eventBus.publish(new UIDownloadEvent(
|
||||
result : [result],
|
||||
sources : [model.host.destination],
|
||||
target : file,
|
||||
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
|
||||
))
|
||||
}
|
||||
|
||||
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewComment() {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.size() != 1)
|
||||
return
|
||||
def result = selectedResults[0]
|
||||
if (result.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(result.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = result
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class CloseWarningController {
|
||||
@MVCMember @Nonnull
|
||||
CloseWarningModel model
|
||||
@MVCMember @Nonnull
|
||||
CloseWarningView view
|
||||
|
||||
UISettings settings
|
||||
File home
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
model.closeWarning = settings.closeWarning
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void close() {
|
||||
boolean showWarning = !view.checkbox.model.isSelected()
|
||||
model.closeWarning = showWarning
|
||||
settings.closeWarning = showWarning
|
||||
|
||||
File props = new File(home, "gui.properties")
|
||||
props.withOutputStream {
|
||||
settings.write(it)
|
||||
}
|
||||
|
||||
view.dialog.setVisible(false)
|
||||
view.mainFrame.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -68,6 +68,16 @@ class ContentPanelController {
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHits() {
|
||||
int selectedRule = view.getSelectedRule()
|
||||
if (selectedRule < 0)
|
||||
return
|
||||
Matcher matcher = model.rules[selectedRule]
|
||||
matcher.matches.clear()
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
|
@@ -13,10 +13,11 @@ import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.SplitPattern
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
@@ -24,6 +25,7 @@ import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
@@ -59,6 +61,7 @@ class MainFrameController {
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -75,16 +78,18 @@ class MainFrameController {
|
||||
|
||||
def searchEvent
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
|
||||
} else {
|
||||
// this can be improved a lot
|
||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
def nonEmpty = []
|
||||
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
}
|
||||
@@ -96,6 +101,7 @@ class MainFrameController {
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = tabTitle
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -106,20 +112,6 @@ class MainFrameController {
|
||||
originator : core.me))
|
||||
}
|
||||
|
||||
private def selectedResult() {
|
||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
def table = selected.getClientProperty("results-table")
|
||||
int row = table.getSelectedRow()
|
||||
if (row == -1)
|
||||
return
|
||||
def sortEvt = group.view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||
}
|
||||
group.model.results[row]
|
||||
}
|
||||
|
||||
private int selectedDownload() {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selected = downloadsTable.getSelectedRow()
|
||||
@@ -129,42 +121,6 @@ class MainFrameController {
|
||||
selected
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return
|
||||
|
||||
if (!model.canDownload(result.infohash))
|
||||
return
|
||||
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
|
||||
def resultsBucket = group.model.hashBucket[result.infohash]
|
||||
def sources = group.model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trustPersonaFromSearch() {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
@@ -205,6 +161,23 @@ class MainFrameController {
|
||||
core.eventBus.publish(new UIDownloadPausedEvent())
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clear() {
|
||||
def toRemove = []
|
||||
model.downloads.each {
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
toRemove << it
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
toRemove << it
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
model.downloads.remove(it)
|
||||
}
|
||||
model.clearButtonEnabled = false
|
||||
|
||||
}
|
||||
|
||||
private void markTrust(String tableName, TrustLevel level, def list) {
|
||||
int row = view.getSelectedTrustTablesRow(tableName)
|
||||
if (row < 0)
|
||||
@@ -299,22 +272,25 @@ class MainFrameController {
|
||||
}
|
||||
|
||||
void unshareSelectedFile() {
|
||||
SharedFile sf = view.selectedSharedFile()
|
||||
def sf = view.selectedSharedFiles()
|
||||
if (sf == null)
|
||||
return
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
sf.each {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
|
||||
}
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
}
|
||||
|
||||
void stopWatchingDirectory() {
|
||||
String directory = mvcGroup.view.getSelectedWatchedDirectory()
|
||||
if (directory == null)
|
||||
|
||||
@ControllerAction
|
||||
void addComment() {
|
||||
def selectedFiles = view.selectedSharedFiles()
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
core.muOptions.watchedDirectories.remove(directory)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory)))
|
||||
|
||||
model.watched.remove(directory)
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params['selectedFiles'] = selectedFiles
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
|
@@ -31,6 +31,9 @@ class MuWireStatusController {
|
||||
model.outgoingConnections = outgoing
|
||||
|
||||
model.knownHosts = core.hostCache.hosts.size()
|
||||
model.failingHosts = core.hostCache.countFailingHosts()
|
||||
model.hopelessHosts = core.hostCache.countHopelessHosts()
|
||||
|
||||
|
||||
model.sharedFiles = core.fileManager.fileToSharedFile.size()
|
||||
|
||||
|
@@ -70,6 +70,10 @@ class OptionsController {
|
||||
model.updateCheckInterval = text
|
||||
settings.updateCheckInterval = Integer.valueOf(text)
|
||||
|
||||
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
|
||||
model.searchComments = searchComments
|
||||
settings.searchComments = searchComments
|
||||
|
||||
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
|
||||
model.autoDownloadUpdate = autoDownloadUpdate
|
||||
settings.autoDownloadUpdate = autoDownloadUpdate
|
||||
@@ -78,9 +82,24 @@ class OptionsController {
|
||||
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
||||
model.shareDownloadedFiles = shareDownloaded
|
||||
settings.shareDownloadedFiles = shareDownloaded
|
||||
|
||||
boolean shareHidden = view.shareHiddenCheckbox.model.isSelected()
|
||||
model.shareHiddenFiles = shareHidden
|
||||
settings.shareHiddenFiles = shareHidden
|
||||
|
||||
boolean browseFiles = view.browseFilesCheckbox.model.isSelected()
|
||||
model.browseFiles = browseFiles
|
||||
settings.browseFiles = browseFiles
|
||||
|
||||
text = view.speedSmoothSecondsField.text
|
||||
model.speedSmoothSeconds = Integer.valueOf(text)
|
||||
settings.speedSmoothSeconds = Integer.valueOf(text)
|
||||
|
||||
String downloadLocation = model.downloadLocation
|
||||
settings.downloadLocation = new File(downloadLocation)
|
||||
|
||||
String incompleteLocation = model.incompleteLocation
|
||||
settings.incompleteLocation = new File(incompleteLocation)
|
||||
|
||||
if (settings.embeddedRouter) {
|
||||
text = view.inBwField.text
|
||||
@@ -96,6 +115,10 @@ class OptionsController {
|
||||
model.onlyTrusted = onlyTrusted
|
||||
settings.setAllowUntrusted(!onlyTrusted)
|
||||
|
||||
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected()
|
||||
model.searchExtraHop = searchExtraHop
|
||||
settings.searchExtraHop = searchExtraHop
|
||||
|
||||
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
|
||||
model.trustLists = trustLists
|
||||
settings.allowTrustLists = trustLists
|
||||
@@ -119,10 +142,9 @@ class OptionsController {
|
||||
text = view.fontField.text
|
||||
model.font = text
|
||||
uiSettings.font = text
|
||||
|
||||
// boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||
// model.showMonitor = showMonitor
|
||||
// uiSettings.showMonitor = showMonitor
|
||||
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -136,10 +158,6 @@ class OptionsController {
|
||||
model.excludeLocalResult = excludeLocalResult
|
||||
uiSettings.excludeLocalResult = excludeLocalResult
|
||||
|
||||
// boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
|
||||
// model.showSearchHashes = showSearchHashes
|
||||
// uiSettings.showSearchHashes = showSearchHashes
|
||||
|
||||
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||
uiSettingsFile.withOutputStream {
|
||||
uiSettings.write(it)
|
||||
@@ -164,4 +182,26 @@ class OptionsController {
|
||||
if (rv == JFileChooser.APPROVE_OPTION)
|
||||
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void incompleteLocation() {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
chooser.setDialogTitle("Select location for downloaded files")
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION)
|
||||
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void automaticFontAction() {
|
||||
model.automaticFontSize = true
|
||||
model.customFontSize = 12
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void customFontAction() {
|
||||
model.automaticFontSize = false
|
||||
}
|
||||
}
|
@@ -4,8 +4,121 @@ import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SearchTabController {
|
||||
}
|
||||
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
@MVCMember @Nonnull
|
||||
SearchTabView view
|
||||
|
||||
Core core
|
||||
|
||||
private def selectedResults() {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
results
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
int selectedSender = view.selectedSenderRow()
|
||||
if (selectedSender < 0)
|
||||
return
|
||||
Persona sender = model.senders[selectedSender]
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['eventBus'] = core.eventBus
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = event
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ShowCommentController {
|
||||
@MVCMember @Nonnull
|
||||
ShowCommentView view
|
||||
|
||||
@ControllerAction
|
||||
void dismiss() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ShutdownWindowController {
|
||||
}
|
@@ -10,15 +10,25 @@ import com.muwire.gui.UISettings
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.ImageIcon
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JTable
|
||||
import javax.swing.LookAndFeel
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.plaf.FontUIResource
|
||||
|
||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||
|
||||
import java.awt.Font
|
||||
import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.Toolkit
|
||||
import java.awt.TrayIcon
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
|
||||
@Log
|
||||
class Initialize extends AbstractLifecycleHandler {
|
||||
@@ -29,6 +39,66 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
@Override
|
||||
void execute() {
|
||||
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
log.info("No config file specified, so turning off most logging")
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
|
||||
}
|
||||
}
|
||||
|
||||
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
|
||||
|
||||
if (SystemTray.isSupported() && (SystemVersion.isMac() || SystemVersion.isWindows())) {
|
||||
try {
|
||||
def tray = SystemTray.getSystemTray()
|
||||
def url = Initialize.class.getResource("/MuWire-16x16.png")
|
||||
def image = new ImageIcon(url, "tray icon").getImage()
|
||||
def popupMenu = new PopupMenu()
|
||||
def trayIcon = new TrayIcon(image, "MuWire", popupMenu)
|
||||
|
||||
|
||||
def exit = new MenuItem("Exit")
|
||||
exit.addActionListener({
|
||||
application.getWindowManager().findWindow("main-frame").setVisible(false)
|
||||
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
|
||||
Core core = application.getContext().get("core")
|
||||
if (core != null) {
|
||||
Thread t = new Thread({
|
||||
core.shutdown()
|
||||
application.shutdown()
|
||||
}as Runnable)
|
||||
t.start()
|
||||
} else
|
||||
application.shutdown()
|
||||
tray.remove(trayIcon)
|
||||
})
|
||||
|
||||
def showMW = {e ->
|
||||
def mainFrame = application.getWindowManager().findWindow("main-frame")
|
||||
if (mainFrame != null) {
|
||||
Core core = application.getContext().get("core")
|
||||
if (core != null)
|
||||
mainFrame.setVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
def show = new MenuItem("Open MuWire")
|
||||
show.addActionListener(showMW)
|
||||
popupMenu.add(show)
|
||||
popupMenu.add(exit)
|
||||
tray.add(trayIcon)
|
||||
|
||||
|
||||
trayIcon.addActionListener(showMW)
|
||||
application.getContext().put("tray-icon", true)
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't set tray icon",bad)
|
||||
}
|
||||
}
|
||||
|
||||
log.info "Loading home dir"
|
||||
def portableHome = System.getProperty("portable.home")
|
||||
def home = portableHome == null ?
|
||||
@@ -43,7 +113,7 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
application.context.put("muwire-home", home.getAbsolutePath())
|
||||
|
||||
System.getProperties().setProperty("awt.useSystemAAFontSettings", "true")
|
||||
System.getProperties().setProperty("awt.useSystemAAFontSettings", "gasp")
|
||||
|
||||
def guiPropsFile = new File(home, "gui.properties")
|
||||
UISettings uiSettings
|
||||
@@ -52,25 +122,43 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
guiPropsFile.withInputStream { props.load(it) }
|
||||
uiSettings = new UISettings(props)
|
||||
|
||||
def lnf
|
||||
log.info("settting user-specified lnf $uiSettings.lnf")
|
||||
try {
|
||||
lookAndFeel(uiSettings.lnf)
|
||||
lnf = lookAndFeel(uiSettings.lnf)
|
||||
} catch (Throwable bad) {
|
||||
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad)
|
||||
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID()
|
||||
log.log(Level.WARNING,"couldn't set desired look and feel, switching to defaults", bad)
|
||||
lnf = lookAndFeel("system","gtk","metal")
|
||||
uiSettings.lnf = lnf.getID()
|
||||
}
|
||||
|
||||
if (uiSettings.font != null) {
|
||||
log.info("setting user-specified font $uiSettings.font")
|
||||
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
|
||||
def defaults = UIManager.getDefaults()
|
||||
defaults.put("Button.font", font)
|
||||
defaults.put("RadioButton.font", font)
|
||||
defaults.put("Label.font", font)
|
||||
defaults.put("CheckBox.font", font)
|
||||
defaults.put("Table.font", font)
|
||||
defaults.put("TableHeader.font", font)
|
||||
// TODO: add others
|
||||
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
|
||||
|
||||
FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
|
||||
|
||||
String fontName
|
||||
if (uiSettings.font != null)
|
||||
fontName = uiSettings.font
|
||||
else
|
||||
fontName = defaultFont.getName()
|
||||
|
||||
int fontSize = defaultFont.getSize()
|
||||
if (uiSettings.autoFontSize) {
|
||||
int resolution = Toolkit.getDefaultToolkit().getScreenResolution()
|
||||
fontSize = resolution / 9;
|
||||
} else {
|
||||
fontSize = uiSettings.fontSize
|
||||
}
|
||||
|
||||
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
|
||||
|
||||
def keys = lnf.getDefaults().keys()
|
||||
while(keys.hasMoreElements()) {
|
||||
def key = keys.nextElement()
|
||||
def value = lnf.getDefaults().get(key)
|
||||
if (value instanceof FontUIResource)
|
||||
lnf.getDefaults().put(key, font)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Properties props = new Properties()
|
||||
@@ -86,6 +174,8 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
}
|
||||
} else {
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||
if (chosen == null)
|
||||
chosen = lookAndFeel('metal')
|
||||
uiSettings.lnf = chosen.getID()
|
||||
log.info("ended up applying $chosen.name")
|
||||
}
|
||||
|
@@ -45,9 +45,12 @@ class Ready extends AbstractLifecycleHandler {
|
||||
props.load(it)
|
||||
}
|
||||
props = new MuWireSettings(props)
|
||||
if (props.incompleteLocation == null)
|
||||
props.incompleteLocation = new File(home, "incompletes")
|
||||
} else {
|
||||
log.info("creating new properties")
|
||||
props = new MuWireSettings()
|
||||
props.incompleteLocation = new File(home, "incompletes")
|
||||
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
def nickname
|
||||
|
@@ -20,6 +20,9 @@ class Shutdown extends AbstractLifecycleHandler {
|
||||
void execute() {
|
||||
log.info("shutting down")
|
||||
Core core = application.context.get("core")
|
||||
core.shutdown()
|
||||
if (core != null) {
|
||||
Thread t = new Thread({ core.shutdown() } as Runnable)
|
||||
t.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
gui/griffon-app/models/com/muwire/gui/AddCommentModel.groovy
Normal file
12
gui/griffon-app/models/com/muwire/gui/AddCommentModel.groovy
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class AddCommentModel {
|
||||
List<SharedFile> selectedFiles
|
||||
}
|
21
gui/griffon-app/models/com/muwire/gui/BrowseModel.groovy
Normal file
21
gui/griffon-app/models/com/muwire/gui/BrowseModel.groovy
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class BrowseModel {
|
||||
Persona host
|
||||
@Observable BrowseStatus status
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable int totalResults
|
||||
@Observable int resultCount
|
||||
|
||||
def results = []
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class CloseWarningModel {
|
||||
@griffon.transform.Observable boolean closeWarning
|
||||
}
|
@@ -40,11 +40,15 @@ class ContentPanelModel {
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
int selectedRule = view.getSelectedRule()
|
||||
rules.clear()
|
||||
rules.addAll(contentManager.matchers)
|
||||
hits.clear()
|
||||
view.rulesTable.model.fireTableDataChanged()
|
||||
view.hitsTable.model.fireTableDataChanged()
|
||||
if (selectedRule >= 0) {
|
||||
view.rulesTable.selectionModel.setSelectionInterval(selectedRule,selectedRule)
|
||||
}
|
||||
}
|
||||
|
||||
void onContentControlEvent(ContentControlEvent e) {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
|
||||
@@ -8,12 +10,16 @@ import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JTable
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.RouterDisconnectedEvent
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
@@ -21,6 +27,7 @@ import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
@@ -57,6 +64,8 @@ class MainFrameModel {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
MainFrameController controller
|
||||
@MVCMember @Nonnull
|
||||
MainFrameView view
|
||||
@Inject @Nonnull GriffonApplication application
|
||||
@Observable boolean coreInitialized = false
|
||||
@Observable boolean routerPresent
|
||||
@@ -64,8 +73,11 @@ class MainFrameModel {
|
||||
def results = new ConcurrentHashMap<>()
|
||||
def downloads = []
|
||||
def uploads = []
|
||||
def shared = []
|
||||
def watched = []
|
||||
boolean treeVisible = true
|
||||
def shared
|
||||
def sharedTree
|
||||
def treeRoot
|
||||
final Map<SharedFile, TreeNode> fileToNode = new HashMap<>()
|
||||
def connectionList = []
|
||||
def searches = new LinkedList()
|
||||
def trusted = []
|
||||
@@ -76,12 +88,12 @@ class MainFrameModel {
|
||||
@Observable String me
|
||||
@Observable int loadedFiles
|
||||
@Observable File hashingFile
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean cancelButtonEnabled
|
||||
@Observable boolean retryButtonEnabled
|
||||
@Observable boolean pauseButtonEnabled
|
||||
@Observable boolean clearButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean addCommentButtonEnabled
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||
@Observable boolean markDistrustedButtonEnabled
|
||||
@@ -90,8 +102,14 @@ class MainFrameModel {
|
||||
@Observable boolean reviewButtonEnabled
|
||||
@Observable boolean updateButtonEnabled
|
||||
@Observable boolean unsubscribeButtonEnabled
|
||||
|
||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||
|
||||
@Observable boolean searchesPaneButtonEnabled
|
||||
@Observable boolean downloadsPaneButtonEnabled
|
||||
@Observable boolean uploadsPaneButtonEnabled
|
||||
@Observable boolean monitorPaneButtonEnabled
|
||||
@Observable boolean trustPaneButtonEnabled
|
||||
|
||||
@Observable Downloader downloader
|
||||
|
||||
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||
|
||||
@@ -104,13 +122,22 @@ class MainFrameModel {
|
||||
void updateTablePreservingSelection(String tableName) {
|
||||
def downloadTable = builder.getVariable(tableName)
|
||||
int selectedRow = downloadTable.getSelectedRow()
|
||||
downloadTable.model.fireTableDataChanged()
|
||||
while(true) {
|
||||
try {
|
||||
downloadTable.model.fireTableDataChanged()
|
||||
break
|
||||
} catch (IllegalArgumentException iae) {} // caused by underlying model changing while table is sorted
|
||||
}
|
||||
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, Object> args) {
|
||||
|
||||
uiSettings = application.context.get("ui-settings")
|
||||
|
||||
shared = []
|
||||
treeRoot = new DefaultMutableTreeNode()
|
||||
sharedTree = new DefaultTreeModel(treeRoot)
|
||||
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
@@ -119,17 +146,26 @@ class MainFrameModel {
|
||||
return
|
||||
|
||||
// remove cancelled or finished downloads
|
||||
def toRemove = []
|
||||
downloads.each {
|
||||
if (uiSettings.clearCancelledDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
|
||||
toRemove << it
|
||||
if (uiSettings.clearFinishedDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
|
||||
toRemove << it
|
||||
}
|
||||
toRemove.each {
|
||||
downloads.remove(it)
|
||||
if (!clearButtonEnabled || uiSettings.clearCancelledDownloads || uiSettings.clearFinishedDownloads) {
|
||||
def toRemove = []
|
||||
downloads.each {
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
if (uiSettings.clearCancelledDownloads) {
|
||||
toRemove << it
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
if (uiSettings.clearFinishedDownloads) {
|
||||
toRemove << it
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
downloads.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
||||
@@ -145,7 +181,6 @@ class MainFrameModel {
|
||||
core = e.getNewValue()
|
||||
routerPresent = core.router != null
|
||||
me = core.me.getHumanReadableName()
|
||||
core.eventBus.register(UIResultEvent.class, this)
|
||||
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||
core.eventBus.register(ConnectionEvent.class, this)
|
||||
@@ -177,7 +212,7 @@ class MainFrameModel {
|
||||
return
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
retryInterval *= 1000
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastRetryTime > retryInterval) {
|
||||
lastRetryTime = now
|
||||
@@ -193,13 +228,19 @@ class MainFrameModel {
|
||||
|
||||
}
|
||||
}
|
||||
}, 60000, 60000)
|
||||
}, 1000, 1000)
|
||||
|
||||
runInsideUIAsync {
|
||||
trusted.addAll(core.trustService.good.values())
|
||||
distrusted.addAll(core.trustService.bad.values())
|
||||
|
||||
resumeButtonText = "Retry"
|
||||
|
||||
searchesPaneButtonEnabled = false
|
||||
downloadsPaneButtonEnabled = true
|
||||
uploadsPaneButtonEnabled = true
|
||||
monitorPaneButtonEnabled = true
|
||||
trustPaneButtonEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -207,9 +248,7 @@ class MainFrameModel {
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
runInsideUIAsync {
|
||||
watched.addAll(core.muOptions.watchedDirectories)
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
@@ -277,7 +316,6 @@ class MainFrameModel {
|
||||
|
||||
void onFileHashingEvent(FileHashingEvent e) {
|
||||
runInsideUIAsync {
|
||||
loadedFiles = shared.size()
|
||||
hashingFile = e.hashingFile
|
||||
}
|
||||
}
|
||||
@@ -288,38 +326,51 @@ class MainFrameModel {
|
||||
}
|
||||
if (e.error != null)
|
||||
return // TODO do something
|
||||
if (infoHashes.contains(e.sharedFile.infoHash))
|
||||
return
|
||||
infoHashes.add(e.sharedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.sharedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.sharedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
if (infoHashes.contains(e.loadedFile.infoHash))
|
||||
return
|
||||
infoHashes.add(e.loadedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.loadedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.loadedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
InfoHash infohash = e.unsharedFile.infoHash
|
||||
if (!infoHashes.remove(infohash))
|
||||
return
|
||||
runInsideUIAsync {
|
||||
shared.remove(e.unsharedFile)
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
|
||||
def dmtn = fileToNode.remove(e.unsharedFile)
|
||||
if (dmtn != null) {
|
||||
loadedFiles = fileToNode.size()
|
||||
while (true) {
|
||||
def parent = dmtn.getParent()
|
||||
parent.remove(dmtn)
|
||||
if (parent == treeRoot)
|
||||
break
|
||||
if (parent.getChildCount() == 0) {
|
||||
File file = parent.getUserObject().file
|
||||
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : parent.getUserObject().file))
|
||||
dmtn = parent
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +499,8 @@ class MainFrameModel {
|
||||
}
|
||||
|
||||
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
|
||||
if (core.getShutdown().get())
|
||||
return
|
||||
runInsideUIAsync {
|
||||
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
|
||||
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
|
||||
@@ -458,13 +511,48 @@ class MainFrameModel {
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (!core.muOptions.shareDownloadedFiles)
|
||||
return
|
||||
infoHashes.add(e.downloadedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.downloadedFile
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.downloadedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
private void insertIntoTree(SharedFile file) {
|
||||
List<File> parents = new ArrayList<>()
|
||||
File tmp = file.file.getParentFile()
|
||||
while(tmp.getParent() != null) {
|
||||
parents << tmp
|
||||
tmp = tmp.getParentFile()
|
||||
}
|
||||
Collections.reverse(parents)
|
||||
TreeNode node = treeRoot
|
||||
for(File path : parents) {
|
||||
boolean exists = false
|
||||
def children = node.children()
|
||||
def child = null
|
||||
while(children.hasMoreElements()) {
|
||||
child = children.nextElement()
|
||||
def userObject = child.getUserObject()
|
||||
if (userObject != null && userObject.file == path) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
child = new DefaultMutableTreeNode(new InterimTreeNode(path))
|
||||
node.add(child)
|
||||
}
|
||||
node = child
|
||||
}
|
||||
|
||||
def dmtn = new DefaultMutableTreeNode(file)
|
||||
fileToNode.put(file, dmtn)
|
||||
node.add(dmtn)
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
|
||||
private static class UIConnection {
|
||||
Destination destination
|
||||
|
@@ -16,6 +16,8 @@ class MuWireStatusModel {
|
||||
@Observable int incomingConnections
|
||||
@Observable int outgoingConnections
|
||||
@Observable int knownHosts
|
||||
@Observable int failingHosts
|
||||
@Observable int hopelessHosts
|
||||
@Observable int sharedFiles
|
||||
@Observable int downloads
|
||||
|
||||
|
@@ -13,7 +13,12 @@ class OptionsModel {
|
||||
@Observable String updateCheckInterval
|
||||
@Observable boolean autoDownloadUpdate
|
||||
@Observable boolean shareDownloadedFiles
|
||||
@Observable boolean shareHiddenFiles
|
||||
@Observable String downloadLocation
|
||||
@Observable String incompleteLocation
|
||||
@Observable boolean searchComments
|
||||
@Observable boolean browseFiles
|
||||
@Observable int speedSmoothSeconds
|
||||
|
||||
// i2p options
|
||||
@Observable String inboundLength
|
||||
@@ -27,6 +32,8 @@ class OptionsModel {
|
||||
@Observable boolean showMonitor
|
||||
@Observable String lnf
|
||||
@Observable String font
|
||||
@Observable boolean automaticFontSize
|
||||
@Observable int customFontSize
|
||||
@Observable boolean clearCancelledDownloads
|
||||
@Observable boolean clearFinishedDownloads
|
||||
@Observable boolean excludeLocalResult
|
||||
@@ -38,6 +45,7 @@ class OptionsModel {
|
||||
|
||||
// trust options
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean searchExtraHop
|
||||
@Observable boolean trustLists
|
||||
@Observable String trustListInterval
|
||||
|
||||
@@ -48,7 +56,12 @@ class OptionsModel {
|
||||
updateCheckInterval = settings.updateCheckInterval
|
||||
autoDownloadUpdate = settings.autoDownloadUpdate
|
||||
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||
shareHiddenFiles = settings.shareHiddenFiles
|
||||
downloadLocation = settings.downloadLocation.getAbsolutePath()
|
||||
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
|
||||
searchComments = settings.searchComments
|
||||
browseFiles = settings.browseFiles
|
||||
speedSmoothSeconds = settings.speedSmoothSeconds
|
||||
|
||||
Core core = application.context.get("core")
|
||||
inboundLength = core.i2pOptions["inbound.length"]
|
||||
@@ -62,6 +75,8 @@ class OptionsModel {
|
||||
showMonitor = uiSettings.showMonitor
|
||||
lnf = uiSettings.lnf
|
||||
font = uiSettings.font
|
||||
automaticFontSize = uiSettings.autoFontSize
|
||||
customFontSize = uiSettings.fontSize
|
||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||
excludeLocalResult = uiSettings.excludeLocalResult
|
||||
@@ -73,6 +88,7 @@ class OptionsModel {
|
||||
}
|
||||
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
searchExtraHop = settings.searchExtraHop
|
||||
trustLists = settings.allowTrustLists
|
||||
trustListInterval = String.valueOf(settings.trustListInterval)
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
@@ -17,14 +18,21 @@ import griffon.metadata.ArtifactProviderFor
|
||||
class SearchTabModel {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean browseActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
String uuid
|
||||
def senders = []
|
||||
def results = []
|
||||
def hashBucket = [:]
|
||||
def sourcesBucket = [:]
|
||||
|
||||
def sendersBucket = new LinkedHashMap<>()
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
@@ -48,6 +56,15 @@ class SearchTabModel {
|
||||
}
|
||||
bucket << e
|
||||
|
||||
def senderBucket = sendersBucket.get(e.sender)
|
||||
if (senderBucket == null) {
|
||||
senderBucket = []
|
||||
sendersBucket[e.sender] = senderBucket
|
||||
senders.clear()
|
||||
senders.addAll(sendersBucket.keySet())
|
||||
}
|
||||
senderBucket << e
|
||||
|
||||
Set sourceBucket = sourcesBucket.get(e.infohash)
|
||||
if (sourceBucket == null) {
|
||||
sourceBucket = new HashSet()
|
||||
@@ -55,8 +72,7 @@ class SearchTabModel {
|
||||
}
|
||||
sourceBucket.addAll(e.sources)
|
||||
|
||||
results << e
|
||||
JTable table = builder.getVariable("results-table")
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
@@ -72,6 +88,14 @@ class SearchTabModel {
|
||||
bucket = []
|
||||
hashBucket[it.infohash] = bucket
|
||||
}
|
||||
|
||||
def senderBucket = sendersBucket.get(it.sender)
|
||||
if (senderBucket == null) {
|
||||
senderBucket = []
|
||||
sendersBucket[it.sender] = senderBucket
|
||||
senders.clear()
|
||||
senders.addAll(sendersBucket.keySet())
|
||||
}
|
||||
|
||||
Set sourceBucket = sourcesBucket.get(it.infohash)
|
||||
if (sourceBucket == null) {
|
||||
@@ -81,9 +105,9 @@ class SearchTabModel {
|
||||
sourceBucket.addAll(it.sources)
|
||||
|
||||
bucket << it
|
||||
results << it
|
||||
senderBucket << it
|
||||
}
|
||||
JTable table = builder.getVariable("results-table")
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ShowCommentModel {
|
||||
UIResultEvent result
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ShutdownWindowModel {
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user