Compare commits

...

186 Commits

Author SHA1 Message Date
Zlatin Balevsky
5e329dfa2c Release 0.5.4 2019-10-26 06:42:14 +01:00
Zlatin Balevsky
742f6da870 update notifications 2019-10-26 06:12:54 +01:00
Zlatin Balevsky
7f46347c0f retry failed downloads 2019-10-26 05:33:22 +01:00
Zlatin Balevsky
b308ac2f37 searches by hash 2019-10-26 05:14:04 +01:00
Zlatin Balevsky
9cdabb51d1 count shared files in dashboard 2019-10-25 22:51:26 +01:00
Zlatin Balevsky
45f0736a5e account for hashing errors 2019-10-25 22:51:15 +01:00
Zlatin Balevsky
fe753ff978 add a download details view 2019-10-25 22:36:25 +01:00
Zlatin Balevsky
ac717b5205 center things horizontally 2019-10-25 22:02:04 +01:00
Zlatin Balevsky
6f624e3afc add some stats to main window 2019-10-25 21:51:16 +01:00
Zlatin Balevsky
623d675ed9 Ability to view comments 2019-10-25 18:57:07 +01:00
Zlatin Balevsky
546b71b632 implement adding comments to shared files 2019-10-25 18:32:55 +01:00
Zlatin Balevsky
804113bb1b typo 2019-10-25 17:46:59 +01:00
Zlatin Balevsky
ab9e10f438 add a note about the CLI 2019-10-25 17:43:15 +01:00
Zlatin Balevsky
00520acdf0 implement browse host 2019-10-25 17:30:16 +01:00
Zlatin Balevsky
8c44d196a7 move gui result processing on gui thread 2019-10-25 13:14:38 +01:00
Zlatin Balevsky
9c5fa0a2ce hook up trust to results 2019-10-25 10:36:26 +01:00
Zlatin Balevsky
d7bca05725 implement trust list review window 2019-10-25 10:00:52 +01:00
Zlatin Balevsky
45fcb2209e Trust List actions 2019-10-25 08:48:07 +01:00
Zlatin Balevsky
7bf0373b80 trust and distrust actions 2019-10-25 08:24:07 +01:00
Zlatin Balevsky
5925b42597 wip on trust window 2019-10-25 07:39:01 +01:00
Zlatin Balevsky
13243b05ad center shutdown dialog 2019-10-25 06:14:14 +01:00
Zlatin Balevsky
43987be463 prevent RejectedExecutionExceptions on shutdown 2019-10-25 06:13:20 +01:00
Zlatin Balevsky
fcd3414e02 refresh number of connections automatically 2019-10-25 06:08:41 +01:00
Zlatin Balevsky
70913ea8fb correct startup sequence, add listeners for allFilesLoadedEvent 2019-10-25 06:01:16 +01:00
Zlatin Balevsky
b30e552498 share and unshare a directory 2019-10-24 22:35:29 +01:00
Zlatin Balevsky
bae66de4eb implement share file dialog 2019-10-24 22:03:20 +01:00
Zlatin Balevsky
626e145e25 properly set size of tables 2019-10-24 19:22:39 +01:00
Zlatin Balevsky
bf72c76f13 limit the size of the table based on the terminal size 2019-10-24 19:12:50 +01:00
Zlatin Balevsky
fce8bbfd97 wip on shared files window 2019-10-24 18:34:27 +01:00
Zlatin Balevsky
1cc7925155 uploads window 2019-10-24 17:24:01 +01:00
Zlatin Balevsky
12b51ceb02 add an ETA column 2019-10-24 16:51:11 +01:00
Zlatin Balevsky
62811861a4 working downloads window 2019-10-24 16:36:10 +01:00
Zlatin Balevsky
837aa6974b display search results in new window 2019-10-24 14:39:25 +01:00
Zlatin Balevsky
94e7c42d19 add option to specify I2CP host and port. Show failure message is I2CP connect fails 2019-10-24 08:15:02 +01:00
Zlatin Balevsky
877bf12a93 fixed progress dialog, wip on search view 2019-10-24 07:49:15 +01:00
Zlatin Balevsky
224266b2dd basic initialization of the core 2019-10-23 22:25:54 +01:00
Zlatin Balevsky
8f16614dc3 start a new project for an interactive cli 2019-10-23 19:38:16 +01:00
Zlatin Balevsky
b412f9fb0c Release 0.5.3 2019-10-23 09:01:19 +01:00
Zlatin Balevsky
b24d04811d set apple quit strategy 2019-10-23 08:55:10 +01:00
Zlatin Balevsky
771f645df0 proper close 2019-10-23 08:48:53 +01:00
Zlatin Balevsky
b6483ad0f4 add an exit menu 2019-10-23 08:45:03 +01:00
Zlatin Balevsky
decb72c8ef show a warning that MW will continue running 2019-10-23 08:31:23 +01:00
Zlatin Balevsky
439b3bf18b fixes 2019-10-23 06:46:20 +01:00
Zlatin Balevsky
06679ffee0 only show MW if the core has loaded 2019-10-23 06:39:25 +01:00
Zlatin Balevsky
1d5b12e2d7 if core is not initialized, just shutdown 2019-10-23 06:31:08 +01:00
Zlatin Balevsky
4e6e1b6f5b Do not show warnings if core is already shutting down 2019-10-23 06:15:34 +01:00
Zlatin Balevsky
f0b5361d7b smaller icon 2019-10-23 06:06:37 +01:00
Zlatin Balevsky
e0c6bfbf51 show the clsoing window if tray is disabled 2019-10-23 06:01:21 +01:00
Zlatin Balevsky
2a0ecd8a47 fix constructor 2019-10-23 05:48:14 +01:00
Zlatin Balevsky
fb1804e849 Use explicit event to shutdown the application. This fixes closing on Linux 2019-10-23 05:45:50 +01:00
Zlatin Balevsky
d4eaa0df8d do not shutdown core on awt thread 2019-10-22 23:37:44 +01:00
Zlatin Balevsky
ffde6ac86f show a window while MW is shutting down 2019-10-22 23:26:54 +01:00
Zlatin Balevsky
7ad677ead2 add an explicit menu to show MW 2019-10-22 21:48:51 +01:00
Zlatin Balevsky
ddb0568aab do not auto-shutdown 2019-10-22 21:40:47 +01:00
Zlatin Balevsky
ff50a84a48 try to get a tray icon working 2019-10-22 21:34:50 +01:00
Zlatin Balevsky
770396ba41 update test 2019-10-22 10:31:28 +01:00
Zlatin Balevsky
b55852e993 typo 2019-10-22 10:16:41 +01:00
Zlatin Balevsky
a6945275a4 i2p 0.9.43 2019-10-22 08:27:08 +01:00
Zlatin Balevsky
7241809e55 update readme 2019-10-22 00:42:18 +01:00
Zlatin Balevsky
54073af933 Release 0.5.2 2019-10-22 00:28:53 +01:00
Zlatin Balevsky
a32903fc8c prettier i2p status panel 2019-10-22 00:11:57 +01:00
Zlatin Balevsky
e40520be46 count hopeless and failing hosts, prettier status panel 2019-10-21 23:57:15 +01:00
Zlatin Balevsky
97482b949a de-capitalize for consistency 2019-10-21 22:50:21 +01:00
Zlatin Balevsky
92ee107312 remove duplicate variable 2019-10-21 22:23:29 +01:00
Zlatin Balevsky
2e8082af64 use titled borders everywhere for consistency 2019-10-21 22:12:39 +01:00
Zlatin Balevsky
8da5a428c9 make the i2p version a variable 2019-10-21 21:02:37 +01:00
Zlatin Balevsky
fd46b3c7d6 do not display fractions in percentage 2019-10-21 20:37:30 +01:00
Zlatin Balevsky
eea3b2563b allign router-specific settings 2019-10-21 20:16:36 +01:00
Zlatin Balevsky
50719f3828 move settings to top of panel 2019-10-21 20:12:08 +01:00
Zlatin Balevsky
01a45a89a8 reorganize the options view 2019-10-21 19:44:33 +01:00
Zlatin Balevsky
66bd249ed3 show percentage of fetched results 2019-10-21 18:28:37 +01:00
Zlatin Balevsky
265cd6ee15 more accurate description 2019-10-20 20:19:47 +01:00
Zlatin Balevsky
1dc88cb96b make speed smoothing interval configurable 2019-10-20 20:09:24 +01:00
Zlatin Balevsky
3e10d497b1 add an ETA column to downloads table 2019-10-20 19:11:32 +01:00
Zlatin Balevsky
9a0b3bb9d6 fix download table selection when sorted 2019-10-20 18:47:48 +01:00
Zlatin Balevsky
a1fe3c01b9 if no incompletes are in serialized json, use the default one, assuming an upgrade 2019-10-20 18:24:16 +01:00
Zlatin Balevsky
ab323db62a add ability to choose the incompletes location 2019-10-20 18:16:07 +01:00
Zlatin Balevsky
d954387e41 fix showing of local files in results 2019-10-20 11:59:48 +01:00
Zlatin Balevsky
ea9db21a18 wip on compressed results 2019-10-20 01:01:34 +01:00
Zlatin Balevsky
136cf89c9b groovy != java 2019-10-20 00:55:31 +01:00
Zlatin Balevsky
46de1baf88 compressed results 2019-10-20 00:54:32 +01:00
Zlatin Balevsky
13f7b8563c fix a bug where disabled browsing was shown as browsable. Log the response code if it's not 200 2019-10-19 22:33:47 +01:00
Zlatin Balevsky
9c15208f3a Release 0.5.1 2019-10-19 19:11:04 +01:00
Zlatin Balevsky
a9ce9d96b3 wip on menu; close zlib stream 2019-10-19 18:54:58 +01:00
Zlatin Balevsky
4d2a5a8018 MainFrameModel doesn't need to listen to single result events anymore 2019-10-19 18:12:30 +01:00
Zlatin Balevsky
8395047386 compress results in browse connections 2019-10-19 17:59:08 +01:00
Zlatin Balevsky
cb23aa44f0 enable SEVERE log messages if no config file specified 2019-10-19 05:53:33 +01:00
Zlatin Balevsky
dbcb8508b8 add a view comment button 2019-10-19 05:35:04 +01:00
Zlatin Balevsky
47d406d93b add a border around the two panels 2019-10-19 04:59:37 +01:00
Zlatin Balevsky
e06f1805c2 redirect griffon logging to jul 2019-10-19 04:45:45 +01:00
Zlatin Balevsky
2b04374e23 add option to disable browsing of files, make the dialog bigger 2019-10-19 00:53:13 +01:00
Zlatin Balevsky
383addbc37 implement view comment from browse window 2019-10-19 00:30:03 +01:00
Zlatin Balevsky
cc39cd7f8e implement downloading from browse window 2019-10-19 00:23:43 +01:00
Zlatin Balevsky
83665d7524 wip on browse host 2019-10-18 23:55:07 +01:00
Zlatin Balevsky
94340480b4 wip on browse host 2019-10-18 23:25:26 +01:00
Zlatin Balevsky
8850d49c63 wip on browse host 2019-10-18 23:16:37 +01:00
Zlatin Balevsky
f0f9d840f0 wip on browse host 2019-10-18 22:35:17 +01:00
Zlatin Balevsky
7f4cd4f331 wip on browse host 2019-10-18 21:17:34 +01:00
Zlatin Balevsky
e6162503f6 wip on browse host 2019-10-18 20:29:39 +01:00
Zlatin Balevsky
7a5d71dc36 add copy name to clipboard option 2019-10-17 19:01:53 +01:00
Zlatin Balevsky
6fa39a5e35 turn off logging if there is no config file 2019-10-17 18:39:28 +01:00
Zlatin Balevsky
c5ae804f61 Implement automatic font sizing; set all font properties on change of font 2019-10-17 18:15:04 +01:00
Zlatin Balevsky
d7695b448d remove my DS_Store 2019-10-17 05:50:29 +01:00
Zlatin Balevsky
946d9c8f32 disable sharing of hidden files by default, add option to enable 2019-10-17 05:46:27 +01:00
Zlatin Balevsky
02441ca1e3 add option to disable searching in comments 2019-10-16 19:57:18 +01:00
Zlatin Balevsky
5fa21b2360 keep tree expanded on modifications 2019-10-16 14:42:40 +01:00
Zlatin Balevsky
d4c08f4fe6 only remove from index if no more files have the same comment pt.2 2019-10-16 14:23:12 +01:00
Zlatin Balevsky
942de287c6 only remove from index if no more files have the same comment 2019-10-16 14:21:50 +01:00
Zlatin Balevsky
d0299f80c6 search through comments 2019-10-16 14:06:11 +01:00
Zlatin Balevsky
1227cf9263 Release 0.5.0 2019-10-15 12:38:25 +01:00
Zlatin Balevsky
a05575485f move things around 2019-10-15 10:40:50 +01:00
Zlatin Balevsky
f5bccd8126 All shared directories are watched directories. Fix manipulation of tree structure 2019-10-15 08:38:23 +01:00
Zlatin Balevsky
70fb789abf remove the watched directories table 2019-10-15 04:51:21 +01:00
Zlatin Balevsky
feb712c253 Move persisting of files on dedicated thread. Introduce an event to forcefully persist files. Do that immediately after unsharing anything 2019-10-15 04:21:40 +01:00
Zlatin Balevsky
d22b403e2a stop watching multiple directories at once 2019-10-14 23:16:05 +01:00
Zlatin Balevsky
a24982e0df fix comments for local results 2019-10-14 22:47:52 +01:00
Zlatin Balevsky
6c26019164 allow switching without restart 2019-10-14 21:40:03 +01:00
Zlatin Balevsky
965fa79bbf fix count of shared files in tree view mode 2019-10-14 20:57:50 +01:00
Zlatin Balevsky
60ddb85461 Tree view of the shared files. The count is wrong for some reason 2019-10-14 20:13:25 +01:00
Zlatin Balevsky
c7284623bc Release 0.4.16 2019-10-13 22:14:33 +01:00
Zlatin Balevsky
3e7f2aa70a Add a note about DND, automatically watch shared directories 2019-10-13 20:21:28 +01:00
Zlatin Balevsky
4f436a636c implement drop on MW -> share files/directories 2019-10-13 20:00:08 +01:00
Zlatin Balevsky
b49dbc30c3 comment already decoded by the time it gets to the gui 2019-10-11 19:01:40 +01:00
Zlatin Balevsky
c25d314e1c typo 2019-10-11 18:56:46 +01:00
Zlatin Balevsky
b28587a275 wip on file comments 2019-10-11 18:42:02 +01:00
Zlatin Balevsky
8b8e5d59be Silence an IllegalArgumentException while sorting downloads table 2019-10-11 11:21:56 +01:00
Zlatin Balevsky
70bbe1f636 update version 2019-10-10 17:33:07 +01:00
Zlatin Balevsky
337605dc0f Release 0.4.15 2019-10-10 16:48:10 +01:00
Zlatin Balevsky
14bdfa6b2e throttle even further - 500/s 2019-10-09 17:34:54 +01:00
Zlatin Balevsky
ed3f9da773 throttle loading even further, to 1000/sec 2019-10-09 16:46:17 +01:00
Zlatin Balevsky
251080d08f throttle loading of files to 500/s 2019-10-09 16:34:09 +01:00
Zlatin Balevsky
f530ab999d operations on multiple selection in shared files table 2019-10-09 03:38:08 +01:00
Zlatin Balevsky
4133384e48 ability to share multiple files and directories 2019-10-08 21:30:34 +01:00
Zlatin Balevsky
600fc98868 update TODO 2019-10-07 12:38:26 +01:00
Zlatin Balevsky
129eeb3b88 JDK needed, not JRE 2019-10-07 12:38:09 +01:00
Zlatin Balevsky
20b51b78a0 reduce priority of file persister thread 2019-10-07 11:59:51 +01:00
Zlatin Balevsky
33fe755b60 implement multiple-selection on downloads table 2019-10-07 04:26:35 +01:00
Zlatin Balevsky
8b0668a134 Rewrite utils into Java, cache the persistable data of shared files to reduce object churn 2019-10-05 22:50:32 +01:00
Zlatin Balevsky
730d2202fd bundles for linux available now 2019-10-05 18:53:43 +01:00
Zlatin Balevsky
69906a986d set i2p.dir.base to prevent router creating files in PWD 2019-10-05 15:03:59 +01:00
Zlatin Balevsky
5bc8fa8633 Preserve selection on refresh #18 2019-10-05 05:13:49 +01:00
Zlatin Balevsky
7de7c9d8f3 Add 'Clear Hits' button to content control panel #18 2019-10-05 05:03:25 +01:00
Zlatin Balevsky
e943f6019d disable all GUI unit tests, enable host-cache unit tests. The 'build' target now succeeds 2019-10-05 04:31:11 +01:00
Zlatin Balevsky
2eec7bec5b fix most core tests 2019-10-05 04:20:14 +01:00
Zlatin Balevsky
c36110cf76 update readme 2019-10-04 16:41:07 +01:00
Zlatin Balevsky
abe28517bc Release 0.4.14 2019-10-04 13:00:57 +01:00
Zlatin Balevsky
15bc4c064d center the button 2019-10-03 21:32:32 +01:00
Zlatin Balevsky
91d771944b add option for sequential download 2019-10-03 20:45:22 +01:00
Zlatin Balevsky
e09c456a13 make the download retry interval in seconds, default still 1 minute 2019-10-03 19:31:15 +01:00
Zlatin Balevsky
d9c1067226 Add Neutral button to search tab, issue #17 2019-10-02 06:02:06 +01:00
Zlatin Balevsky
eda3e7ad3a Add option to not search extra hop, only considered if connecting only to trusted peers, issue #6 2019-10-02 05:45:46 +01:00
Zlatin Balevsky
e9798c7eaa remember last rejection and back off from hosts that reject us. Fix return value of retry and hopelessness predicates 2019-10-01 08:34:43 +01:00
Zlatin Balevsky
66bb4eef5b close outbound establishments on a separate thread 2019-10-01 07:50:29 +01:00
Zlatin Balevsky
55f260b3f4 update version 2019-09-29 19:21:06 +01:00
Zlatin Balevsky
32d4c3965e Release 0.4.13 2019-09-29 19:00:20 +01:00
Zlatin Balevsky
de1534d837 reduce the default host retry interval 2019-09-29 18:45:09 +01:00
Zlatin Balevsky
7b58e8a88a separate setting for the interval after which a host is considered hopeless 2019-09-29 18:43:39 +01:00
Zlatin Balevsky
8a03b89985 clean up the filtering logic; allow serialization of hosts that can be retried 2019-09-29 16:49:02 +01:00
Zlatin Balevsky
1d97374857 track last successful attempt. Only re-attempt hosts if they have ever been successful. Do not serialize hosts considered hopeless 2019-09-29 16:19:19 +01:00
Zlatin Balevsky
549e8c2d98 Release 0.4.12 2019-09-22 16:55:04 +01:00
Zlatin Balevsky
b54d24db0d new update server destination 2019-09-22 16:47:35 +01:00
Zlatin Balevsky
fa12e84345 stronger sig type 2019-09-22 16:23:01 +01:00
Zlatin Balevsky
6430ff2691 bump i2p libs version 2019-09-22 16:13:12 +01:00
Zlatin Balevsky
591313c81c point to the pkg project 2019-09-20 21:09:53 +01:00
Zlatin Balevsky
ce7b6a0c65 change to gasp AA font table, try metal lnf if the others fail 2019-09-16 15:06:45 +01:00
Zlatin Balevsky
5c4d4c4580 embedded router will not work without reseed certificates, so remove it 2019-09-16 15:04:34 +01:00
Zlatin Balevsky
4cb864ff9f update version 2019-09-16 15:03:20 +01:00
Zlatin Balevsky
417675ad07 update dark_trion's hostcache address 2019-07-22 21:48:29 +01:00
Zlatin Balevsky
9513e5ba3c update todo 2019-07-20 13:15:44 +01:00
Zlatin Balevsky
85610cf169 add new host-cache 2019-07-15 22:05:09 +01:00
Zlatin Balevsky
e8322384b8 Release 0.4.11 2019-07-15 14:28:21 +01:00
Zlatin Balevsky
179279ed30 Merge branch 'master' of https://github.com/zlatinb/muwire 2019-07-14 06:19:18 +01:00
Zlatin Balevsky
ae79f0fded Clear Done button, thanks to Aegon 2019-07-14 06:19:05 +01:00
Zlatin Balevsky
ed878b3762 Merge pull request #11 from zetok/readme
Add info about the default I2CP port to README.md
2019-07-12 09:17:24 +01:00
Zetok Zalbavar
623cca0ef2 Add info about the default I2CP port to README.md
Also:
 - improved formatting a bit
 - removed trailing whitespaces
2019-07-12 07:28:12 +01:00
Zlatin Balevsky
eaa883c3ba count duplicate files towards total in Uploads panel 2019-07-11 23:28:12 +01:00
Zlatin Balevsky
7ae8076865 disable webui for now 2019-07-11 22:29:47 +01:00
Zlatin Balevsky
b1aa92661c do not pack200 some jars because of duplicate entries 2019-07-11 20:42:24 +01:00
Zlatin Balevsky
9ed94c8376 do not include tomcat runtime 2019-07-11 20:41:57 +01:00
Zlatin Balevsky
fa6aea1abe attempt to produce an I2P plugin 2019-07-11 19:49:04 +01:00
Zlatin Balevsky
0de84e704b hello webui 2019-07-11 18:34:27 +01:00
Zlatin Balevsky
a767dda044 add empty grails project for a web ui 2019-07-11 17:56:42 +01:00
Zlatin Balevsky
56e9235d7b avoid FS call to get file length 2019-07-11 15:28:25 +01:00
Zlatin Balevsky
2fba9a74ce persist files.json every minute 2019-07-11 14:32:57 +01:00
Zlatin Balevsky
2bb6826906 canonicalize all files before they enter FileManager and do not look for absolute path on persistence 2019-07-11 14:32:12 +01:00
Zlatin Balevsky
9f339629a9 remove unnecessary canonicalization 2019-07-11 11:58:20 +01:00
208 changed files with 30680 additions and 847 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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'
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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])
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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) }
}
}

View File

@@ -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])
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -35,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.4.10")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.4.10")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -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'

View File

@@ -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 = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
}

View File

@@ -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.10")
Core core = new Core(props, home, "0.5.3")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -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)) }

View File

@@ -0,0 +1,7 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
}

View File

@@ -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,

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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))
}

View File

@@ -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]

View File

@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
UIResultEvent[] result
Set<Destination> sources
File target
boolean sequential
}

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package com.muwire.core.files
import com.muwire.core.Event
class UIPersistFilesEvent extends Event {
}

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.search;
public enum BrowseStatus {
CONNECTING, FETCHING, FINISHED, FAILED
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.search
import com.muwire.core.Event
class BrowseStatusEvent extends Event {
BrowseStatus status
int totalResults
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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 }

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -14,6 +14,8 @@ class UIResultEvent extends Event {
long size
InfoHash infohash
int pieceSize
String comment
boolean browse
@Override
public String toString() {

View File

@@ -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==")
}

View File

@@ -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
}

View 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;
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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())
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -1,8 +1,20 @@
group = com.muwire
version = 0.4.10
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

View File

@@ -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")
}
}
*/

View File

@@ -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'
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()

View File

@@ -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
@@ -76,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))
}
@@ -157,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)
@@ -251,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() {

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -4,67 +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 selectedResult() {
int row = view.resultsTable.getSelectedRow()
if (row == -1)
private def selectedResults() {
int[] rows = view.resultsTable.getSelectedRows()
if (rows.length == 0)
return null
def sortEvt = view.lastSortEvent
if (sortEvt != null) {
row = view.resultsTable.rowSorter.convertRowIndexToModel(row)
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
}
model.results[row]
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return
if (!mvcGroup.parentGroup.model.canDownload(result.infohash))
return
@ControllerAction
void download() {
def results = selectedResults()
if (results == null)
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
results.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
mvcGroup.parentGroup.view.showDownloadsWindow.call()
}
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
@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))
}
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
}
mvcGroup.parentGroup.view.showDownloadsWindow.call()
}
@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 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)
}
}

View File

@@ -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()
}
}

View File

@@ -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 {
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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()
}
}
}

View 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
}

View 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 = []
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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 = []
@@ -79,7 +91,9 @@ class MainFrameModel {
@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
@@ -97,8 +111,6 @@ class MainFrameModel {
@Observable Downloader downloader
private final Set<InfoHash> infoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
@Observable volatile Core core
@@ -110,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({
@@ -125,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()
@@ -151,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)
@@ -183,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
@@ -199,7 +228,7 @@ class MainFrameModel {
}
}
}, 60000, 60000)
}, 1000, 1000)
runInsideUIAsync {
trusted.addAll(core.trustService.good.values())
@@ -219,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))
@@ -289,7 +316,6 @@ class MainFrameModel {
void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile
}
}
@@ -300,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()
}
}
@@ -460,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)
@@ -470,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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -21,6 +21,8 @@ class SearchTabModel {
@Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean browseActionEnabled
@Observable boolean viewCommentActionEnabled
Core core
UISettings uiSettings

View File

@@ -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
}

View File

@@ -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