Compare commits

...

215 Commits

Author SHA1 Message Date
bcb41baca2 update version, add link to Packaging page 2020-09-27 07:26:06 +01:00
72985bacb6 add ability to disable updates completely, intended for 3rd party packaging 2020-09-26 17:53:29 +01:00
3387d22a6c remove HOPELESS downloads from the download list via an event 2020-09-26 17:10:24 +01:00
10bd566d58 Release 0.7.4 2020-09-25 16:51:40 +01:00
f4e0c707df fix cleaning up of hopeless downloads in plugin 2020-09-23 15:23:51 +01:00
c11a427483 fix cleaning up of hopeless downloads in gui 2020-09-23 15:21:47 +01:00
e9db22c562 option for download attempts before giving up in desktop gui 2020-09-23 14:54:19 +01:00
fa53a35023 option for download attempts before giving up in plugin 2020-09-23 14:44:51 +01:00
94dd6101aa show sequential status and hopeless host count in plugin 2020-09-23 14:33:18 +01:00
e65fbe1bd1 show sequential status and hopeless host count in download details panel 2020-09-23 14:26:02 +01:00
964e315367 add a hopeless state for a download where all sources are hopeless 2020-09-23 14:17:40 +01:00
140231e362 Give up on download sources after a number of attempts 2020-09-23 14:00:52 +01:00
c73a821c67 put verified sources in the responder cache as well 2020-09-23 11:30:20 +01:00
0ebe00b526 reduce hopeless interval to 1hr and purge interval to 24hrs 2020-09-22 16:08:17 +01:00
b2a3bfce54 make sure we have the Persona of the altloc 2020-09-22 16:07:18 +01:00
c490a511bd distinguish between discovered sources and verified sources. Only propagate and persist verified sources 2020-09-22 13:34:49 +01:00
cbaa3470d2 make responder cache size configurable 2020-09-22 12:17:27 +01:00
84d61fccd5 cache recent responders and always forward queries to them. Thanks to qtm for the idea 2020-09-21 15:28:15 +01:00
a88db8f50f FixedSizeFIFOSet 2020-09-21 15:13:44 +01:00
5a38154e15 make sure the pongs uuid matches the last sent ping uuid 2020-09-20 18:42:26 +01:00
e5891de136 send and read up to 2 hosts per pong 2020-09-20 18:26:29 +01:00
1e729bae1c implement forgetting of hopeless hosts after some time 2020-09-20 17:49:07 +01:00
3e6e0c7e9f cache calls to System.currenTimeMillis() 2020-09-20 17:34:28 +01:00
44af23c162 restrict forwarding of queries to sqrt of neighboring connections. Thanks to 'qtm' for the idea 2020-09-19 17:28:19 +01:00
a262c99efe reduce limit on peer connections 2020-09-19 17:16:36 +01:00
9eff723dd3 Release 0.7.3 2020-09-18 18:44:15 +01:00
f2531c80d5 disable Configure button if no directory is selected 2020-09-18 18:35:10 +01:00
944cb29901 show error messages if the command is not in the appropriate room 2020-09-18 18:15:15 +01:00
2c7cf24942 browse and subscribe actions from upload table for plugin 2020-09-18 17:30:47 +01:00
5a94d14b8e pass event correctly 2020-09-18 16:13:48 +01:00
f20b23434f one more 2020-09-18 16:13:23 +01:00
8d523a6265 update to api 2020-09-18 15:56:39 +01:00
38b9ab5200 gui browse/feed/chat actions from upload table 2020-09-18 15:42:35 +01:00
f6fdf9e33f parse browse/feed/chat headers on the uploader side 2020-09-18 14:59:58 +01:00
b729a89672 Advertise browse/feed/chat abilities in download headers 2020-09-18 14:50:49 +01:00
e531093b28 do not try to connect to hopeless hosts 2020-09-17 19:58:36 +01:00
b18772465c limit the number of hosts read from each pong 2020-09-17 16:19:48 +01:00
20aac03789 send an uuid with pings and echo it in pongs 2020-09-17 16:11:32 +01:00
ac8d9c1281 update room scrollback limit to new api, update TODO 2020-09-16 17:41:01 +01:00
ad8693d512 some styling of chat lines 2020-09-16 17:15:23 +01:00
144ad634c8 fix ambiguous Math.max 2020-09-16 14:46:34 +01:00
4cdb383b9f fix minutes-to-milliseconds conversion of feed update interval in desktop gui. Make default feed update interval one hour 2020-09-16 14:37:08 +01:00
9c6f6bf266 Make number of connections for ultrapeer a property 2020-09-16 12:30:41 +01:00
9a4e6b868b get rid of clear search button to make more space for search field, as it is not very big on Mac LnF 2020-09-14 15:35:49 +01:00
31e0962b73 fix test target by excluding testng dependency 2020-09-14 14:38:12 +01:00
2acac4b1ea fix java 14 on mac 2020-09-14 13:33:01 +01:00
03d00a22d7 try native lnf on all platforms with metal last 2020-09-14 13:32:25 +01:00
0e54fb1ed1 update TODO 2020-09-14 12:45:42 +01:00
2dee5e2a8a short-circuit logic if monitor is not visible 2020-09-14 12:44:52 +01:00
c7406a4838 preserve selection when updating shared files table 2020-09-14 12:39:18 +01:00
c9eb702d7c add a section to the TODO for mwtrackerd with some items 2020-09-12 18:16:27 +01:00
253603cac7 add a log statement for infohash parsing 2020-09-12 14:37:54 +01:00
3af6ee3bce update TODO 2020-09-12 14:26:48 +01:00
bfa88b0b7a Release 0.7.2 2020-09-10 16:43:21 +01:00
1400967b22 update TODO 2020-09-04 14:47:13 +01:00
5739760075 up router version to 0.9.47 2020-09-04 14:46:37 +01:00
fec042ec36 check if watched directories disappeared while MW was down 2020-09-04 14:44:04 +01:00
d3477b91fc Merge branch 'iconfix' into 'master'
update the muwire icon which appears on the router console itself

See merge request zlatinb/muwire!48
2020-08-14 14:49:33 +00:00
idk
1f973cf076 Merge branch 'iconfix-redux' of i2pgit.org:idk/muwire into iconfix 2020-08-12 21:30:55 -04:00
idk
fdb64f5539 re-try muwire icon fix with update codebase 2020-08-12 21:28:20 -04:00
idk
5b4f3202d6 update the image and create a smaller, icon-ized version 2020-08-12 21:23:42 -04:00
idk
64eb2dad80 add larger muwire logo, use it for the home page 2020-08-11 22:12:52 -04:00
idk
ffe328eee6 update the muwire icon which appears on the router console itself 2020-08-11 19:26:53 -04:00
eb1f2fe19d escape download file name 2020-06-18 21:23:51 +01:00
17c59102ad one more --add-opens 2020-06-16 14:05:41 +01:00
26e8300d18 first pass at a collections proposal 2020-06-12 19:02:05 +01:00
47ac0fd9ac fix gui:run target 2020-06-11 19:16:48 +01:00
0b8b489169 read i2p.properties from file 2020-06-11 17:38:17 +01:00
fb32690c7c de-hardcode groovy versions 2020-06-06 14:01:32 +01:00
a11c504271 update all of the gui to groovy 3.0.4 2020-06-06 04:23:14 +01:00
76e726b520 switch to the Groovy json library 2020-06-05 20:29:39 +01:00
4f626615d8 disable translation instead of throwing an Error if the user is on development i2p router build 2020-06-05 20:05:03 +01:00
061a1a88dd disable indy because on groovy 3 it uses too much memory 2020-06-05 14:32:54 +01:00
ad20d7cf9a get rid of illegal reflective access warnings 2020-06-05 13:23:54 +01:00
895df6cf94 disable pack200 2020-06-05 02:35:04 +01:00
59b5d88829 shorter connect message 2020-06-03 12:58:16 +01:00
f382d2ecbf move the share button to the left next to the shared files count 2020-06-03 12:55:20 +01:00
6740d09479 checkboxes for font style 2020-06-03 12:44:56 +01:00
8cbada110e switch to gridBagLayout to hopefully avoid hiding of short name 2020-06-03 02:58:33 +01:00
33982dd24b preserve main frame dimensions across restarts 2020-06-03 02:22:59 +01:00
274edcc599 preserve initial font+size+style accross restarts 2020-06-03 02:03:44 +01:00
af218a369c migrate to groovy 3.0.4 2020-06-01 13:40:28 +01:00
f0aaa83b7f clean up most of noise on console when running without a log file 2020-06-01 13:14:33 +01:00
b9c34cb944 Add ability to specify default values for the wizard from system property. GitHub issue #32 2020-06-01 12:09:58 +01:00
59353a6718 update readme 2020-05-29 12:35:51 +01:00
c25546e1e1 Release 0.7.1 2020-05-29 12:06:53 +01:00
f9fb9e4f07 get rid of dead code 2020-05-29 12:04:55 +01:00
72f2b2bd37 fix manual searching for updates 2020-05-29 12:03:09 +01:00
eb242b0889 reduce default speed smoothing interval 2020-05-29 11:14:56 +01:00
6508522c9c tunnel quantity and length sliders step 2020-05-29 11:05:58 +01:00
f38b8217c2 steps for embedded or external router, formatting 2020-05-29 10:52:58 +01:00
c9c5e8617a Directory validation and creation 2020-05-29 01:51:13 +01:00
8c4bafda82 move the button enabling logic in view 2020-05-29 01:50:53 +01:00
c2044044c0 add a final step 2020-05-29 01:27:36 +01:00
cb54b30967 apply steps at the end, add ability to cancel wizard 2020-05-29 01:15:56 +01:00
c041f6baaa skeleton of setup wizard 2020-05-28 20:08:57 +01:00
bf28278f72 Release 0.7.0 2020-05-26 19:03:05 +01:00
6462675091 enable dual keys 2020-05-26 19:01:31 +01:00
5adf8d8276 up to router 0.9.46 2020-05-26 18:58:40 +01:00
2fbab55f68 remove references to tunnelName variable 2020-05-25 19:44:50 +01:00
0d783a6bcd harmonize some strings 2020-05-21 11:31:27 +01:00
017454c4b3 use sliders instead of fields in I2P settings 2020-05-18 14:39:16 +01:00
ec41985d31 Release 0.6.15 2020-05-17 22:36:08 +01:00
5daad35ee2 new icon 2020-05-17 14:30:23 +01:00
8df9f63bc7 new icons 2020-05-17 13:11:59 +01:00
367a43825f trim whitespaces before signing 2020-05-15 13:21:51 +01:00
7b34b0cffc link to mucats 2020-05-14 20:15:54 +01:00
bb6692c38e Merge branch 'strings' into 'master'
Prep for push to transifex

See merge request zlatinb/muwire!47
2020-05-13 15:31:37 +00:00
zzz
f1a2b103a8 Prep for push to transifex
Additional string cleanup, regenerate English po file
2020-05-13 10:29:18 -04:00
c1324c92ba Merge branch 'csp3' into 'master'
Fix script line changes from previous MR

See merge request zlatinb/muwire!46
2020-05-11 17:02:49 +00:00
zzz
179c3438cd Fix script line changes from previous MR
Fix some missing quotes
Move util.js and tables.js to css.jsi
script files don't require nonce; inline scripts do.
Nonce doesn't matter until we turn on the CSP.
2020-05-11 10:59:28 -04:00
7fa6812ee9 merge from zzz/csp2 2020-05-11 13:38:20 +01:00
zzz
a1c714b46e Replace innerHTML part 1 (Gitlab issue #45)
Change all plain text and empty content from innerHTML to textContent
2020-05-11 08:20:03 -04:00
4f7cf4fbfc Merge branch 'csp1' into 'master'
Plugin headers and CSP (Gitlab issue #44)

See merge request zlatinb/muwire!44
2020-05-11 12:04:25 +00:00
zzz
2d3e843d64 Plugin headers and CSP (Gitlab issue #44)
Prep for stricter script-src:
Add headers, remove js onload, move init call to the js
Add nonces to all scripts, can't use yet due to innerHTML (see Gitlab issue #45)
2020-05-11 07:50:36 -04:00
2e36812740 Merge branch 'sigtype' into 'master'
Signature must be constructed with the sigtype of the signing key

See merge request zlatinb/muwire!43
2020-05-10 11:15:12 +00:00
zzz
61340f346a Signature must be constructed with the sigtype of the signing key 2020-05-10 06:26:35 -04:00
992daa1e45 size limit on nicknames 2020-05-10 09:51:56 +01:00
3b825263a7 Make the random port selection range match that of the I2P router 2020-05-08 17:37:06 +01:00
e1bf6c0821 prevent invalid characters in searchers of persisted files from breaking the loading process. Related to GitHub issue #45 2020-05-08 17:33:57 +01:00
a6eca11479 Release 0.6.14 2020-05-07 13:39:17 +01:00
11aa6dda70 sign tool in web ui 2020-05-07 13:29:21 +01:00
3116e20c7c Fix i2np port change on every restart, github issue #45 2020-05-07 03:12:09 +01:00
58a92e7442 disallow certain characters in nicknames 2020-05-06 11:49:52 +01:00
d18cdb15cd disallow certain characters in nicknames 2020-05-06 11:39:08 +01:00
ed02b718d9 sign raw UTF-8 representation, removing size limit 2020-05-06 05:36:59 +01:00
564db3473c publish core to local maven repo 2020-05-05 16:01:23 +01:00
6d6063829a convert the core project into a library 2020-05-05 15:39:54 +01:00
ecaec1df3b sign tool 2020-05-04 23:16:32 +01:00
8b99f83db8 Release 0.6.13 2020-05-04 14:18:08 +01:00
33b159477a get test targets to pass, ignoring some tests which are not relevant anymore 2020-05-04 13:08:18 +01:00
91d8175cc5 fix method name 2020-05-04 13:07:32 +01:00
b4c6c77167 Ability to configure watched directories from swing gui 2020-05-04 12:15:27 +01:00
fb59d1ca0c fix the wait window while core is loading 2020-05-04 08:15:25 +01:00
3de4c65d2f Merge branch 'accordion' into 'master'
Accordion

See merge request zlatinb/muwire!41
2020-05-03 16:35:13 +00:00
zzz
91ea2c0184 Move accordion javascript to its own file
Open the accordion section for the page you are on
2020-05-03 12:16:05 -04:00
4a81a3539e Merge branch 'master' into 'master'
Clean up help text for consistency and translatability

See merge request zlatinb/muwire!40
2020-05-03 12:56:03 +00:00
zzz
fcfb506787 Clean up help text for consistency and translatability 2020-05-03 12:56:03 +00:00
zzz
44dc7b808f Clean up help text for consistency and translatability 2020-05-03 08:28:05 -04:00
339f4aaa3e Merge branch 'master' into 'master'
Add top-level groovy and java compile options to build.gradle

See merge request zlatinb/muwire!39
2020-05-02 15:20:25 +00:00
zzz
bf06c3b15f Add top-level groovy and java compile options to build.gradle
Add compilerArgs to gradle.properties
Fix compile warnings in DataUtil
2020-05-02 10:53:00 -04:00
b5e41d72b8 typo 2020-04-29 12:57:01 +01:00
2fe9309519 update README with link to Tracker wiki page 2020-04-29 12:55:17 +01:00
2410ed7199 Merge branch 'tracking-server-side' 2020-04-29 12:37:06 +01:00
9167c9edf7 add a max failuires parameter when deciding whether to expire a host. Report the number of negative hosts in the info rpc 2020-04-29 12:20:23 +01:00
028a8d5044 handle tracker pongs 2020-04-29 11:24:58 +01:00
356d7fe2ff always include the uuid in the tracker response 2020-04-29 11:23:42 +01:00
9da7a90653 wip on pinging swarm members 2020-04-29 07:26:51 +01:00
2001419f1a Iterate through the swarms in order of last pinged, get hosts which have not been pinged recently, also in chronological order 2020-04-29 06:07:22 +01:00
eec9bab081 Start work on timer-based swarm tracking 2020-04-29 05:21:18 +01:00
0a66267264 add missing dependency on java11 2020-04-29 03:47:01 +01:00
ad698cf1b9 use spring configuration for the tracker properties 2020-04-29 02:58:48 +01:00
fd9866c519 implement "info" json-rpc method 2020-04-29 02:03:50 +01:00
83bea0c823 report # of swarms in status, add forget method 2020-04-29 00:54:05 +01:00
71789d96d2 working injection and query kickoff through json-rpc, wip on swarm monitoring 2020-04-28 23:35:29 +01:00
7860aa2b1c prevent replay attacks by attaching an uuid to the crawler pings and pongs 2020-04-28 19:46:13 +01:00
301c2ec0e2 make I2PSession visible 2020-04-28 19:29:09 +01:00
c306864781 add type to the tracker pong and echo the infohash that was queried 2020-04-28 19:18:37 +01:00
acee9a5805 customize port and interface of web server 2020-04-28 18:26:36 +01:00
d34c4e1990 hello spring boot 2020-04-28 18:11:26 +01:00
7be3821e53 will use spring boot for json-rpc endpoints 2020-04-28 17:03:00 +01:00
872e932629 logging.properties for the hostcache and a script to count total hosts 2020-04-27 19:43:33 +01:00
84c7da1fe0 * More logging
* Include leaseset in crawler pings
* serialize hourly files in a directory, keep history
2020-04-26 20:15:48 +01:00
4aed958319 wip on tracker 2020-04-26 19:31:21 +01:00
5fc0283da7 revert change to constructor 2020-04-26 19:30:26 +01:00
c4d908f571 switch to simple-json-rpc library, add basic rpc server over tcp 2020-04-15 10:26:47 +01:00
4d5497c12f setup wizard 2020-04-14 13:18:13 +01:00
1d22abfa88 add ability to change the tunnel name 2020-04-14 13:17:47 +01:00
7a7ebc9690 skeleton of tracker project 2020-04-13 19:43:48 +01:00
16d3a109ca option to disable tracking in web ui 2020-04-12 11:40:21 +01:00
7864eebb24 gui option to disable tracking 2020-04-12 11:26:08 +01:00
9f7aaec991 include local persona in tracker response 2020-04-12 06:57:39 +01:00
1c214ad68a server side of file tracking 2020-04-12 05:56:06 +01:00
3436af75bf remove redundant header parsing code 2020-04-10 08:04:00 +01:00
9b6a2fd952 write to memmapped file in 8kb increments 2020-04-08 13:25:08 +01:00
85ad3109f9 get rid of sNL and darktrion hostcaches, add echelon's 2020-04-02 12:29:44 +01:00
293ff76ae9 Move the wait for client manager in the background thread, hopefully fixes #42 2020-03-30 13:28:22 +01:00
acb70f72d6 fix determination if a directory is shared 2020-03-30 12:42:16 +01:00
62bb4f9e5f actions dropdown on trust lists page 2020-03-29 21:16:54 +01:00
03d6fb15f2 Actions menu on TrustUsers page 2020-03-29 20:58:15 +01:00
699f3ce1b6 convert the Mark (Dis)Trusted links on search results page to hover menu 2020-03-29 19:15:34 +01:00
7f9c8bddb6 fix the color of the hover menu when hovering over a table 2020-03-29 13:18:40 +01:00
d111983d68 help text for each page 2020-03-28 23:33:11 +00:00
50148e5603 add a Help tooltip section in the header. To be updated with different text for each page 2020-03-28 22:51:07 +00:00
1054fe0935 x -> px 2020-03-28 20:19:01 +00:00
2de2badb0b tooltips on config options 2020-03-28 19:55:00 +00:00
424922f2e3 start adding tooltips to config options 2020-03-28 19:19:35 +00:00
adce4b1574 help tooltips on Browse and Feeds pages 2020-03-28 18:39:30 +00:00
355535e660 help tooltips on search box and share input box 2020-03-28 16:14:03 +00:00
09db68182c add a description of the advanced sharing page, wording and css tweaks 2020-03-28 03:02:48 +00:00
1e67139e74 display Never if directory was never synced 2020-03-28 02:52:22 +00:00
9837e1e3d7 emit an event on every dir sync so that UI can update timestamps 2020-03-28 02:47:29 +00:00
2c52486476 fix manual syncing 2020-03-27 15:47:58 +00:00
a88dc17064 add a sync option, fix sorting of table 2020-03-27 15:42:50 +00:00
862967bf8e configure panel for directories 2020-03-27 15:14:36 +00:00
9f1f718870 show the dirs in a table, no actions yet 2020-03-27 12:54:01 +00:00
2fd0a3833f wip on web ui for advanced sharing 2020-03-27 11:10:25 +00:00
435170cb1b update the advanced sharing pane 2020-03-26 17:32:42 +00:00
1c5fec7e9a Merge branch 'master' of 127.0.0.1:zlatinb/muwire into watched-directories
So that I can get B0B's icon
2020-03-26 15:41:34 +00:00
e2a0a37abf ui force sync event 2020-03-26 15:40:53 +00:00
a4bee73b8a process changes in configuration 2020-03-26 15:19:09 +00:00
056e5800c2 implement directory polling 2020-03-26 14:55:44 +00:00
6e0d51c221 first load all watched directories, only then register and scan the auto-watched 2020-03-26 13:10:56 +00:00
496e2e7f91 scan autoWatched directories on startup 2020-03-26 12:53:54 +00:00
a560b14d91 hook up directory manager with share & unshare events 2020-03-26 12:24:07 +00:00
faad6b6b0e query the manager if a directory is watched instead of settings 2020-03-26 12:23:15 +00:00
dfc62b943f wip on persisting and loading of watched directory metadata, emit the event to register on autowatch service 2020-03-26 06:21:41 +00:00
244ce43794 persistence of WatchedDirectory object 2020-03-26 05:31:39 +00:00
f0c8c11094 get rid of UI-side watching of directories on AllFilesLoadedEvent 2020-03-26 05:31:05 +00:00
11e320ef53 wip on directory watching 2020-03-26 04:09:18 +00:00
aae88e80ee Merge branch 'master' into 'master'
AdvancedSharing.png icons. Creative Commons CC0.

See merge request zlatinb/muwire!38
2020-03-25 23:37:51 +00:00
Bob
bbf97311d1 AdvancedSharing.png icons. Creative Commons CC0. 2020-03-25 23:37:51 +00:00
23b6995bf2 start work on advanced watched directories 2020-03-25 22:39:03 +00:00
518bdc44e6 update TODO 2020-03-25 20:27:51 +00:00
5368dbe181 CSS tweaks from B0B 2020-03-25 15:37:48 +00:00
191 changed files with 6214 additions and 1181 deletions

4
.gitignore vendored
View File

@ -2,7 +2,7 @@
**/.settings
**/build
.gradle
.project
.classpath
**/.project
**/.classpath
**/*.rej
**/*.orig

View File

@ -4,7 +4,7 @@ The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and is
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
The current stable release - 0.6.8 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
The current stable release - 0.7.4 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
@ -21,7 +21,7 @@ If you want to run the unit tests, type
./gradlew clean build
```
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project. If you want to package MuWire for a Linux distribution, see the [Packaging] wiki page.
## Running the GUI
@ -30,9 +30,7 @@ Type
./gradlew gui:run
```
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`
[Default I2CP port]\: `7654`
The setup wizard will ask you for the host and port of an I2P or I2Pd router.
## Running the CLI
@ -51,6 +49,12 @@ MuWire is available as a Docker image. For more information see the [Docker] pa
## Translations
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
## Related Projects
### MuWire Tracker Daemon
The MuWire Tracker Daemon (or mwtrackerd for short) is a project to bring functionality similar to BitTorrent tracking to MuWire. For more info see the [Tracker] page.
### MuCats
MuCats is a project to create a website for hosting hashes of files shared on the MuWire network. For more info see the [MuCats] project.
## GPG Fingerprint
```
@ -64,8 +68,11 @@ You can find the full key at https://keybase.io/zlatinb
[Wiki]: https://github.com/zlatinb/muwire/wiki
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[Packaging]: https://github.com/zlatinb/muwire/wiki/Packaging
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
[I2P Github]: https://github.com/i2p/i2p.i2p
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
[Tracker]: https://github.com/zlatinb/muwire/wiki/Tracker-Daemon
[MuCats]: https://github.com/zlatinb/mucats

12
TODO.md
View File

@ -15,16 +15,14 @@ This helps with scalability
* Metadata parsing and search
* Automatic adjustment of number of I2P tunnels
* Persist trust immediately
* Check if user-selected download and incomplete locations exist and are writeable
* Enum i18n
* Ability to share trust list only with trusted users
* Confidential files visible only to certain users
* Advertise file feed and browseability in upload headers
* Download queue with priorities
* Use tracker pings - either embedded logic or external mwtrackerd to add more sources to downloads
### Chat
* echo "unknown/innappropriate command" in the console
* break up lines on CR/LF, send multiple messages
* Style timestamps and persona names
* enforce # in room names or ignore it
* auto-create/join channel on server start
* jump from notification window to room with message
@ -33,7 +31,6 @@ This helps with scalability
* I2P Status panel - display message when connected to external router
* Search box - left identation
* Ability to disable switching of tabs on actions
* Ability to trust/browse/subscribe from uploads tab
### Web UI/Plugin
* HTML 5 media players
@ -42,4 +39,7 @@ This helps with scalability
* Upload files from browser to plugin via drag-and-drop
* Check permissions, display better errors when sharing local folders
### mwtrackerd
* `save` and `load` JSON-RPC commands that save and load swarm state respectively
* load-test with many many hashes (1M?)
* evaluate other usage scenarios besides website backend

View File

@ -2,13 +2,26 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile 'org.codehaus.groovy:groovy:2.4.15'
compile 'org.codehaus.groovy:groovy-jsr223:2.4.15'
compile 'org.codehaus.groovy:groovy-json:2.4.15'
compile "org.codehaus.groovy:groovy:${groovyVersion}"
compile "org.codehaus.groovy:groovy-jsr223:${groovyVersion}"
compile "org.codehaus.groovy:groovy-json:${groovyVersion}"
}
compileGroovy {
groovyOptions.optimizationOptions.indy = true
groovyOptions.optimizationOptions.indy = false
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
options.compilerArgs += project.compilerArgs
options.deprecation = true
options.encoding = 'UTF-8'
}
compileJava {
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
options.compilerArgs += project.compilerArgs
options.deprecation = true
options.encoding = 'UTF-8'
}
repositories {

View File

@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.6.12"
private static final String MW_VERSION = "0.7.4"
private static volatile Core core

View File

@ -28,7 +28,6 @@ class FilesModel {
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)
@ -38,15 +37,6 @@ class FilesModel {
}
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)

View File

@ -1,12 +1,42 @@
apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
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'
plugins {
id 'java-library'
id 'maven-publish'
}
dependencies {
api "net.i2p:i2p:${i2pVersion}"
api "net.i2p:router:${i2pVersion}"
api "net.i2p.client:mstreaming:${i2pVersion}"
implementation "net.i2p.client:streaming:${i2pVersion}"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testImplementation 'junit:junit:4.12'
testImplementation 'org.codehaus.groovy:groovy-all:3.0.4'
}
// this is necessary because applying both groovy and java-library doesn't work well
configurations {
apiElements.outgoing.variants {
classes {
artifact file: compileGroovy.destinationDir, builtBy: compileGroovy
}
}
}
configurations.testImplementation {
exclude group:'org.codehaus.groovy', module:'groovy-testng'
}
// publish core to local maven repo for sister projects
publishing {
publications {
muCore(MavenPublication) {
from components.java
}
}
repositories {
mavenLocal()
}
}

View File

@ -23,8 +23,10 @@ import com.muwire.core.connection.I2PAcceptor
import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadHopelessEvent
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
@ -55,7 +57,11 @@ import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.directories.UISyncDirectoryEvent
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
import com.muwire.core.files.directories.WatchedDirectoryConverter
import com.muwire.core.files.directories.WatchedDirectoryManager
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@ -66,6 +72,7 @@ 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.ResponderCache
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent
@ -81,6 +88,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.MuWireLogManager
import com.muwire.core.content.ContentControlEvent
import com.muwire.core.content.ContentManager
import com.muwire.core.tracker.TrackerResponder
import groovy.util.logging.Log
import net.i2p.I2PAppContext
@ -108,7 +116,8 @@ public class Core {
final Properties i2pOptions
final MuWireSettings muOptions
private final I2PSession i2pSession;
final I2PSession i2pSession;
private I2PSocketManager i2pSocketManager
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
@ -130,6 +139,9 @@ public class Core {
final ChatManager chatManager
final FeedManager feedManager
private final FeedClient feedClient
private final WatchedDirectoryConverter watchedDirectoryConverter
final WatchedDirectoryManager watchedDirectoryManager
private final TrackerResponder trackerResponder
private final Router router
@ -161,15 +173,17 @@ public class Core {
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
}
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
&& i2pOptions.hasProperty("i2np.udp.port")
if (!(i2pOptions.containsKey("i2np.ntcp.port")
&& i2pOptions.containsKey("i2np.udp.port")
)) {
Random r = new Random()
int port = r.nextInt(60000) + 4000
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
i2pOptions["i2np.udp.port"] = String.valueOf(port)
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
}
i2pOptions['i2cp.leaseSetEncType']='4,0'
if (!props.embeddedRouter) {
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
@ -210,14 +224,13 @@ public class Core {
// options like tunnel length and quantity
I2PSocketManager socketManager
keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
i2pSocketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
}
socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000)
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = socketManager.getSession()
i2pSocketManager.getDefaultOptions().setReadTimeout(60000)
i2pSocketManager.getDefaultOptions().setConnectTimeout(30000)
i2pSocketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = i2pSocketManager.getSession()
def destination = new Destination()
spk = new SigningPrivateKey(Constants.SIG_TYPE)
@ -274,6 +287,7 @@ public class Core {
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
eventBus.register(SourceDiscoveredEvent.class, meshManager)
eventBus.register(SourceVerifiedEvent.class, meshManager)
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
@ -296,10 +310,17 @@ public class Core {
eventBus.register(HostDiscoveredEvent.class, hostCache)
eventBus.register(ConnectionEvent.class, hostCache)
log.info("initializing responder cache")
ResponderCache responderCache = new ResponderCache(props.responderCacheSize)
eventBus.register(UIResultBatchEvent.class, responderCache)
eventBus.register(SourceVerifiedEvent.class, responderCache)
log.info("initializing connection manager")
connectionManager = props.isLeaf() ?
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
new UltrapeerConnectionManager(eventBus, me, props.peerConnections, props.leafConnections, hostCache, responderCache, trustService, props)
eventBus.register(TrustEvent.class, connectionManager)
eventBus.register(ConnectionEvent.class, connectionManager)
eventBus.register(DisconnectionEvent.class, connectionManager)
@ -308,16 +329,16 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
if (!props.plugin) {
if (!(props.plugin || props.disableUpdates)) {
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
} else
log.info("running as plugin, not initializing update client")
log.info("running as plugin or updates disabled, not initializing update client")
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
I2PConnector i2pConnector = new I2PConnector(i2pSocketManager)
log.info("initializing certificate client")
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
@ -352,24 +373,6 @@ public class Core {
eventBus.register(QueryEvent.class, searchManager)
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing chat manager")
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
eventBus.with {
@ -379,17 +382,33 @@ public class Core {
register(ChatDisconnectionEvent.class, chatManager)
}
log.info("initializing download manager")
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me, chatServer)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
eventBus.register(DownloadHopelessEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
log.info("initializing tracker responder")
trackerResponder = new TrackerResponder(i2pSession, props, fileManager, downloadManager, meshManager, trustService, me)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
I2PAcceptor i2pAcceptor = new I2PAcceptor(i2pSocketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager, chatServer)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
@ -411,6 +430,28 @@ public class Core {
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
eventBus.register(UIBrowseEvent.class, browseManager)
log.info("initializing watched directory converter")
watchedDirectoryConverter = new WatchedDirectoryConverter(this)
eventBus.register(AllFilesLoadedEvent.class, watchedDirectoryConverter)
log.info("initializing watched directory manager")
watchedDirectoryManager = new WatchedDirectoryManager(home, eventBus, fileManager)
eventBus.with {
register(WatchedDirectoryConfigurationEvent.class, watchedDirectoryManager)
register(WatchedDirectoryConvertedEvent.class, watchedDirectoryManager)
register(FileSharedEvent.class, watchedDirectoryManager)
register(DirectoryUnsharedEvent.class, watchedDirectoryManager)
register(UISyncDirectoryEvent.class, watchedDirectoryManager)
}
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, watchedDirectoryManager)
eventBus.with {
register(DirectoryWatchedEvent.class, directoryWatcher)
register(WatchedDirectoryConvertedEvent.class, directoryWatcher)
register(DirectoryUnsharedEvent.class, directoryWatcher)
register(WatchedDirectoryConfigurationEvent.class, directoryWatcher)
}
}
public void startServices() {
@ -427,6 +468,7 @@ public class Core {
updateClient?.start()
feedManager.start()
feedClient.start()
trackerResponder.start()
}
public void shutdown() {
@ -454,6 +496,8 @@ public class Core {
connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down watch directory manager")
watchedDirectoryManager.shutdown()
log.info("shutting down cache client")
cacheClient.stop()
log.info("shutting down chat server")
@ -464,10 +508,16 @@ public class Core {
feedManager.stop()
log.info("shutting down feed client")
feedClient.stop()
log.info("shutting down tracker responder")
trackerResponder.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
log.info("killing i2p session")
i2pSession.destroySession()
if (updateClient != null) {
log.info("shutting down update client")
updateClient.stop()
}
log.info("killing socket manager")
i2pSocketManager.destroySocketManager()
if (router != null) {
log.info("shutting down embedded router")
router.shutdown(0)
@ -511,7 +561,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.12")
Core core = new Core(props, home, "0.7.4")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -16,7 +16,7 @@ class MuWireSettings {
boolean allowTrustLists
int trustListInterval
Set<Persona> trustSubscriptions
int downloadRetryInterval
int downloadRetryInterval, downloadMaxFailures
int totalUploadSlots
int uploadSlotsPerUser
int updateCheckInterval
@ -31,15 +31,20 @@ class MuWireSettings {
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean allowTracking
boolean fileFeed
boolean advertiseFeed
boolean autoPublishSharedFiles
boolean defaultFeedAutoDownload
int defaultFeedUpdateInterval
long defaultFeedUpdateInterval
int defaultFeedItemsToKeep
boolean defaultFeedSequential
int peerConnections
int leafConnections
int responderCacheSize
boolean startChatServer
int maxChatConnections
@ -47,11 +52,12 @@ class MuWireSettings {
File chatWelcomeFile
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
int hostClearInterval, hostHopelessInterval, hostRejectInterval, hostHopelessPurgeInterval
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
boolean plugin
boolean disableUpdates
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
@ -75,6 +81,7 @@ class MuWireSettings {
if (incompleteLocationProp != null)
incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
downloadMaxFailures = Integer.parseInt(props.getProperty("downloadMaxFailures","10"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
@ -83,15 +90,18 @@ class MuWireSettings {
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "60"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
hostHopelessPurgeInterval = Integer.valueOf(props.getProperty("hostHopelessPurgeInterval","1440"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
disableUpdates = Boolean.valueOf(props.getProperty("disableUpdates","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"))
allowTracking = Boolean.valueOf(props.getProperty("allowTracking","true"))
// feed settings
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
@ -100,9 +110,16 @@ class MuWireSettings {
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60000"))
defaultFeedUpdateInterval = Long.valueOf(props.getProperty("defaultFeedUpdateInterval", "3600000"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
// ultrapeer connection settings
leafConnections = Integer.valueOf(props.getProperty("leafConnections","512"))
peerConnections = Integer.valueOf(props.getProperty("peerConnections","128"))
// responder cache settings
responderCacheSize = Integer.valueOf(props.getProperty("responderCacheSize","32"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","10"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
@ -140,6 +157,7 @@ class MuWireSettings {
if (incompleteLocation != null)
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("downloadMaxFailures", String.valueOf(downloadMaxFailures))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
@ -150,13 +168,16 @@ class MuWireSettings {
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("hostHopelessPurgeInterval", String.valueOf(hostHopelessPurgeInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("plugin", String.valueOf(plugin))
props.setProperty("disableUpdates", String.valueOf(disableUpdates))
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("allowTracking", String.valueOf(allowTracking))
// feed settings
props.setProperty("fileFeed", String.valueOf(fileFeed))
@ -167,6 +188,13 @@ class MuWireSettings {
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
// ultrapeer connection settings
props.setProperty("peerConnections", String.valueOf(peerConnections))
props.setProperty("leafConnections", String.valueOf(leafConnections))
// responder cache settings
props.setProperty("responderCacheSize", String.valueOf(responderCacheSize))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))

View File

@ -233,7 +233,7 @@ class ChatConnection implements ChatLink {
daos.close()
byte [] signed = baos.toByteArray()
def spk = sender.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
def signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, signed, spk)
}

View File

@ -29,6 +29,10 @@ class ChatManager {
timer.schedule({connect()} as TimerTask, 1000, 1000)
}
boolean isConnected(Persona p) {
clients.containsKey(p)
}
void onUIConnectChatEvent(UIConnectChatEvent e) {
if (e.host == me) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL,

View File

@ -60,6 +60,10 @@ class ChatServer {
echo(getWelcome(),me.destination)
}
public boolean isRunning() {
running.get()
}
private String getWelcome() {
String welcome = DEFAULT_WELCOME
if (settings.chatWelcomeFile != null)
@ -197,9 +201,19 @@ class ChatServer {
return
}
if ((command.action.console && e.room != CONSOLE) ||
(!command.action.console && e.room == CONSOLE) ||
!command.action.user)
if (command.action.console && e.room != CONSOLE) {
echo("/SAY ERROR: You can only execute that command in the chat console, not in a chat room.",
e.sender.destination, e.room)
return
}
if (!command.action.console && e.room == CONSOLE) {
echo("/SAY ERROR: You need to be in a chat room. Type /LIST for list of rooms or /JOIN to join or create a room.",
e.sender.destination)
return
}
if (!command.action.user)
return
if (command.action.local && e.sender != me)
@ -296,17 +310,17 @@ class ChatServer {
echo(help, d)
}
private void echo(String payload, Destination d) {
private void echo(String payload, Destination d, String room = CONSOLE) {
log.info "echoing $payload"
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
byte [] sig = ChatConnection.sign(uuid, now, CONSOLE, payload, me, me, spk)
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
ChatMessageEvent echo = new ChatMessageEvent(
uuid : uuid,
payload : payload,
sender : me,
host : me,
room : CONSOLE,
room : room,
chatTime : now,
sig : sig
)

View File

@ -49,6 +49,8 @@ abstract class Connection implements Closeable {
protected final String name
long lastPingSentTime, lastPongReceivedTime
private volatile UUID lastPingUUID
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
@ -132,6 +134,8 @@ abstract class Connection implements Closeable {
def ping = [:]
ping.type = "Ping"
ping.version = 1
lastPingUUID = UUID.randomUUID()
ping.uuid = lastPingUUID.toString()
messages.put(ping)
lastPingSentTime = System.currentTimeMillis()
}
@ -160,12 +164,14 @@ abstract class Connection implements Closeable {
messages.put(query)
}
protected void handlePing() {
protected void handlePing(def ping) {
log.fine("$name received ping")
def pong = [:]
pong.type = "Pong"
pong.version = 1
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
if (ping.uuid != null)
pong.uuid = ping.uuid
pong.pongs = hostCache.getGoodHosts(2).collect { d -> d.toBase64() }
messages.put(pong)
}
@ -174,7 +180,23 @@ abstract class Connection implements Closeable {
lastPongReceivedTime = System.currentTimeMillis()
if (pong.pongs == null)
throw new Exception("Pong doesn't have pongs")
pong.pongs.each {
if (lastPingUUID == null) {
log.fine "$name received an unexpected pong"
return
}
if (pong.uuid == null) {
log.fine "$name pong doesn't have uuid"
return
}
UUID pongUUID = UUID.fromString(pong.uuid)
if (pongUUID != lastPingUUID) {
log.fine "$name ping/pong uuid mismatch"
return
}
lastPingUUID = null
pong.pongs.stream().limit(2).forEach {
def dest = new Destination(it)
eventBus.publish(new HostDiscoveredEvent(destination: dest))
}
@ -244,7 +266,7 @@ abstract class Connection implements Closeable {
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
def signature = new Signature(spk.getType(), sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
@ -266,7 +288,7 @@ abstract class Connection implements Closeable {
queryTime = search.queryTime
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig2)
def signature = new Signature(spk.getType(), sig2)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("extended signature didn't match uuid and timestamp")
return

View File

@ -380,7 +380,7 @@ class ConnectionAcceptor {
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.running.get() && settings.advertiseChat
boolean chat = chatServer.isRunning() && settings.advertiseChat
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean feed = settings.fileFeed && settings.advertiseFeed

View File

@ -34,6 +34,8 @@ class ConnectionEstablisher {
final ExecutorService executor, closer
final Set inProgress = new ConcurrentHashSet()
private volatile boolean shutdown
ConnectionEstablisher(){}
@ -60,12 +62,15 @@ class ConnectionEstablisher {
}
void stop() {
shutdown = true
timer.cancel()
executor.shutdownNow()
closer.shutdownNow()
}
private void connectIfNeeded() {
if (shutdown)
return
if (!connectionManager.needsConnections())
return
if (inProgress.size() >= CONCURRENT)
@ -89,6 +94,8 @@ class ConnectionEstablisher {
}
private void connect(Destination toTry) {
if (shutdown)
return
log.info("starting connect to ${toTry.toBase32()}")
try {
def endpoint = i2pConnector.connect(toTry)
@ -123,6 +130,8 @@ class ConnectionEstablisher {
}
private void fail(Endpoint endpoint) {
if (shutdown)
return
if (!closer.isShutdown()) {
closer.execute {
endpoint.close()

View File

@ -53,7 +53,7 @@ class PeerConnection extends Connection {
if (json.type == null)
throw new Exception("missing json type")
switch(json.type) {
case "Ping" : handlePing(); break;
case "Ping" : handlePing(json); break;
case "Pong" : handlePong(json); break;
case "Search": handleSearch(json); break
default :

View File

@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResponderCache
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
@ -18,18 +19,22 @@ class UltrapeerConnectionManager extends ConnectionManager {
final int maxPeers, maxLeafs
final TrustService trustService
final ResponderCache responderCache
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
private final Random random = new Random()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
HostCache hostCache, ResponderCache responderCache, TrustService trustService, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxPeers = maxPeers
this.maxLeafs = maxLeafs
this.trustService = trustService
this.responderCache = responderCache
}
@Override
public void drop(Destination d) {
@ -44,8 +49,18 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (e.replyTo != me.destination && e.receivedOn != me.destination &&
!leafConnections.containsKey(e.receivedOn))
e.firstHop = false
final int connCount = peerConnections.size()
if (connCount == 0)
return
final int treshold = (int)(Math.sqrt(connCount)) + 1
peerConnections.values().each {
if (e.getReceivedOn() != it.getEndpoint().getDestination())
// 1. do not send query back to originator
// 2. if firstHop forward to everyone
// 3. otherwise to everyone who has recently responded/transferred to us + randomized sqrt of neighbors
if (e.getReceivedOn() != it.getEndpoint().getDestination() &&
(e.firstHop ||
responderCache.hasResponded(it.endpoint.destination) ||
random.nextInt(connCount) < treshold))
it.sendQuery(e)
}
}
@ -105,8 +120,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
@Override
void shutdown() {
super.shutdown()
peerConnections.values().stream().parallel().forEach({v -> v.close()})
leafConnections.values().stream().parallel().forEach({v -> v.close()})
peerConnections.values().stream().forEach({v -> v.close()})
leafConnections.values().stream().forEach({v -> v.close()})
peerConnections.clear()
leafConnections.clear()
}

View File

@ -0,0 +1,7 @@
package com.muwire.core.download
import com.muwire.core.Event
class DownloadHopelessEvent extends Event {
Downloader downloader
}

View File

@ -23,6 +23,8 @@ import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatServer
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
@ -35,16 +37,17 @@ public class DownloadManager {
private final EventBus eventBus
private final TrustService trustService
private final MeshManager meshManager
private final MuWireSettings muSettings
final MuWireSettings muSettings
private final I2PConnector connector
private final Executor executor
private final File home
private final Persona me
private final ChatServer chatServer
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
I2PConnector connector, File home, Persona me) {
I2PConnector connector, File home, Persona me, ChatServer chatServer) {
this.eventBus = eventBus
this.trustService = trustService
this.meshManager = meshManager
@ -52,6 +55,7 @@ public class DownloadManager {
this.connector = connector
this.home = home
this.me = me
this.chatServer = chatServer
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
@ -94,9 +98,9 @@ public class DownloadManager {
incompletes.mkdirs()
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
def downloader = new Downloader(eventBus, this, me, target, size,
def downloader = new Downloader(eventBus, this, chatServer, me, target, size,
infoHash, pieceSize, connector, destinations,
incompletes, pieces)
incompletes, pieces, muSettings.downloadMaxFailures)
downloaders.put(infoHash, downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
@ -108,6 +112,11 @@ public class DownloadManager {
persistDownloaders()
}
public void onDownloadHopelessEvent(DownloadHopelessEvent e) {
downloaders.remove(e.downloader.infoHash)
persistDownloaders()
}
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
persistDownloaders()
}
@ -158,8 +167,8 @@ public class DownloadManager {
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)
def downloader = new Downloader(eventBus, this, chatServer, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces, muSettings.downloadMaxFailures)
if (json.paused != null)
downloader.paused = json.paused

View File

@ -37,13 +37,15 @@ class DownloadSession {
private final long fileLength
private final Set<Integer> available
private final MessageDigest digest
private final boolean browse, feed, chat
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead,
boolean browse, boolean feed, boolean chat) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@ -54,6 +56,9 @@ class DownloadSession {
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
this.browse = browse
this.feed = feed
this.chat = chat
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@ -94,6 +99,12 @@ class DownloadSession {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
if (browse)
os.write("Browse: true\r\n".getBytes(StandardCharsets.US_ASCII))
if (feed)
os.write("Feed: true\r\n".getBytes(StandardCharsets.US_ASCII))
if (chat)
os.write("Chat: true\r\n".getBytes(StandardCharsets.US_ASCII))
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
@ -183,15 +194,14 @@ class DownloadSession {
mapped.position(position)
byte[] tmp = new byte[0x1 << 13]
DataInputStream dis = new DataInputStream(is)
while(mapped.hasRemaining()) {
if (mapped.remaining() < tmp.length)
tmp = new byte[mapped.remaining()]
int read = is.read(tmp)
if (read == -1)
throw new IOException()
dis.readFully(tmp)
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead.addAndGet(read)
mapped.put(tmp)
dataSinceLastRead.addAndGet(tmp.length)
pieces.markPartial(piece, mapped.position())
}
}
@ -205,6 +215,8 @@ class DownloadSession {
pieces.markPartial(piece, 0)
throw new BadHashException("bad hash on piece $piece")
}
eventBus.publish(new SourceVerifiedEvent(infoHash : infoHash, source : endpoint.destination))
} finally {
try { channel?.close() } catch (IOException ignore) {}
DataUtil.tryUnmap(mapped)

View File

@ -2,6 +2,8 @@ package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatServer
import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
@ -29,7 +31,7 @@ import net.i2p.util.ConcurrentHashSet
@Log
public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, HOPELESS, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@ -41,6 +43,7 @@ public class Downloader {
private final EventBus eventBus
private final DownloadManager downloadManager
private final ChatServer chatServer
private final Persona me
private final File file
private final Pieces pieces
@ -56,10 +59,14 @@ public class Downloader {
final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
/** LOCKING: itself */
private final Map<Destination, Integer> failingDestinations = new HashMap<>()
private final int maxFailures
private volatile boolean cancelled, paused
private final AtomicBoolean eventFired = new AtomicBoolean()
private final AtomicBoolean hopelessEventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
@ -68,13 +75,14 @@ public class Downloader {
private int speedPos = 0
private int speedAvg = 0
public Downloader(EventBus eventBus, DownloadManager downloadManager,
public Downloader(EventBus eventBus, DownloadManager downloadManager, ChatServer chatServer,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes, Pieces pieces) {
File incompletes, Pieces pieces, int maxFailures) {
this.eventBus = eventBus
this.me = me
this.downloadManager = downloadManager
this.chatServer = chatServer
this.file = file
this.infoHash = infoHash
this.length = length
@ -87,6 +95,7 @@ public class Downloader {
this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
this.nPieces = pieces.nPieces
this.maxFailures = maxFailures
}
public synchronized InfoHash getInfoHash() {
@ -116,7 +125,7 @@ public class Downloader {
void download() {
readPieces()
destinations.each {
if (it != me.destination) {
if (it != me.destination && !isHopeless(it)) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
@ -206,6 +215,8 @@ public class Downloader {
if (allFinished) {
if (pieces.isComplete())
return DownloadState.FINISHED
if (!hasLiveSources())
return DownloadState.HOPELESS
return DownloadState.FAILED
}
@ -269,11 +280,22 @@ public class Downloader {
public int getTotalWorkers() {
return activeWorkers.size();
}
public int countHopelessSources() {
synchronized(failingDestinations) {
return destinations.count { isHopeless(it)}
}
}
private boolean hasLiveSources() {
destinations.size() > countHopelessSources()
}
public void resume() {
paused = false
readPieces()
destinations.each { destination ->
destinations.stream().filter({!isHopeless(it)}).forEach { destination ->
log.fine("resuming source ${destination.toBase32()}")
def worker = activeWorkers.get(destination)
if (worker != null) {
if (worker.currentState == WorkerState.FINISHED) {
@ -290,8 +312,9 @@ public class Downloader {
}
void addSource(Destination d) {
if (activeWorkers.containsKey(d))
if (activeWorkers.containsKey(d) || isHopeless(d))
return
destinations.add(d)
DownloadWorker newWorker = new DownloadWorker(d)
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
@ -347,6 +370,28 @@ public class Downloader {
try {os?.close() } catch (IOException ignore) {}
}
}
private boolean isHopeless(Destination d) {
if (maxFailures < 0)
return false
synchronized(failingDestinations) {
return !successfulDestinations.contains(d) &&
failingDestinations.containsKey(d) &&
failingDestinations[d] >= maxFailures
}
}
private void markFailed(Destination d) {
log.fine("marking failed ${d.toBase32()}")
synchronized(failingDestinations) {
Integer count = failingDestinations.get(d)
if (count == null) {
failingDestinations.put(d, 1)
} else {
failingDestinations.put(d, count + 1)
}
}
}
class DownloadWorker implements Runnable {
private final Destination destination
@ -373,10 +418,16 @@ public class Downloader {
setInfoHash(received)
}
currentState = WorkerState.DOWNLOADING
boolean browse = downloadManager.muSettings.browseFiles
boolean feed = downloadManager.muSettings.fileFeed && downloadManager.muSettings.advertiseFeed
boolean chat = chatServer.isRunning() && downloadManager.muSettings.advertiseChat
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead,
browse, feed, chat)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@ -385,6 +436,9 @@ public class Downloader {
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
markFailed(destination)
if (!hasLiveSources() && hopelessEventFired.compareAndSet(false, true))
eventBus.publish(new DownloadHopelessEvent(downloader : Downloader.this))
} finally {
writePieces()
currentState = WorkerState.FINISHED

View File

@ -2,7 +2,7 @@ package com.muwire.core.download
class Pieces {
private final BitSet done, claimed
private final int nPieces
final int nPieces
private final float ratio
private final Random random = new Random()
private final Map<Integer,Integer> partials = new HashMap<>()

View File

@ -0,0 +1,11 @@
package com.muwire.core.download
import com.muwire.core.Event
import com.muwire.core.InfoHash
import net.i2p.data.Destination
class SourceVerifiedEvent extends Event {
InfoHash infoHash
Destination source
}

View File

@ -105,7 +105,7 @@ class Certificate {
byte [] payload = baos.toByteArray()
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
Signature signature = new Signature(Constants.SIG_TYPE, sig)
Signature signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, payload, spk)
}

View File

@ -121,8 +121,13 @@ abstract class BasePersisterService extends Service{
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
if (it.searcher != null) {
try {
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
} catch (Exception ignore) {
return
}
}
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)

View File

@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
import com.muwire.core.files.directories.WatchedDirectoryManager
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
@ -33,27 +36,27 @@ class DirectoryWatcher {
}
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus
private final FileManager fileManager
private final WatchedDirectoryManager watchedDirectoryManager
private final Thread watcherThread, publisherThread
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, WatchedDirectoryManager watchedDirectoryManager) {
this.home = home
this.muOptions = muOptions
this.eventBus = eventBus
this.fileManager = fileManager
this.watchedDirectoryManager = watchedDirectoryManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
watcherThread.setDaemon(true)
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
publisherThread.setDaemon(true)
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
watchService = FileSystems.getDefault().newWatchService()
watcherThread.start()
publisherThread.start()
@ -71,26 +74,26 @@ class DirectoryWatcher {
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(canonical, wk)
if (muOptions.watchedDirectories.add(canonical.toString()))
saveMuSettings()
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
if (muOptions.watchedDirectories.remove(e.directory.toString()))
saveMuSettings()
}
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
})
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
if (watchService == null)
return // still converting
if (!e.autoWatch) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
} else if (!watchedDirectories.containsKey(e.directory)) {
Path path = e.directory.toPath()
def wk = path.register(watchService, kinds)
watchedDirectories.put(e.directory, wk)
} // else it was already watched
}
private void watch() {
try {
while(!shutdown) {
@ -115,7 +118,7 @@ class DirectoryWatcher {
File f= join(parent, path)
log.fine("created entry $f")
if (f.isDirectory())
f.toPath().register(watchService, kinds)
eventBus.publish(new FileSharedEvent(file : f, fromWatch : true))
else
waitingFiles.put(f, System.currentTimeMillis())
}
@ -133,7 +136,7 @@ class DirectoryWatcher {
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
else if (muOptions.watchedDirectories.contains(f.toString()))
else if (watchedDirectoryManager.isWatched(f))
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
else
log.fine("Entry was not relevant");
@ -153,7 +156,7 @@ class DirectoryWatcher {
waitingFiles.each { file, timestamp ->
if (now - timestamp > WAIT_TIME) {
log.fine("publishing file $file")
eventBus.publish new FileSharedEvent(file : file)
eventBus.publish new FileSharedEvent(file : file, fromWatch: true)
published << file
}
}

View File

@ -5,9 +5,10 @@ import com.muwire.core.Event
class FileSharedEvent extends Event {
File file
boolean fromWatch
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
return super.toString() + " file: "+file.getAbsolutePath() + " fromWatch: $fromWatch"
}
}

View File

@ -53,7 +53,6 @@ class HasherService {
private void process(File f) {
if (f.isDirectory()) {
eventBus.publish(new DirectoryWatchedEvent(directory : f))
f.listFiles().each {
eventBus.publish new FileSharedEvent(file: it)
}

View File

@ -0,0 +1,7 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class UISyncDirectoryEvent extends Event {
File directory
}

View File

@ -0,0 +1,37 @@
package com.muwire.core.files.directories
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class WatchedDirectory {
final File directory
final String encodedName
boolean autoWatch
int syncInterval
long lastSync
WatchedDirectory(File directory) {
this.directory = directory.getCanonicalFile()
this.encodedName = Base64.encode(DataUtil.encodei18nString(directory.getAbsolutePath()))
}
def toJson() {
def rv = [:]
rv.directory = encodedName
rv.autoWatch = autoWatch
rv.syncInterval = syncInterval
rv.lastSync = lastSync
rv
}
static WatchedDirectory fromJson(def json) {
String dirName = DataUtil.readi18nString(Base64.decode(json.directory))
File dir = new File(dirName)
def rv = new WatchedDirectory(dir)
rv.autoWatch = json.autoWatch
rv.syncInterval = json.syncInterval
rv.lastSync = json.lastSync
rv
}
}

View File

@ -0,0 +1,9 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class WatchedDirectoryConfigurationEvent extends Event {
File directory
boolean autoWatch
int syncInterval
}

View File

@ -0,0 +1,10 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
/**
* Emitted when converting an old watched directory entry to the
* new format.
*/
class WatchedDirectoryConvertedEvent extends Event {
}

View File

@ -0,0 +1,27 @@
package com.muwire.core.files.directories
import com.muwire.core.Core
import com.muwire.core.files.AllFilesLoadedEvent
/**
* converts the setting-based format to new folder-based format.
*/
class WatchedDirectoryConverter {
private final Core core
WatchedDirectoryConverter(Core core) {
this.core = core
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
core.getMuOptions().getWatchedDirectories().each {
File directory = new File(it)
directory = directory.getCanonicalFile()
core.eventBus.publish(new WatchedDirectoryConfigurationEvent(directory : directory, autoWatch: true))
}
core.getMuOptions().getWatchedDirectories().clear()
core.saveMuSettings()
core.eventBus.publish(new WatchedDirectoryConvertedEvent())
}
}

View File

@ -0,0 +1,224 @@
package com.muwire.core.files.directories
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.stream.Stream
import com.muwire.core.EventBus
import com.muwire.core.SharedFile
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.FileListCallback
import com.muwire.core.files.FileManager
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
@Log
class WatchedDirectoryManager {
private final File home
private final EventBus eventBus
private final FileManager fileManager
private final Map<File, WatchedDirectory> watchedDirs = new ConcurrentHashMap<>()
private final ExecutorService diskIO = Executors.newSingleThreadExecutor({r ->
Thread t = new Thread(r, "disk-io")
t.setDaemon(true)
t
} as ThreadFactory)
private final Timer timer = new Timer("directory-timer", true)
private boolean converting = true
WatchedDirectoryManager(File home, EventBus eventBus, FileManager fileManager) {
this.home = new File(home, "directories")
this.home.mkdir()
this.eventBus = eventBus
this.fileManager = fileManager
}
public boolean isWatched(File f) {
watchedDirs.containsKey(f)
}
public Stream<WatchedDirectory> getWatchedDirsStream() {
watchedDirs.values().stream()
}
public void shutdown() {
diskIO.shutdown()
timer.cancel()
}
void onUISyncDirectoryEvent(UISyncDirectoryEvent e) {
def wd = watchedDirs.get(e.directory)
if (wd == null) {
log.warning("Got a sync event for non-watched dir ${e.directory}")
return
}
diskIO.submit({sync(wd, System.currentTimeMillis())} as Runnable)
}
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
if (converting) {
def newDir = new WatchedDirectory(e.directory)
// conversion is always autowatch really
newDir.autoWatch = e.autoWatch
persist(newDir)
} else {
def wd = watchedDirs.get(e.directory)
if (wd == null) {
log.severe("got a configuration event for a non-watched directory ${e.directory}")
return
}
wd.autoWatch = e.autoWatch
wd.syncInterval = e.syncInterval
persist(wd)
}
}
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
converting = false
diskIO.submit({
def slurper = new JsonSlurper()
Files.walk(home.toPath()).filter({
it.getFileName().toString().endsWith(".json")
}).
forEach {
def parsed = slurper.parse(it.toFile())
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
if (wd.directory.exists() && wd.directory.isDirectory()) // check if directory disappeared
watchedDirs.put(wd.directory, wd)
else
it.toFile().delete()
}
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
eventBus.publish(new FileSharedEvent(file : it.directory))
}
timer.schedule({sync()} as TimerTask, 1000, 1000)
} as Runnable)
}
private void persist(WatchedDirectory dir) {
diskIO.submit({doPersist(dir)} as Runnable)
}
private void doPersist(WatchedDirectory dir) {
def json = JsonOutput.toJson(dir.toJson())
def targetFile = new File(home, dir.getEncodedName() + ".json")
targetFile.text = json
}
void onFileSharedEvent(FileSharedEvent e) {
if (e.file.isFile() || watchedDirs.containsKey(e.file))
return
def wd = new WatchedDirectory(e.file)
if (e.fromWatch) {
// parent should be already watched, copy settings
def parent = watchedDirs.get(e.file.getParentFile())
if (parent == null) {
log.severe("watching found a directory without a watched parent? ${e.file}")
return
}
wd.autoWatch = parent.autoWatch
wd.syncInterval = parent.syncInterval
} else
wd.autoWatch = true
watchedDirs.put(wd.directory, wd)
persist(wd)
if (wd.autoWatch)
eventBus.publish(new DirectoryWatchedEvent(directory: wd.directory))
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
def wd = watchedDirs.remove(e.directory)
if (wd == null) {
log.warning("unshared a directory that wasn't watched? ${e.directory}")
return
}
File persistFile = new File(home, wd.getEncodedName() + ".json")
persistFile.delete()
}
private void sync() {
long now = System.currentTimeMillis()
watchedDirs.values().stream().
filter({!it.autoWatch}).
filter({it.syncInterval > 0}).
filter({it.lastSync + it.syncInterval * 1000 < now}).
forEach({wd -> diskIO.submit({sync(wd, now)} as Runnable )})
}
private void sync(WatchedDirectory wd, long now) {
log.fine("syncing ${wd.directory}")
wd.lastSync = now
doPersist(wd)
eventBus.publish(new WatchedDirectorySyncEvent(directory: wd.directory, when: now))
def cb = new DirSyncCallback()
fileManager.positiveTree.list(wd.directory, cb)
Set<File> filesOnFS = new HashSet<>()
Set<File> dirsOnFS = new HashSet<>()
wd.directory.listFiles().each {
File canonical = it.getCanonicalFile()
if (canonical.isFile())
filesOnFS.add(canonical)
else
dirsOnFS.add(canonical)
}
Set<File> addedFiles = new HashSet<>(filesOnFS)
addedFiles.removeAll(cb.files)
addedFiles.each {
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
}
Set<File> addedDirs = new HashSet<>(dirsOnFS)
addedDirs.removeAll(cb.dirs)
addedDirs.each {
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
}
Set<File> deletedFiles = new HashSet<>(cb.files)
deletedFiles.removeAll(filesOnFS)
deletedFiles.each {
eventBus.publish(new FileUnsharedEvent(unsharedFile : fileManager.getFileToSharedFile().get(it), deleted : true))
}
Set<File> deletedDirs = new HashSet<>(cb.dirs)
deletedDirs.removeAll(dirsOnFS)
deletedDirs.each {
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted: true))
}
}
private static class DirSyncCallback implements FileListCallback<SharedFile> {
private final Set<File> files = new HashSet<>()
private final Set<File> dirs = new HashSet<>()
@Override
public void onFile(File f, SharedFile value) {
files.add(f)
}
@Override
public void onDirectory(File f) {
dirs.add(f)
}
}
}

View File

@ -0,0 +1,8 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class WatchedDirectorySyncEvent extends Event {
File directory
long when
}

View File

@ -127,7 +127,8 @@ class CacheClient {
@Override
public void disconnected(I2PSession session) {
log.severe "I2P session disconnected"
if (!stopped.get())
log.severe "Cache client I2P session disconnected"
}
@Override

View File

@ -8,10 +8,8 @@ class CacheServers {
private static Set<Destination> CACHES = [
// zlatinb
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
// 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==")
// echelon
new Destination("2MJTl8gYVPK43iJZJa~-5K1OchgPaPHXpqZmKIiKFvxyy8BlIJzUSrF4mazdta--shFHISfT0PEeI95j1yDyKMpGxatUyjSt3ZnyTfAehQR-H2kYV9FvjHo68uA9X5AaGYHKRYLuWMkihMXygd8ywoLjZtFP0UbKMPggfOZaWmjHF4081XoUXt~7MEAeYSQowndiUx0AH3HxNEiv0N373JJS61OsIXb5ctqVKkwIiX1R0ZxESzpP9Xwp8-T0ou8fsLksygbKyH~3K1CyTHjTS51Ux-U-CjOPH9rtCOjjAaifdyMpK0PxW1fVdoGswFywTz9Q-6DUMsIu5TsPMF0-UO1Wn8vCpVAWbBJAOtKCfBrGzp-E~GCbfCNs5xY19nLobMD5ehjsBdI1lXwGDCQ7kBOwC58uuC3BOoazgrB6IrGskyMTexawtthO9mhuPm91bq4xhNaCYHAe059xg5emnM7jFBVzQgjaZ5lOLn~HqcWofJ7oc0doE6XI6kOo~YncBQAEAAcAAA==")
]
static List<Destination> getCacheServers() {

View File

@ -7,17 +7,19 @@ class Host {
private static final int MAX_FAILURES = 3
final Destination destination
private final int clearInterval, hopelessInterval, rejectionInterval
private final int clearInterval, hopelessInterval, rejectionInterval, purgeInterval
int failures,successes
long lastAttempt
long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval,
int purgeInterval) {
this.destination = destination
this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
this.purgeInterval = purgeInterval
}
private void connectSuccessful() {
@ -54,17 +56,22 @@ class Host {
failures = 0
}
synchronized boolean canTryAgain() {
synchronized boolean canTryAgain(final long now) {
lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
now - lastAttempt > (clearInterval * 60 * 1000)
}
synchronized boolean isHopeless() {
synchronized boolean isHopeless(final long now) {
isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
now - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
}
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
synchronized boolean isRecentlyRejected(final long now) {
now - lastRejection < (rejectionInterval * 60 * 1000)
}
synchronized boolean shouldBeForgotten(final long now) {
isHopeless(now) &&
now - lastAttempt > (purgeInterval * 60 * 1000)
}
}

View File

@ -52,7 +52,8 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures()
return
}
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
if (allowHost(host)) {
hosts.put(e.destination, host)
}
@ -64,7 +65,8 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)
if (host == null) {
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
hosts.put(dest, host)
}
@ -84,9 +86,10 @@ class HostCache extends Service {
List<Destination> getHosts(int n) {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {allowHost(hosts[it])}
final long now = System.currentTimeMillis()
rv.removeAll {
def h = hosts[it];
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
(h.isFailed() && !h.canTryAgain(now)) || h.isRecentlyRejected(now) || h.isHopeless(now)
}
if (rv.size() <= n)
return rv
@ -116,8 +119,9 @@ class HostCache extends Service {
int countHopelessHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
final long now = System.currentTimeMillis()
rv.retainAll {
hosts[it].isHopeless()
hosts[it].isHopeless(now)
}
rv.size()
}
@ -128,7 +132,8 @@ class HostCache extends Service {
storage.eachLine {
def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
@ -161,10 +166,12 @@ class HostCache extends Service {
}
private void save() {
final long now = System.currentTimeMillis()
hosts.keySet().removeAll { hosts[it].shouldBeForgotten(now) }
storage.delete()
storage.withPrintWriter { writer ->
hosts.each { dest, host ->
if (allowHost(host) && !host.isHopeless()) {
if (allowHost(host) && !host.isHopeless(now)) {
def map = [:]
map.destination = dest.toBase64()
map.failures = host.failures

View File

@ -1,28 +1,68 @@
package com.muwire.core.mesh
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Collectors
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
/**
* Representation of a download mesh.
*
* Two data structures - collection of known sources and collection of sources
* we have successfully transferred data with.
*
* @author zab
*/
class Mesh {
private final InfoHash infoHash
private final Set<Persona> sources = new ConcurrentHashSet<>()
private final Pieces pieces
private final Map<Destination,Persona> sources = new HashMap<>()
private final Set<Destination> verified = new HashSet<>()
final Pieces pieces
Mesh(InfoHash infoHash, Pieces pieces) {
this.infoHash = infoHash
this.pieces = pieces
}
Set<Persona> getRandom(int n, Persona exclude) {
List<Persona> tmp = new ArrayList<>(sources)
tmp.remove(exclude)
synchronized Set<Persona> getRandom(int n, Persona exclude) {
List<Destination> tmp = new ArrayList<>(verified)
if (exclude != null)
tmp.remove(exclude.destination)
tmp.retainAll(sources.keySet()) // verified may contain nodes not in sources
Collections.shuffle(tmp)
if (tmp.size() < n)
return tmp
tmp[0..n-1]
if (tmp.size() > n)
tmp = tmp[0..n-1]
tmp.collect(new HashSet<>(), { sources[it] })
}
synchronized void add(Persona persona) {
sources.put(persona.destination, persona)
}
synchronized void verify(Destination d) {
verified.add(d)
}
synchronized def toJson() {
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(infoHash.getRoot())
Set<Persona> toPersist = new HashSet<>(sources.values())
toPersist.retainAll { verified.contains(it.destination) }
json.sources = toPersist.collect {it.toBase64()}
json.nPieces = pieces.nPieces
List<Integer> downloaded = pieces.getDownloaded()
if( downloaded.size() > pieces.nPieces)
return null
json.xHave = DataUtil.encodeXHave(downloaded, pieces.nPieces)
json
}
}

View File

@ -9,6 +9,7 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.files.FileManager
import com.muwire.core.util.DataUtil
@ -56,25 +57,25 @@ class MeshManager {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.sources.add(e.source)
save()
mesh.add(e.source)
}
void onSourceVerifiedEvent(SourceVerifiedEvent e) {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.verify(e.source)
save()
}
private void save() {
File meshFile = new File(home, "mesh.json")
synchronized(meshes) {
meshFile.withPrintWriter { writer ->
meshes.values().each { mesh ->
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
json.nPieces = mesh.pieces.nPieces
List<Integer> downloaded = mesh.pieces.getDownloaded()
if( downloaded.size() > mesh.pieces.nPieces)
return
json.xHave = DataUtil.encodeXHave(downloaded, mesh.pieces.nPieces)
writer.println(JsonOutput.toJson(json))
def json = mesh.toJson()
if (json != null)
writer.println(JsonOutput.toJson(json))
}
}
}
@ -99,7 +100,8 @@ class MeshManager {
Mesh mesh = new Mesh(infoHash, pieces)
json.sources.each { source ->
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
mesh.sources.add(persona)
mesh.add(persona)
mesh.verify(persona.destination) // assume if persisted it was verified
}
if (json.xHave != null) {

View File

@ -0,0 +1,30 @@
package com.muwire.core.search
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.util.FixedSizeFIFOSet
import net.i2p.data.Destination
/**
* Caches destinations that have recently responded to with results.
*/
class ResponderCache {
private final FixedSizeFIFOSet<Destination> cache
ResponderCache(int capacity) {
cache = new FixedSizeFIFOSet<>(capacity)
}
synchronized void onUIResultBatchEvent(UIResultBatchEvent e) {
cache.add(e.results[0].sender.destination)
}
synchronized void onSourceVerifiedEvent(SourceVerifiedEvent e) {
cache.add(e.source)
}
synchronized boolean hasResponded(Destination d) {
cache.contains(d)
}
}

View File

@ -9,9 +9,11 @@ import com.muwire.core.Persona
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
if (json.type != "Result")
@ -103,6 +105,8 @@ class ResultsParser {
int certificates = 0
if (json.certificates != null)
certificates = json.certificates
log.fine("Received result from ${p.getHumanReadableName()} name \"$name\" infoHash:\"${json.infohash}\"")
return new UIResultEvent( sender : p,
name : name,

View File

@ -88,7 +88,7 @@ class ResultsSender {
sources : suggested,
comment : comment,
certificates : certificates,
chat : chatServer.running.get() && settings.advertiseChat,
chat : chatServer.isRunning() && settings.advertiseChat,
feed : settings.fileFeed && settings.advertiseFeed
)
uiResultEvents << uiResultEvent
@ -137,7 +137,7 @@ class ResultsSender {
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))
boolean chat = chatServer.running.get() && settings.advertiseChat
boolean chat = chatServer.isRunning() && settings.advertiseChat
os.write("Chat: $chat\r\n".getBytes(StandardCharsets.US_ASCII))
boolean feed = settings.fileFeed && settings.advertiseFeed
os.write("Feed: $feed\r\n".getBytes(StandardCharsets.US_ASCII))

View File

@ -0,0 +1,218 @@
package com.muwire.core.tracker
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Pieces
import com.muwire.core.files.FileManager
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.data.Base64
@Log
class TrackerResponder {
private final I2PSession i2pSession
private final MuWireSettings muSettings
private final FileManager fileManager
private final DownloadManager downloadManager
private final MeshManager meshManager
private final TrustService trustService
private final Persona me
private final Map<UUID,Long> uuids = new HashMap<>()
private final Timer expireTimer = new Timer("tracker-responder-timer", true)
private static final long UUID_LIFETIME = 10 * 60 * 1000
private volatile boolean shutdown
TrackerResponder(I2PSession i2pSession, MuWireSettings muSettings,
FileManager fileManager, DownloadManager downloadManager,
MeshManager meshManager, TrustService trustService,
Persona me) {
this.i2pSession = i2pSession
this.muSettings = muSettings
this.fileManager = fileManager
this.downloadManager = downloadManager
this.meshManager = meshManager
this.trustService = trustService
this.me = me
}
void start() {
i2pSession.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
expireTimer.schedule({expireUUIDs()} as TimerTask, UUID_LIFETIME, UUID_LIFETIME)
}
void stop() {
shutdown = true
expireTimer.cancel()
}
private void expireUUIDs() {
final long now = System.currentTimeMillis()
synchronized(uuids) {
for (Iterator<UUID> iter = uuids.keySet().iterator(); iter.hasNext();) {
UUID uuid = iter.next();
Long time = uuids.get(uuid)
if (now - time > UUID_LIFETIME)
iter.remove()
}
}
}
private void respond(host, json) {
log.info("responding to host $host with json $json")
def message = JsonOutput.toJson(json)
def maker = new I2PDatagramMaker(i2pSession)
message = maker.makeI2PDatagram(message.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(false)
i2pSession.sendMessage(host, message, 0, message.length, I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
}
class Listener implements I2PSessionMuxedListener {
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning "Received unexpected protocol $proto"
return
}
byte[] payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
log.info("got a tracker datagram from ${sender.toBase32()}")
// if not trusted, just drop it
TrustLevel trustLevel = trustService.getLevel(sender)
if (trustLevel == TrustLevel.DISTRUSTED ||
(trustLevel == TrustLevel.NEUTRAL && !muSettings.allowUntrusted)) {
log.info("dropping, untrusted")
return
}
payload = dissector.getPayload()
def slurper = new JsonSlurper()
def json = slurper.parse(payload)
if (json.type != "TrackerPing") {
log.warning("unknown type $json.type")
return
}
def response = [:]
response.type = "TrackerPong"
response.me = me.toBase64()
if (json.infoHash == null) {
log.warning("infoHash missing")
return
}
if (json.uuid == null) {
log.warning("uuid missing")
return
}
UUID uuid = UUID.fromString(json.uuid)
synchronized(uuids) {
if (uuids.containsKey(uuid)) {
log.warning("duplicate uuid $uuid")
return
}
uuids.put(uuid, System.currentTimeMillis())
}
response.uuid = json.uuid
if (!muSettings.allowTracking) {
response.code = 403
respond(sender, response)
return
}
if (json.version != 1) {
log.warning("unknown version $json.version")
response.code = 400
response.message = "I only support version 1"
respond(sender,response)
return
}
byte[] infoHashBytes = Base64.decode(json.infoHash)
InfoHash infoHash = new InfoHash(infoHashBytes)
log.info("servicing request for infoHash ${json.infoHash} with uuid ${json.uuid}")
if (!(fileManager.isShared(infoHash) || downloadManager.isDownloading(infoHash))) {
response.code = 404
respond(sender, response)
return
}
Mesh mesh = meshManager.get(infoHash)
if (fileManager.isShared(infoHash))
response.code = 200
else if (mesh != null) {
response.code = 206
Pieces pieces = mesh.getPieces()
response.xHave = DataUtil.encodeXHave(pieces, pieces.getnPieces())
}
if (mesh != null)
response.altlocs = mesh.getRandom(10, me).stream().map({it.toBase64()}).collect(Collectors.toList())
respond(sender,response)
} catch (Exception e) {
log.log(Level.WARNING, "invalid datagram", e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
if (!shutdown)
log.severe("Tracker Responder session disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}

View File

@ -2,6 +2,7 @@ package com.muwire.core.update
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
@ -48,6 +49,7 @@ class UpdateClient {
private volatile boolean updateDownloading
private volatile String text
private volatile boolean shutdown
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
FileManager fileManager, Persona me, SigningPrivateKey spk) {
@ -63,11 +65,12 @@ class UpdateClient {
}
void start() {
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT)
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
}
void stop() {
shutdown = true
timer.cancel()
}
@ -108,7 +111,7 @@ class UpdateClient {
ping = maker.makeI2PDatagram(ping.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT, 0, options)
}
class Listener implements I2PSessionMuxedListener {
@ -198,7 +201,8 @@ class UpdateClient {
@Override
public void disconnected(I2PSession session) {
log.severe("I2P session disconnected")
if (!shutdown)
log.severe("I2P session disconnected")
}
@Override

View File

@ -3,4 +3,5 @@ package com.muwire.core.upload
class ContentRequest extends Request {
Range range
int have
boolean browse, feed, chat
}

View File

@ -137,4 +137,24 @@ class ContentUploader extends Uploader {
request.infoHash == other.request.infoHash &&
request.getDownloader() == other.request.getDownloader()
}
@Override
public boolean isBrowseEnabled() {
request.browse
}
@Override
public boolean isFeedEnabled() {
request.feed
}
@Override
public boolean isChatEnabled() {
request.chat
}
@Override
public Persona getDownloaderPersona() {
request.downloader
}
}

View File

@ -4,6 +4,7 @@ import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import net.i2p.data.Base64
@ -75,4 +76,24 @@ class HashListUploader extends Uploader {
HashListUploader other = (HashListUploader)o
infoHash == other.infoHash && request.downloader == other.request.downloader
}
@Override
public boolean isBrowseEnabled() {
return false;
}
@Override
public boolean isFeedEnabled() {
return false;
}
@Override
public boolean isChatEnabled() {
return false;
}
@Override
public Persona getDownloaderPersona() {
request.downloader
}
}

View File

@ -22,7 +22,7 @@ class Request {
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
Map<String, String> headers = parseHeaders(is)
Map<String, String> headers = DataUtil.readAllHeaders(is)
if (!headers.containsKey("Range"))
throw new IOException("Range header not found")
@ -55,12 +55,18 @@ class Request {
def encoded = headers["X-Have"].trim()
have = DataUtil.decodeXHave(encoded).size()
}
boolean browse = headers.containsKey("Browse") && Boolean.parseBoolean(headers['Browse'])
boolean feed = headers.containsKey("Feed") && Boolean.parseBoolean(headers['Feed'])
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
headers : headers, downloader : downloader, have : have)
headers : headers, downloader : downloader, have : have,
browse : browse, feed : feed, chat : chat)
}
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
Map<String,String> headers = parseHeaders(is)
Map<String,String> headers = DataUtil.readAllHeaders(is)
Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
@ -69,55 +75,4 @@ class Request {
}
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
}
private static Map<String, String> parseHeaders(InputStream is) {
Map<String,String> headers = new HashMap<>()
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
while(headers.size() < Constants.MAX_HEADERS) {
boolean r = false
boolean n = false
int idx = 0
while (true) {
byte read = is.read()
if (read == -1)
throw new IOException("Stream closed")
if (!r && read == N)
throw new IOException("Received N before R")
if (read == R) {
if (r)
throw new IOException("double R")
r = true
continue
}
if (r && !n) {
if (read != N)
throw new IOException("R not followed by N")
n = true
break
}
if (idx == 0x1 << 14)
throw new IOException("Header too long")
tmp[idx++] = read
}
if (idx == 0)
break
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
log.fine("Read header $header")
int keyIdx = header.indexOf(":")
if (keyIdx < 1)
throw new IOException("Header key not found")
if (keyIdx == header.length())
throw new IOException("Header value not found")
String key = header.substring(0, keyIdx)
String value = header.substring(keyIdx + 1)
headers.put(key, value)
}
headers
}
}

View File

@ -11,6 +11,7 @@ import com.muwire.core.connection.Endpoint
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.files.FileManager
import com.muwire.core.files.PersisterFolderService
import com.muwire.core.mesh.Mesh
@ -123,6 +124,7 @@ public class UploadManager {
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
} finally {
decrementUploads(request.downloader)
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
@ -259,6 +261,7 @@ public class UploadManager {
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
} finally {
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}

View File

@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
abstract class Uploader {
@ -31,6 +32,7 @@ abstract class Uploader {
}
abstract String getName();
/**
* @return an integer between 0 and 100
@ -38,6 +40,7 @@ abstract class Uploader {
abstract int getProgress();
abstract String getDownloader();
abstract Persona getDownloaderPersona();
abstract int getDonePieces();
@ -45,6 +48,10 @@ abstract class Uploader {
abstract long getTotalSize();
abstract boolean isBrowseEnabled();
abstract boolean isFeedEnabled();
abstract boolean isChatEnabled();
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)

View File

@ -4,6 +4,8 @@ import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%";
public static final int MAX_NICKNAME_LENGTH = 30;
public static final byte FILE_CERT_VERSION = (byte)2;
public static final int CHAT_VERSION = 1;
@ -17,5 +19,8 @@ public class Constants {
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
public static final int UPDATE_PORT = 2;
public static final int TRACKER_PORT = 3;
}

View File

@ -0,0 +1,25 @@
package com.muwire.core;
public class InvalidNicknameException extends Exception {
public InvalidNicknameException() {
}
public InvalidNicknameException(String message) {
super(message);
}
public InvalidNicknameException(Throwable cause) {
super(cause);
}
public InvalidNicknameException(String message, Throwable cause) {
super(message, cause);
}
public InvalidNicknameException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -7,6 +7,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.muwire.core.util.DataUtil;
import net.i2p.crypto.DSAEngine;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
@ -25,12 +27,15 @@ public class Persona {
private volatile String base64;
private volatile byte[] payload;
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException {
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException, InvalidNicknameException {
version = (byte) (personaStream.read() & 0xFF);
if (version != Constants.PERSONA_VERSION)
throw new IOException("Unknown version "+version);
name = new Name(personaStream);
if (!DataUtil.isValidName(name.name))
throw new InvalidNicknameException(name.name + " is not a valid nickname");
destination = Destination.create(personaStream);
sig = new byte[SIG_LEN];
DataInputStream dis = new DataInputStream(personaStream);
@ -38,7 +43,7 @@ public class Persona {
if (!verify(version, name, destination, sig))
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify");
}
private static boolean verify(byte version, Name name, Destination destination, byte [] sig)
throws IOException, DataFormatException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -47,7 +52,7 @@ public class Persona {
destination.writeBytes(baos);
byte[] payload = baos.toByteArray();
SigningPublicKey spk = destination.getSigningPublicKey();
Signature signature = new Signature(Constants.SIG_TYPE, sig);
Signature signature = new Signature(spk.getType(), sig);
return DSAEngine.getInstance().verifySignature(signature, payload, spk);
}

View File

@ -6,7 +6,7 @@ public class Feed {
private final Persona publisher;
private int updateInterval;
private long updateInterval;
private long lastUpdated;
private volatile long lastUpdateAttempt;
private int itemsToKeep;
@ -19,11 +19,11 @@ public class Feed {
this.status = FeedFetchStatus.IDLE;
}
public int getUpdateInterval() {
public long getUpdateInterval() {
return updateInterval;
}
public void setUpdateInterval(int updateInterval) {
public void setUpdateInterval(long updateInterval) {
this.updateInterval = updateInterval;
}

View File

@ -58,9 +58,9 @@ public class DataUtil {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF);
return ((header[0] & 0x7F) << 16) |
((header[1] & 0xFF) << 8) |
(header[2] & 0xFF);
}
public static String readi18nString(byte [] encoded) {
@ -174,7 +174,7 @@ public class DataUtil {
clean.setAccessible(true);
clean.invoke(cleaner.invoke(cb));
} else {
Class unsafeClass;
Class<?> unsafeClass;
try {
unsafeClass = Class.forName("sun.misc.Unsafe");
} catch(Exception ex) {
@ -216,4 +216,13 @@ public class DataUtil {
Signature sig = DSAEngine.getInstance().sign(payload, spk);
return sig.getData();
}
public static boolean isValidName(String name) {
if (name.length() > Constants.MAX_NICKNAME_LENGTH)
return false;
for (int i = 0; i < Constants.INVALID_NICKNAME_CHARS.length(); i++)
if (name.indexOf(Constants.INVALID_NICKNAME_CHARS.charAt(i)) >= 0)
return false;
return true;
}
}

View File

@ -0,0 +1,35 @@
package com.muwire.core.util;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
public class FixedSizeFIFOSet<T> {
private final int capacity;
private final Set<T> set = new HashSet<>();
private final Deque<T> fifo = new ArrayDeque<>();
public FixedSizeFIFOSet(final int capacity) {
this.capacity = capacity;
}
public boolean contains(T element) {
return set.contains(element);
}
public void add(T element) {
if (!set.contains(element)) {
if (set.size() == capacity) {
T toRemove = fifo.removeLast();
set.remove(toRemove);
}
fifo.addFirst(element);
set.add(element);
} else {
fifo.remove(element);
fifo.addFirst(element);
}
}
}

View File

@ -46,7 +46,7 @@ class DownloadSessionTest {
eventBus = new EventBus()
}
private void initSession(int size, def claimedPieces = []) {
private void initSession(int size, def claimedPieces = [], boolean browse = false, boolean feed = false, boolean chat = false) {
Random r = new Random()
byte [] content = new byte[size]
r.nextBytes(content)
@ -78,7 +78,8 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong(),
browse, feed, chat)
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()

View File

@ -39,13 +39,13 @@ class FileManagerTest {
@Test
void testHash1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih, 0)
byte [] root = new byte[32]
SharedFile sf = new SharedFile(f,root, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
UUID uuid = UUID.randomUUID()
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
manager.onSearchEvent(se)
Thread.sleep(20)
@ -58,14 +58,14 @@ class FileManagerTest {
@Test
void testHash2Results() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
byte [] root = new byte[32]
SharedFile sf1 = new SharedFile(new File("a b.c"), root, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), root, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
UUID uuid = UUID.randomUUID()
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
manager.onSearchEvent(se)
Thread.sleep(20)
@ -81,7 +81,7 @@ class FileManagerTest {
void testHash0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih, 0)
SharedFile sf = new SharedFile(f,ih.getRoot(), 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -95,7 +95,7 @@ class FileManagerTest {
void testKeyword1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih,0)
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -113,12 +113,12 @@ class FileManagerTest {
void testKeyword2Results() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
UUID uuid = UUID.randomUUID()
@ -136,7 +136,7 @@ class FileManagerTest {
void testKeyword0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih,0)
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@ -149,8 +149,8 @@ class FileManagerTest {
@Test
void testRemoveFileExistingHash() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih.getRoot(), 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@ -167,12 +167,12 @@ class FileManagerTest {
void testRemoveFile() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
@ -198,7 +198,7 @@ class FileManagerTest {
comment = Base64.encode(DataUtil.encodei18nString(comment))
File f1 = new File("MuWire-0.5.10.AppImage")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
sf1.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
@ -206,7 +206,7 @@ class FileManagerTest {
File f2 = new File("MuWire-0.6.0.AppImage")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
sf2.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))

View File

@ -45,7 +45,7 @@ class HasherServiceTest {
def hashed = listener.poll()
assert hashed instanceof FileHashedEvent
assert hashed.sharedFile.file == f.getCanonicalFile()
assert hashed.sharedFile.infoHash != null
assert hashed.sharedFile.root != null
assert listener.isEmpty()
}

View File

@ -85,7 +85,7 @@ class PersisterServiceLoadingTest {
def loadedFile = listener.publishedFiles[0]
assert loadedFile != null
assert loadedFile.file == sharedFile1.getCanonicalFile()
assert loadedFile.infoHash == ih1
assert loadedFile.root == ih1.getRoot()
}
private static String getSharedFileJsonName(File sharedFile) {
@ -128,7 +128,7 @@ class PersisterServiceLoadingTest {
def loadedFile = listener.publishedFiles[0]
assert loadedFile != null
assert loadedFile.file == sharedFile1.getCanonicalFile()
assert loadedFile.infoHash == ih1
assert loadedFile.root == ih1.getRoot()
}
@Test
@ -169,10 +169,10 @@ class PersisterServiceLoadingTest {
assert listener.publishedFiles.size() == 2
def loadedFile1 = listener.publishedFiles[0]
assert loadedFile1.file == sharedFile1.getCanonicalFile()
assert loadedFile1.infoHash == ih1
assert loadedFile1.root == ih1.getRoot()
def loadedFile2 = listener.publishedFiles[1]
assert loadedFile2.file == sharedFile2.getCanonicalFile()
assert loadedFile2.infoHash == ih2
assert loadedFile2.root == ih2.getRoot()
}
@Test

View File

@ -2,6 +2,7 @@ package com.muwire.core.files
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import com.muwire.core.Destinations
@ -16,6 +17,7 @@ import groovy.json.JsonSlurper
import net.i2p.data.Base32
import net.i2p.data.Base64
@Ignore
class PersisterServiceSavingTest {
File f

View File

@ -75,6 +75,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@ -97,6 +98,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@ -114,6 +116,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@ -136,6 +139,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -160,6 +164,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 100 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -182,6 +187,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -211,6 +217,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -246,6 +253,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -266,6 +274,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@ -301,6 +310,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
def rv = cache.getHosts(5)

View File

@ -0,0 +1,49 @@
package com.muwire.core.util
import org.junit.Test
class FixedSizeFIFOSetTest {
@Test
public void testFifo() {
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
fifoSet.add("a")
assert fifoSet.contains("a")
fifoSet.add("b")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
fifoSet.add("c")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
fifoSet.add("d")
assert !fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
assert fifoSet.contains("d")
}
@Test
public void testDuplicateElement() {
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
fifoSet.add("a")
fifoSet.add("b")
fifoSet.add("c")
fifoSet.add("a")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
fifoSet.add("d")
assert fifoSet.contains("a")
assert !fifoSet.contains("b")
assert fifoSet.contains("c")
assert fifoSet.contains("d")
}
}

49
doc/collections.md Normal file
View File

@ -0,0 +1,49 @@
# MuWire Collections
Status: Draft, Proposal, Unimplemented
MuWire collections are files containing meta-information about a grouping of files. They serve a similar purpose like .torrent files, but the internal format is rather different to account for the MuWire identity management.
A user wishing to create a collection of files needs to have shared all the files that are going to be part of the collection. Their full MuWire ID will be stored in the collection, so anyone wishing to download any of the files in the collection will try to download from them first.
The collection will be signed, so anyone can verify that the embedded full MuWire ID authored the collection.
### File Format
Header:
```
byte 0: Collection version, currently fixed at "1".
bytes 1,2 : unsigned 16-bit value of the number of files in the collection. Empty files or directories are not allowed.
bytes 3-N: Full MuWire ID of the publisher of the collection, in Persona format.
bytes N+1 to N+9: Timestamp of the collection, in milliseconds since epoch UTC
bytes N+9 to M: Free-form description of the collection (comment). Format is UTF-8, maximum size is 32kb.
```
The header is followed by a file entry for each file in the collection. The format is the follows:
```
byte 0: File entry version, currently fixed at "1".
byte 1-33: hash of the file
byte 34: Unsigned 8-bit number of path elements from root to where the file will ultimately be placed upon download.
bytes 35-N : UTF-8 encoded length-prefixed path elements. Each element can be at most 32kb long. The last element is the name of the file.
bytes N-M: free from description of the file (comment). Format is UTF-8, maximum size is 32kb.
```
After the file entries follows a footer, which is simply a signature of the byte payload of the header and the file entries.
### Downloading
Since the collection is created from individual shared files, every file within the collection is searchable. It is possible to extend the shared file data structure to contain refererences to any collections the file belongs to - TBD.
When a user searches for a keyword or hash, they can find either the collection metafile itself or a file which is a member of one or more collections. In the latter case, the user is given the option to download the collection metafile.
If the user chooses to download the collection metafile, they will be presented with a dialog containing the metainformation contained in the collection descriptor. They will be able to see directory structure contained in the collection and will be able to choose individual files to download.
TBD - what happens when some of the files are already downloaded but are not in the final directory location?
Finally, when starting the download, the downloader always queries the persona in the collection first, regardless of who returned the search result.
### Sharing
When downloading the collection descriptor, the user makes the descriptor available for indexing. This way collection descriptors can propagate on the network.
TBD - do they also index the comments and file names in the descriptor, even if they haven't downloaded the files?

View File

@ -1,15 +1,17 @@
group = com.muwire
version = 0.6.12
i2pVersion = 0.9.45
groovyVersion = 2.4.15
version = 0.7.4
i2pVersion = 0.9.47
groovyVersion = 3.0.4
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
griffonEnv=prod
# javac properties
sourceCompatibility=1.8
targetCompatibility=1.8
compilerArgs=-Xlint:unchecked,cast,path,divzero,empty,path,finally,overrides
# plugin properties
author = zab@mail.i2p
@ -18,4 +20,4 @@ keystorePassword=changeit
websiteURL=http://muwire.i2p
updateURLsu3=http://muwire.i2p/MuWire-update.su3
pack200=true
pack200=false

View File

@ -42,10 +42,25 @@ griffon {
application {
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire'
}
run {
applicationDefaultJvmArgs=[]
}
startScripts.doFirst {
application.applicationDefaultJvmArgs = ["-Djava.util.logging.config.file=logging.properties",
"-Xmx256M",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.base/sun.nio.fs=ALL-UNNAMED",
"--add-opens", "java.base/java.nio=ALL-UNNAMED",
"--add-opens", "java.desktop/java.awt=ALL-UNNAMED",
"--add-opens", "java.desktop/javax.swing=ALL-UNNAMED",
"--add-opens", "java.desktop/javax.swing.plaf.basic=ALL-UNNAMED"]
}
apply from: 'gradle/publishing.gradle'
// apply from: 'gradle/code-coverage.gradle'
// apply from: 'gradle/code-quality.gradle'
@ -57,9 +72,27 @@ apply plugin: 'org.kordamp.gradle.stats'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'com.github.kt3k.coveralls'
configurations.all {
exclude group:'org.codehaus.groovy', module:'groovy-test'
exclude group:'org.codehaus.groovy', module:'groovy-testng'
exclude group:'org.codehaus.groovy', module:'groovy-test-junit5'
exclude group:'org.codehaus.groovy', module:'groovy-ant'
exclude group:'org.codehaus.groovy', module:'groovy-sql'
exclude group:'org.codehaus.groovy', module:'groovy-nio'
exclude group:'org.codehaus.groovy', module:'groovy-servlet'
exclude group:'org.codehaus.groovy', module:'groovy-jmx'
exclude group:'org.codehaus.groovy', module:'groovy-groovydoc'
exclude group:'org.codehaus.groovy', module:'groovy-groovysh'
exclude group:'org.codehaus.groovy', module:'groovy-xml'
exclude group:'org.codehaus.groovy', module:'groovy-docgenerator'
// TODO: add more as discovered
}
dependencies {
compile project(":core")
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
// runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
@ -68,6 +101,9 @@ dependencies {
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
// because java 14 doesn't come with it
runtime 'mrj:MRJToolkitStubs:1.0'
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}"
testCompile('org.awaitility:awaitility-groovy:3.1.0') {

View File

@ -131,4 +131,19 @@ mvcGroups {
view = 'com.muwire.gui.FeedConfigurationView'
controller = 'com.muwire.gui.FeedConfigurationController'
}
'watched-directory' {
model = 'com.muwire.gui.WatchedDirectoryModel'
view = 'com.muwire.gui.WatchedDirectoryView'
controller = 'com.muwire.gui.WatchedDirectoryController'
}
'sign' {
model = 'com.muwire.gui.SignModel'
view = 'com.muwire.gui.SignView'
controller = 'com.muwire.gui.SignController'
}
'wizard' {
model = 'com.muwire.gui.wizard.WizardModel'
view = 'com.muwire.gui.wizard.WizardView'
controller = 'com.muwire.gui.wizard.WizardController'
}
}

View File

@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.directories.UISyncDirectoryEvent
@ArtifactProviderFor(GriffonController)
class AdvancedSharingController {
@ -14,4 +15,25 @@ class AdvancedSharingController {
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingView view
@ControllerAction
void configure() {
def wd = view.selectedWatchedDirectory()
if (wd == null)
return
def params = [:]
params['core'] = model.core
params['directory'] = wd
mvcGroup.createMVCGroup("watched-directory",params)
}
@ControllerAction
void sync() {
def wd = view.selectedWatchedDirectory()
if (wd == null)
return
def event = new UISyncDirectoryEvent(directory : wd.directory)
model.core.eventBus.publish(event)
}
}

View File

@ -15,6 +15,7 @@ import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import javax.swing.text.StyledDocument
import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand
@ -54,14 +55,17 @@ class ChatRoomController {
long now = System.currentTimeMillis()
if (command.action == ChatAction.SAY && command.payload.length() > 0) {
String toShow = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + "> "+command.payload
String header = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + ">"
StyledDocument sd = view.roomTextArea.getStyledDocument()
sd.insertString(sd.getEndPosition().getOffset() - 1, header, sd.getStyle("italic"))
sd.insertString(sd.getEndPosition().getOffset() - 1, " ", sd.getStyle("regular"))
sd.insertString(sd.getEndPosition().getOffset() - 1, command.payload, sd.getStyle("regular"))
sd.insertString(sd.getEndPosition().getOffset() - 1, "\n", sd.getStyle("regular"))
view.roomTextArea.append(toShow)
view.roomTextArea.append('\n')
trimLines()
}
if (command.action == ChatAction.JOIN) {
if (command.action == ChatAction.JOIN && model.console) {
String newRoom = command.payload
String groupId = model.host.getHumanReadableName()+"-"+newRoom
if (!mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
@ -190,9 +194,8 @@ class ChatRoomController {
}
private void processSay(ChatMessageEvent e, String text) {
String toDisplay = DataHelper.formatTime(e.timestamp) + " <"+e.sender.getHumanReadableName()+"> " + text + "\n"
runInsideUIAsync {
view.roomTextArea.append(toDisplay)
view.appendSay(text, e.sender, e.timestamp)
trimLines()
if (!model.console)
view.chatNotificator.onMessage(mvcGroup.mvcId)
@ -203,7 +206,7 @@ class ChatRoomController {
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " joined the room\n"
runInsideUIAsync {
model.members.add(p)
view.roomTextArea.append(toDisplay)
view.appendGray(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
@ -223,7 +226,7 @@ class ChatRoomController {
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " left the room\n"
runInsideUIAsync {
model.members.remove(p)
view.roomTextArea.append(toDisplay)
view.appendGray(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
@ -233,7 +236,7 @@ class ChatRoomController {
String toDisplay = DataHelper.formatTime(System.currentTimeMillis()) + " " + p.getHumanReadableName() + " disconnected\n"
runInsideUIAsync {
if (model.members.remove(p)) {
view.roomTextArea.append(toDisplay)
view.appendGray(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
@ -243,11 +246,8 @@ class ChatRoomController {
private void trimLines() {
if (model.settings.maxChatLines < 0)
return
while(view.roomTextArea.getLineCount() > model.settings.maxChatLines) {
int line0Start = view.roomTextArea.getLineStartOffset(0)
int line0End = view.roomTextArea.getLineEndOffset(0)
view.roomTextArea.replaceRange(null, line0Start, line0End)
}
while(view.getLineCount() > model.settings.maxChatLines)
view.removeFirstLine()
}
void rejoinRoom() {

View File

@ -21,7 +21,7 @@ class FeedConfigurationController {
model.feed.setAutoDownload(view.autoDownloadCheckbox.model.isSelected())
model.feed.setSequential(view.sequentialCheckbox.model.isSelected())
model.feed.setItemsToKeep(Integer.parseInt(view.itemsToKeepField.text))
model.feed.setUpdateInterval(Integer.parseInt(view.updateIntervalField.text) * 60000)
model.feed.setUpdateInterval(Long.parseLong(view.updateIntervalField.text) * 60000)
model.core.eventBus.publish(new UIFeedConfigurationEvent(feed : model.feed))

View File

@ -33,6 +33,7 @@ import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.filefeeds.Feed
import com.muwire.core.filefeeds.FeedItem
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
import com.muwire.core.filefeeds.UIFeedDeletedEvent
import com.muwire.core.filefeeds.UIFeedUpdateEvent
import com.muwire.core.filefeeds.UIFilePublishedEvent
@ -61,13 +62,6 @@ class MainFrameController {
private volatile Core core
@ControllerAction
void clearSearch() {
def searchField = builder.getVariable("search-field")
searchField.setSelectedItem(null)
searchField.requestFocus()
}
@ControllerAction
void search(ActionEvent evt) {
if (evt?.getActionCommand() == null)
@ -151,6 +145,7 @@ class MainFrameController {
params["search-terms"] = tabTitle
params["uuid"] = uuid.toString()
params["core"] = core
params["settings"] = view.settings
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
@ -227,17 +222,13 @@ class MainFrameController {
@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.downloads.removeAll {
def state = it.downloader.getCurrentState()
state == Downloader.DownloadState.CANCELLED ||
state == Downloader.DownloadState.FINISHED ||
state == Downloader.DownloadState.HOPELESS
}
model.clearButtonEnabled = false
}
@ -362,6 +353,47 @@ class MainFrameController {
startChat(p)
view.showChatWindow.call()
}
@ControllerAction
void browseFromUpload() {
Uploader u = view.selectedUploader()
if (u == null)
return
Persona p = u.getDownloaderPersona()
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
}
@ControllerAction
void subscribeFromUpload() {
Uploader u = view.selectedUploader()
if (u == null)
return
Persona p = u.getDownloaderPersona()
Feed feed = new Feed(p)
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
feed.setSequential(core.muOptions.defaultFeedSequential)
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval)
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed : true))
view.showFeedsWindow.call()
}
@ControllerAction
void chatFromUpload() {
Uploader u = view.selectedUploader()
if (u == null)
return
Persona p = u.getDownloaderPersona()
startChat(p)
view.showChatWindow.call()
}
void unshareSelectedFile() {
def sf = view.selectedSharedFiles()

View File

@ -11,6 +11,7 @@ import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import java.awt.Font
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
@ -32,22 +33,14 @@ class OptionsController {
def i2pProps = core.i2pOptions
text = view.inboundLengthField.text
model.inboundLength = text
i2pProps["inbound.length"] = text
text = view.inboundQuantityField.text
model.inboundQuantity = text
i2pProps["inbound.quantity"] = text
text = view.outboundQuantityField.text
model.outboundQuantity = text
i2pProps["outbound.quantity"] = text
text = view.outboundLengthField.text
model.outboundLength = text
i2pProps["outbound.length"] = text
int tunnelLength = view.tunnelLengthSlider.value
i2pProps["inbound.length"] = String.valueOf(tunnelLength)
i2pProps["outbound.length"] = String.valueOf(tunnelLength)
int tunnelQuantity = view.tunnelQuantitySlider.value
i2pProps["inbound.quantity"] = String.valueOf(tunnelQuantity)
i2pProps["outbound.quantity"] = String.valueOf(tunnelQuantity)
if (settings.embeddedRouter) {
text = view.i2pNTCPPortField.text
model.i2pNTCPPort = text
@ -66,8 +59,11 @@ class OptionsController {
text = view.retryField.text
model.downloadRetryInterval = text
settings.downloadRetryInterval = Integer.valueOf(text)
text = view.downloadMaxFailuresField.text
model.downloadMaxFailures = text
settings.downloadMaxFailures = Integer.valueOf(text)
text = view.updateField.text
model.updateCheckInterval = text
@ -104,6 +100,10 @@ class OptionsController {
model.browseFiles = browseFiles
settings.browseFiles = browseFiles
boolean allowTracking = view.allowTrackingCheckbox.model.isSelected()
model.allowTracking = allowTracking
settings.allowTracking = allowTracking
text = view.speedSmoothSecondsField.text
model.speedSmoothSeconds = Integer.valueOf(text)
settings.speedSmoothSeconds = Integer.valueOf(text)
@ -151,7 +151,7 @@ class OptionsController {
String defaultFeedUpdateInterval = view.defaultFeedUpdateIntervalField.text
model.defaultFeedUpdateInterval = defaultFeedUpdateInterval
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval)
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval) * 60000L
// trust saving
@ -204,6 +204,12 @@ class OptionsController {
uiSettings.autoFontSize = model.automaticFontSize
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
uiSettings.fontStyle = Font.PLAIN
if (view.fontStyleBoldCheckbox.model.isSelected())
uiSettings.fontStyle |= Font.BOLD
if (view.fontStyleItalicCheckbox.model.isSelected())
uiSettings.fontStyle |= Font.ITALIC
uiSettings.groupByFile = model.groupByFile

View File

@ -119,7 +119,7 @@ class SearchTabController {
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
feed.setSequential(core.muOptions.defaultFeedSequential)
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval * 60 * 1000)
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval)
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed: true))
mvcGroup.parentGroup.view.showFeedsWindow.call()

View File

@ -0,0 +1,50 @@
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.crypto.DSAEngine
import net.i2p.data.Base64
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class SignController {
Core core
@MVCMember @Nonnull
SignView view
@ControllerAction
void sign() {
String plain = view.plainTextArea.getText()
byte[] payload = plain.trim().getBytes(StandardCharsets.UTF_8)
def sig = DSAEngine.getInstance().sign(payload, core.spk)
view.signedTextArea.setText(Base64.encode(sig.data))
}
@ControllerAction
void copy() {
String signed = view.signedTextArea.getText()
StringSelection selection = new StringSelection(signed)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -0,0 +1,33 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
@ArtifactProviderFor(GriffonController)
class WatchedDirectoryController {
@MVCMember @Nonnull
WatchedDirectoryModel model
@MVCMember @Nonnull
WatchedDirectoryView view
@ControllerAction
void save() {
def event = new WatchedDirectoryConfigurationEvent(
directory : model.directory.directory,
autoWatch : view.autoWatchCheckbox.model.isSelected(),
syncInterval : Integer.parseInt(view.syncIntervalField.text))
model.core.eventBus.publish(event)
cancel()
}
@ControllerAction
void cancel() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -0,0 +1,49 @@
package com.muwire.gui.wizard
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.swing.JOptionPane
@ArtifactProviderFor(GriffonController)
class WizardController {
@MVCMember @Nonnull
WizardModel model
@MVCMember @Nonnull
WizardView view
@ControllerAction
void previous() {
model.currentStep--
view.updateLayout()
}
@ControllerAction
void next() {
def errors = model.steps[model.currentStep].validate()
if (errors != null && !errors.isEmpty()) {
String errorMessage = String.join("\n", errors)
JOptionPane.showMessageDialog(model.parent, errorMessage, "Invalid Input", JOptionPane.ERROR_MESSAGE)
} else {
model.currentStep++
view.updateLayout()
}
}
@ControllerAction
void finish() {
model.steps.each {
it.apply(model.muSettings, model.i2pProps)
}
model.finished['applied'] = true
view.hide()
}
@ControllerAction
void cancel() {
model.finished['applied'] = false
view.hide()
}
}

View File

@ -40,15 +40,6 @@ 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())) {
@ -133,7 +124,7 @@ class Initialize extends AbstractLifecycleHandler {
uiSettings.lnf = lnf.getID()
}
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0 ) {
FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
@ -151,7 +142,7 @@ class Initialize extends AbstractLifecycleHandler {
fontSize = uiSettings.fontSize
}
rowHeight = fontSize + 3
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
FontUIResource font = new FontUIResource(fontName, uiSettings.fontStyle, fontSize)
def keys = lnf.getDefaults().keys()
while(keys.hasMoreElements()) {
@ -167,21 +158,16 @@ class Initialize extends AbstractLifecycleHandler {
Properties props = new Properties()
uiSettings = new UISettings(props)
log.info "will try default lnfs"
if (isMacOSX()) {
if (SystemVersion.isJava9()) {
uiSettings.lnf = "metal"
lookAndFeel("metal")
} else {
uiSettings.lnf = "nimbus"
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
}
} else {
LookAndFeel chosen = lookAndFeel('system', 'gtk')
if (chosen == null)
chosen = lookAndFeel('metal')
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
}
LookAndFeel chosen = lookAndFeel('system', 'gtk', 'metal')
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
FontUIResource defaultFont = chosen.getDefaults().getFont("Label.font")
uiSettings.font = defaultFont.getName()
uiSettings.fontSize = defaultFont.getSize()
uiSettings.fontStyle = defaultFont.getStyle()
rowHeight = uiSettings.fontSize + 3
}
application.context.put("row-height", rowHeight)

View File

@ -6,10 +6,13 @@ import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.util.DataUtil
import com.muwire.gui.wizard.WizardDefaults
import javax.annotation.Nonnull
import javax.inject.Inject
@ -44,36 +47,53 @@ class Ready extends AbstractLifecycleHandler {
propsFile.withReader("UTF-8", {
props.load(it)
})
if (!props.containsKey("nickname"))
props.setProperty("nickname", selectNickname())
props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
if (System.getProperties().containsKey("disableUpdates"))
props.disableUpdates = Boolean.valueOf(System.getProperty("disableUpdates"))
} 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")
props.setNickname(selectNickname())
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv != JFileChooser.APPROVE_OPTION) {
JOptionPane.showMessageDialog(null, "MuWire will now exit")
System.exit(0)
}
props.downloadLocation = chooser.getSelectedFile()
boolean embeddedRouterAvailable = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
def defaults
if (System.getProperties().containsKey("wizard.defaults")) {
File defaultsFile = new File(System.getProperty("wizard.defaults"))
Properties defaultsProps = new Properties()
defaultsFile.withInputStream { defaultsProps.load(it) }
defaults = new WizardDefaults(defaultsProps)
} else
defaults = new WizardDefaults()
def parent = application.windowManager.findWindow("event-list")
Properties i2pProps = new Properties()
def params = [:]
params['parent'] = parent
params['defaults'] = defaults
params['embeddedRouterAvailable'] = embeddedRouterAvailable
params['muSettings'] = props
params['i2pProps'] = i2pProps
def finished = [:]
params['finished'] = finished
application.mvcGroupManager.createMVCGroup("wizard", params)
if (!finished['applied']) {
JOptionPane.showMessageDialog(parent, "MuWire will now exit")
System.exit(0)
}
File i2pPropsFile = new File(home, "i2p.properties")
i2pPropsFile.withPrintWriter { i2pProps.store(it, "") }
props.embeddedRouter = embeddedRouterAvailable
props.updateType = System.getProperty("updateType","jar")
props.disableUpdates = Boolean.parseBoolean(System.getProperty("disableUpdates", "false"))
propsFile.withPrintWriter("UTF-8", {
props.write(it)
})
@ -100,31 +120,5 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0)
}
}
private String selectNickname() {
String nickname
while (true) {
nickname = JOptionPane.showInputDialog(null,
"Your nickname is displayed when you send search results so other MuWire users can choose to trust you",
"Please choose a nickname", JOptionPane.PLAIN_MESSAGE)
if (nickname == null) {
JOptionPane.showMessageDialog(null, "MuWire cannot start without a nickname and will now exit", JOptionPane.PLAIN_MESSAGE)
System.exit(0)
}
if (nickname.trim().length() == 0) {
JOptionPane.showMessageDialog(null, "Nickname cannot be empty", "Select another nickname",
JOptionPane.WARNING_MESSAGE)
continue
}
if (nickname.contains("@")) {
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
"Select another nickname", JOptionPane.WARNING_MESSAGE)
continue
}
nickname = nickname.trim()
break
}
nickname
}
}

View File

@ -1,32 +1,50 @@
package com.muwire.gui
import javax.annotation.Nonnull
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import com.muwire.core.Core
import com.muwire.core.files.FileTree
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectorySyncEvent
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class AdvancedSharingModel {
@MVCMember @Nonnull
AdvancedSharingView view
def watchedDirectories = []
def treeRoot
def negativeTree
Core core
@Observable boolean syncActionEnabled
@Observable boolean configureActionEnabled
void mvcGroupInit(Map<String,String> args) {
watchedDirectories.addAll(core.muOptions.watchedDirectories)
watchedDirectories.addAll(core.watchedDirectoryManager.watchedDirs.values())
core.eventBus.register(WatchedDirectorySyncEvent.class, this)
core.eventBus.register(WatchedDirectoryConfigurationEvent.class, this)
treeRoot = new DefaultMutableTreeNode()
negativeTree = new DefaultTreeModel(treeRoot)
copyTree(treeRoot, core.fileManager.negativeTree.root)
}
void mvcGroupDestroy() {
core.eventBus.unregister(WatchedDirectorySyncEvent.class, this)
core.eventBus.unregister(WatchedDirectoryConfigurationEvent.class, this)
}
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
jtreeNode.setUserObject(fileTreeNode.file?.getName())
fileTreeNode.children.each {
@ -36,4 +54,16 @@ class AdvancedSharingModel {
}
}
void onWatchedDirectorySyncEvent(WatchedDirectorySyncEvent e) {
runInsideUIAsync {
view.watchedDirsTable.model.fireTableDataChanged()
}
}
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
runInsideUIAsync {
view.watchedDirsTable.model.fireTableDataChanged()
}
}
}

View File

@ -177,22 +177,25 @@ class MainFrameModel {
if (!mvcGroup.alive)
return
// remove cancelled or finished downloads
// remove cancelled or finished or hopeless downloads
if (!clearButtonEnabled || uiSettings.clearCancelledDownloads || uiSettings.clearFinishedDownloads) {
def toRemove = []
downloads.each {
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
def state = it.downloader.getCurrentState()
if (state == Downloader.DownloadState.CANCELLED) {
if (uiSettings.clearCancelledDownloads) {
toRemove << it
} else {
clearButtonEnabled = true
}
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
} else if (state == Downloader.DownloadState.FINISHED) {
if (uiSettings.clearFinishedDownloads) {
toRemove << it
} else {
clearButtonEnabled = true
}
} else if (state == Downloader.DownloadState.HOPELESS) {
clearButtonEnabled = true
}
}
toRemove.each {
@ -245,7 +248,7 @@ class MainFrameModel {
core.eventBus.publish(new ContentControlEvent(term : it, regex: true, add: true))
}
chatServerRunning = core.chatServer.running.get()
chatServerRunning = core.chatServer.isRunning()
timer.schedule({
if (core.shutdown.get())
@ -294,8 +297,6 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
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))
}
@ -415,7 +416,7 @@ class MainFrameModel {
break
if (parent.getChildCount() == 0) {
File file = parent.getUserObject().file
if (core.muOptions.watchedDirectories.contains(file.toString()))
if (core.watchedDirectoryManager.isWatched(file))
unshared.add(file)
dmtn = parent
continue
@ -503,6 +504,9 @@ class MainFrameModel {
}
void onQueryEvent(QueryEvent e) {
if (!uiSettings.showMonitor)
return
if (e.replyTo == core.me.destination)
return

View File

@ -7,9 +7,12 @@ import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
import java.awt.Font
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
@Observable String downloadMaxFailures
@Observable String updateCheckInterval
@Observable boolean autoDownloadUpdate
@Observable boolean shareDownloadedFiles
@ -18,16 +21,15 @@ class OptionsModel {
@Observable String incompleteLocation
@Observable boolean searchComments
@Observable boolean browseFiles
@Observable boolean allowTracking
@Observable int speedSmoothSeconds
@Observable int totalUploadSlots
@Observable int uploadSlotsPerUser
@Observable boolean storeSearchHistory
// i2p options
@Observable String inboundLength
@Observable String inboundQuantity
@Observable String outboundLength
@Observable String outboundQuantity
@Observable int tunnelLength
@Observable int tunnelQuantity
@Observable String i2pUDPPort
@Observable String i2pNTCPPort
@ -37,6 +39,8 @@ class OptionsModel {
@Observable String font
@Observable boolean automaticFontSize
@Observable int customFontSize
@Observable boolean fontStyleBold
@Observable boolean fontStyleItalic
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@ -71,10 +75,13 @@ class OptionsModel {
@Observable boolean advertiseChat
@Observable int maxChatLines
@Observable String chatWelcomeFile
boolean disableUpdates
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
downloadMaxFailures = settings.downloadMaxFailures
updateCheckInterval = settings.updateCheckInterval
autoDownloadUpdate = settings.autoDownloadUpdate
shareDownloadedFiles = settings.shareDownloadedFiles
@ -83,15 +90,14 @@ class OptionsModel {
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
allowTracking = settings.allowTracking
speedSmoothSeconds = settings.speedSmoothSeconds
totalUploadSlots = settings.totalUploadSlots
uploadSlotsPerUser = settings.uploadSlotsPerUser
Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"]
inboundQuantity = core.i2pOptions["inbound.quantity"]
outboundLength = core.i2pOptions["outbound.length"]
outboundQuantity = core.i2pOptions["outbound.quantity"]
tunnelLength = Math.max(Integer.parseInt(core.i2pOptions["inbound.length"]), Integer.parseInt(core.i2pOptions['outbound.length']))
tunnelQuantity = Math.max(Integer.parseInt(core.i2pOptions["inbound.quantity"]), Integer.parseInt(core.i2pOptions['outbound.quantity']))
i2pUDPPort = core.i2pOptions["i2np.udp.port"]
i2pNTCPPort = core.i2pOptions["i2np.ntcp.port"]
@ -101,6 +107,8 @@ class OptionsModel {
font = uiSettings.font
automaticFontSize = uiSettings.autoFontSize
customFontSize = uiSettings.fontSize
fontStyleBold = (uiSettings.fontStyle & Font.BOLD) == Font.BOLD
fontStyleItalic = (uiSettings.fontStyle & Font.ITALIC) == Font.ITALIC
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
@ -121,7 +129,7 @@ class OptionsModel {
defaultFeedAutoDownload = settings.defaultFeedAutoDownload
defaultFeedItemsToKeep = String.valueOf(settings.defaultFeedItemsToKeep)
defaultFeedSequential = settings.defaultFeedSequential
defaultFeedUpdateInterval = String.valueOf(settings.defaultFeedUpdateInterval)
defaultFeedUpdateInterval = String.valueOf(Math.max(1L, (long)(settings.defaultFeedUpdateInterval / 60000L)))
onlyTrusted = !settings.allowUntrusted()
searchExtraHop = settings.searchExtraHop
@ -133,5 +141,7 @@ class OptionsModel {
advertiseChat = settings.advertiseChat
maxChatLines = uiSettings.maxChatLines
chatWelcomeFile = settings.chatWelcomeFile?.getAbsolutePath()
disableUpdates = settings.disableUpdates
}
}

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

View File

@ -0,0 +1,22 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.files.directories.WatchedDirectory
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class WatchedDirectoryModel {
Core core
WatchedDirectory directory
@Observable boolean autoWatch
@Observable int syncInterval
void mvcGroupInit(Map<String,String> args) {
autoWatch = directory.autoWatch
syncInterval = directory.syncInterval
}
}

View File

@ -0,0 +1,43 @@
package com.muwire.gui.wizard
import java.awt.Component
import com.muwire.core.MuWireSettings
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class WizardModel {
Component parent
WizardDefaults defaults
boolean embeddedRouterAvailable
MuWireSettings muSettings
Properties i2pProps
def finished
final List<WizardStep> steps = []
int currentStep
@Observable boolean finishButtonEnabled
@Observable boolean previousButtonEnabled
@Observable boolean nextButtonEnabled
void mvcGroupInit(Map<String,String> args) {
steps << new NicknameStep()
steps << new DirectoriesStep(defaults)
if (embeddedRouterAvailable)
steps << new EmbeddedRouterStep(defaults)
else
steps << new ExternalRouterStep(defaults)
steps << new TunnelStep(defaults)
steps << new LastStep(embeddedRouterAvailable)
currentStep = 0
previousButtonEnabled = false
nextButtonEnabled = steps.size() > (currentStep + 1)
finishButtonEnabled = steps.size() == currentStep + 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -3,13 +3,23 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTabbedPane
import javax.swing.JTree
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.files.directories.WatchedDirectory
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
@ -21,6 +31,8 @@ class AdvancedSharingView {
FactoryBuilderSupport builder
@MVCMember @Nonnull
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingController controller
def mainFrame
def dialog
@ -28,6 +40,7 @@ class AdvancedSharingView {
def negativeTreePanel
def watchedDirsTable
def watchedDirsTableSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
@ -43,10 +56,17 @@ class AdvancedSharingView {
scrollPane( constraints : BorderLayout.CENTER ) {
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.watchedDirectories) {
closureColumn(header : "Directory", type : String, read : {it})
closureColumn(header : "Directory", preferredWidth: 350, type : String, read : {it.directory.toString()})
closureColumn(header : "Auto", preferredWidth: 100, type : Boolean, read : {it.autoWatch})
closureColumn(header : "Interval", preferredWidth : 100, type : Integer, read : {it.syncInterval})
closureColumn(header : "Last Sync", preferredWidth: 250, type : Long, read : {it.lastSync})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Configure", enabled : bind{model.configureActionEnabled}, configureAction)
button(text : "Sync", enabled : bind{model.syncActionEnabled}, syncAction)
}
}
negativeTreePanel = builder.panel {
@ -59,6 +79,55 @@ class AdvancedSharingView {
tree(rootVisible : false, rowHeight : rowHeight,jtree)
}
}
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
watchedDirsTable.setDefaultRenderer(Long.class, new DateRenderer())
watchedDirsTable.setDefaultRenderer(Integer.class, centerRenderer)
watchedDirsTable.rowSorter.addRowSorterListener({evt -> watchedDirsTableSortEvent = evt})
def selectionModel = watchedDirsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
def directory = selectedWatchedDirectory()
model.syncActionEnabled = !(directory == null || directory.autoWatch)
model.configureActionEnabled = directory != null
})
watchedDirsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
}
private void showMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
JMenuItem configure = new JMenuItem("Configure")
configure.addActionListener({controller.configure()})
menu.add(configure)
if (model.syncActionEnabled) {
JMenuItem sync = new JMenuItem("Sync")
sync.addActionListener({controller.sync()})
menu.add(sync)
}
menu.show(e.getComponent(), e.getX(), e.getY())
}
WatchedDirectory selectedWatchedDirectory() {
int row = watchedDirsTable.getSelectedRow()
if (row < 0)
return null
if (watchedDirsTableSortEvent != null)
row = watchedDirsTable.rowSorter.convertRowIndexToModel(row)
model.watchedDirectories[row]
}
void mvcGroupInit(Map<String,String> args) {

View File

@ -3,18 +3,26 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTextPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.text.Element
import javax.swing.text.Style
import javax.swing.text.StyleConstants
import javax.swing.text.StyleContext
import javax.swing.text.StyledDocument
import javax.swing.SpringLayout.Constraints
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionAttemptStatus
import java.awt.BorderLayout
import java.awt.Color
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
@ -34,7 +42,7 @@ class ChatRoomView {
def pane
def parent
def sayField
def roomTextArea
JTextPane roomTextArea
def textScrollPane
def membersTable
def lastMembersTableSortEvent
@ -48,7 +56,7 @@ class ChatRoomView {
panel(constraints : BorderLayout.CENTER) {
gridLayout(rows : 1, cols : 1)
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
roomTextArea = textPane(editable : false)
}
}
panel(constraints : BorderLayout.SOUTH) {
@ -78,7 +86,7 @@ class ChatRoomView {
panel {
gridLayout(rows : 1, cols : 1)
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
roomTextArea = textPane(editable : false)
}
}
}
@ -135,6 +143,15 @@ class ChatRoomView {
}
})
}
// styles
StyledDocument document = roomTextArea.getStyledDocument()
Style regular = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE)
Style italic = document.addStyle("italic", regular)
StyleConstants.setItalic(italic, true)
Style gray = document.addStyle("gray", regular)
StyleConstants.setForeground(gray, Color.GRAY)
}
private void showPopupMenu(MouseEvent e) {
@ -179,4 +196,28 @@ class ChatRoomView {
chatNotificator.roomClosed(mvcGroup.mvcId)
mvcGroup.destroy()
}
void appendGray(String gray) {
StyledDocument doc = roomTextArea.getStyledDocument()
doc.insertString(doc.getEndPosition().getOffset() - 1, gray, doc.getStyle("gray"))
}
void appendSay(String text, Persona sender, long timestamp) {
StyledDocument doc = roomTextArea.getStyledDocument()
String header = DataHelper.formatTime(timestamp) + " <" + sender.getHumanReadableName() + "> "
doc.insertString(doc.getEndPosition().getOffset() - 1, header, doc.getStyle("italic"))
doc.insertString(doc.getEndPosition().getOffset() - 1, text, doc.getStyle("regular"))
doc.insertString(doc.getEndPosition().getOffset() - 1, "\n", doc.getStyle("regular"))
}
int getLineCount() {
StyledDocument doc = roomTextArea.getStyledDocument()
doc.getDefaultRootElement().getElementCount() - 1
}
void removeFirstLine() {
StyledDocument doc = roomTextArea.getStyledDocument()
Element element = doc.getParagraphElement(0)
doc.remove(0, element.getEndOffset())
}
}

View File

@ -44,6 +44,8 @@ import com.muwire.core.filefeeds.FeedFetchStatus
import com.muwire.core.filefeeds.FeedItem
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.upload.Uploader
import java.awt.BorderLayout
import java.awt.CardLayout
import java.awt.FlowLayout
@ -91,7 +93,7 @@ class MainFrameView {
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
application(size : [1024,768], id: 'main-frame',
application(size : [settings.mainFrameX,settings.mainFrameY], id: 'main-frame',
locationRelativeTo : null,
defaultCloseOperation : JFrame.DO_NOTHING_ON_CLOSE,
title: application.configuration['application.title'] + " " +
@ -144,6 +146,11 @@ class MainFrameView {
mvcGroup.createMVCGroup("chat-monitor","chat-monitor",env)
}
})
menuItem("Sign Tool", actionPerformed : {
def env = [:]
env['core'] = model.core
mvcGroup.createMVCGroup("sign",env)
})
}
}
borderLayout()
@ -163,12 +170,12 @@ class MainFrameView {
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout()
label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
text : " MuWire is connecting, please wait.") // TODO: real padding
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
label(" Enter search ", constraints: BorderLayout.WEST) // TODO: fix this
def searchFieldModel = new SearchFieldModel(settings, new File(application.context.get("muwire-home")))
JComboBox myComboBox = new SearchField(searchFieldModel)
@ -178,7 +185,6 @@ class MainFrameView {
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
button(text : "", icon : imageIcon("/close_tab.png"), clearSearchAction)
}
}
}
@ -255,10 +261,14 @@ class MainFrameView {
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
label(text : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, gridy:1))
label(text : "Sequential", constraints : gbc(gridx: 0, gridy: 2))
label(text : bind {model.downloader?.isSequential()}, constraints : gbc(gridx:1, gridy:2, insets : [0,0,0,20]))
label(text : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
label(text : bind {model.downloader?.activeWorkers?.size()}, constraints : gbc(gridx:4, gridy:0, insets : [0,0,0,20]))
label(text : "Active Sources:", constraints : gbc(gridx:3, gridy:1))
label(text : bind {model.downloader?.activeWorkers()}, constraints : gbc(gridx:4, gridy:1, insets : [0,0,0,20]))
label(text : "Hopeless Sources:", constraints : gbc(gridx:3, gridy:2))
label(text : bind {model.downloader?.countHopelessSources()}, constraints : gbc(gridx:4, gridy:2, insets : [0,0,0,20]))
label(text : "Total Pieces:", constraints : gbc(gridx:5, gridy: 0))
label(text : bind {model.downloader?.nPieces}, constraints : gbc(gridx:6, gridy:0, insets : [0,0,0,20]))
label(text : "Done Pieces:", constraints: gbc(gridx:5, gridy: 1))
@ -324,16 +334,16 @@ class MainFrameView {
}
panel {
gridBagLayout()
button(text : "Share", constraints : gbc(gridx: 0), actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 1), addCommentAction)
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 2), issueCertificateAction)
button(text : bind {model.publishButtonText}, enabled : bind {model.publishButtonEnabled}, constraints : gbc(gridx:3), publishAction)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 0), addCommentAction)
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 1), issueCertificateAction)
button(text : bind {model.publishButtonText}, enabled : bind {model.publishButtonEnabled}, constraints : gbc(gridx:2), publishAction)
}
panel {
panel {
label("Shared:")
label(text : bind {model.loadedFiles}, id : "shared-files-count")
}
button(text : "Share", actionPerformed : shareFiles)
}
}
}
@ -560,9 +570,10 @@ class MainFrameView {
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
panel (constraints : BorderLayout.WEST) {
label(text : bind {model.me})
button(text : "Copy Short", copyShortAction)
button(text : "Copy Full", copyFullAction)
gridBagLayout()
label(text : bind {model.me}, constraints : gbc(gridx:0, gridy:0))
button(text : "Copy Short", constraints : gbc(gridx:1, gridy:0), copyShortAction)
button(text : "Copy Full", constraints : gbc(gridx:2, gridy:0), copyFullAction)
}
panel (constraints : BorderLayout.EAST) {
label("Connections:")
@ -777,18 +788,14 @@ class MainFrameView {
selectionModel = uploadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
JPopupMenu uploadsTableMenu = new JPopupMenu()
JMenuItem showInLibrary = new JMenuItem("Show in library")
showInLibrary.addActionListener({mvcGroup.controller.showInLibrary()})
uploadsTableMenu.add(showInLibrary)
uploadsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(uploadsTableMenu, e)
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
showUploadsMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(uploadsTableMenu, e)
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
showUploadsMenu(e)
}
})
@ -1200,7 +1207,6 @@ class MainFrameView {
List<FeedItem> items = selectedFeedItems()
if (items == null || items.isEmpty())
return
// TODO: finish
JPopupMenu menu = new JPopupMenu()
if (model.downloadFeedItemButtonEnabled) {
JMenuItem download = new JMenuItem("Download")
@ -1250,6 +1256,37 @@ class MainFrameView {
}
}
void showUploadsMenu(MouseEvent e) {
Uploader uploader = selectedUploader()
if (uploader == null)
return
JPopupMenu uploadsTableMenu = new JPopupMenu()
JMenuItem showInLibrary = new JMenuItem("Show in library")
showInLibrary.addActionListener({mvcGroup.controller.showInLibrary()})
uploadsTableMenu.add(showInLibrary)
if (uploader.isBrowseEnabled()) {
JMenuItem browseItem = new JMenuItem("Browse Host")
browseItem.addActionListener({mvcGroup.controller.browseFromUpload()})
uploadsTableMenu.add(browseItem)
}
if (uploader.isFeedEnabled() && mvcGroup.controller.core.feedManager.getFeed(uploader.getDownloaderPersona()) == null) {
JMenuItem feedItem = new JMenuItem("Subscribe")
feedItem.addActionListener({mvcGroup.controller.subscribeFromUpload()})
uploadsTableMenu.add(feedItem)
}
if (uploader.isChatEnabled() && !mvcGroup.controller.core.chatManager.isConnected(uploader.getDownloaderPersona())) {
JMenuItem chatItem = new JMenuItem("Chat")
chatItem.addActionListener({mvcGroup.controller.chatFromUpload()})
uploadsTableMenu.add(chatItem)
}
showPopupMenu(uploadsTableMenu, e)
}
void showRestoreOrEmpty() {
def searchWindow = builder.getVariable("search window")
String id
@ -1401,8 +1438,12 @@ class MainFrameView {
expanded.each { tree.expandPath(it) }
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
def table = builder.getVariable("shared-files-table")
int [] selectedRows = table.getSelectedRows()
table.model.fireTableDataChanged()
for (int row : selectedRows)
table.selectionModel.addSelectionInterval(row, row)
}
public void refreshFeeds() {
@ -1451,10 +1492,10 @@ class MainFrameView {
for (int i = 0; i < count; i++)
settings.openTabs.add(tabbedPane.getTitleAt(i))
File uiPropsFile = new File(core.home, "gui.properties")
uiPropsFile.withOutputStream { settings.write(it) }
def mainFrame = builder.getVariable("main-frame")
JFrame mainFrame = builder.getVariable("main-frame")
settings.mainFrameX = mainFrame.getSize().width
settings.mainFrameY = mainFrame.getSize().height
mainFrame.setVisible(false)
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
if (core != null) {
@ -1464,6 +1505,9 @@ class MainFrameView {
}as Runnable)
t.start()
}
File uiPropsFile = new File(core.home, "gui.properties")
uiPropsFile.withOutputStream { settings.write(it) }
}
private static class TreeExpansions implements TreeExpansionListener {

View File

@ -6,7 +6,9 @@ import griffon.metadata.ArtifactProviderFor
import groovy.swing.factory.TitledBorderFactory
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSlider
import javax.swing.JTabbedPane
import javax.swing.SwingConstants
import javax.swing.border.TitledBorder
@ -37,20 +39,20 @@ class OptionsView {
def chat
def retryField
def downloadMaxFailuresField
def updateField
def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox
def shareHiddenCheckbox
def searchCommentsCheckbox
def browseFilesCheckbox
def allowTrackingCheckbox
def speedSmoothSecondsField
def totalUploadSlotsField
def uploadSlotsPerUserField
def inboundLengthField
def inboundQuantityField
def outboundLengthField
def outboundQuantityField
def tunnelLengthSlider
def tunnelQuantitySlider
def i2pUDPPortField
def i2pNTCPPortField
@ -58,6 +60,8 @@ class OptionsView {
def monitorCheckbox
def fontField
def fontSizeField
def fontStyleBoldCheckbox
def fontStyleItalicCheckbox
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
@ -107,6 +111,10 @@ class OptionsView {
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 1,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
label(text : "Allow tracking", constraints : gbc(gridx: 0, gridy: 2, anchor: GridBagConstraints.LINE_START,
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
allowTrackingCheckbox = checkBox(selected : bind {model.allowTracking}, constraints : gbc(gridx: 1, gridy : 2,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx : 0))
}
panel (border : titledBorder(title : "Download Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
@ -116,13 +124,17 @@ class OptionsView {
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2,
constraints : gbc(gridx: 2, gridy: 0, anchor : GridBagConstraints.LINE_END, weightx: 0))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:1), downloadLocationAction)
label(text : "Give up on sources after this many failures (-1 means never)", constraints: gbc(gridx: 0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
downloadMaxFailuresField = textField(text : bind { model.downloadMaxFailures }, columns : 2,
constraints : gbc(gridx: 2, gridy: 1, anchor : GridBagConstraints.LINE_END, weightx: 0))
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), downloadLocationAction)
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:3, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:3), incompleteLocationAction)
}
panel (border : titledBorder(title : "Upload Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
@ -146,16 +158,18 @@ class OptionsView {
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:1, weightx : 0))
}
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
if (!model.disableUpdates) {
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx : 0, gridy : 4, fill : GridBagConstraints.HORIZONTAL))) {
gridBagLayout()
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
gridBagLayout()
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
constraints : gbc(gridx:1, gridy : 1, anchor : GridBagConstraints.LINE_END))
}
}
}
i = builder.panel {
@ -163,19 +177,24 @@ class OptionsView {
label(text : "Changing any I2P settings requires a restart", constraints : gbc(gridx:0, gridy : 0))
panel (border : titledBorder(title : "Tunnel Settings", border : etchedBorder(), titlePosition: TitledBorder.TOP,
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
gridBagLayout()
label(text : "Inbound length", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:0,
anchor : GridBagConstraints.LINE_END))
label(text : "Inbound quantity", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:1,
anchor : GridBagConstraints.LINE_END))
label(text : "Outbound length", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx : 100))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:2,
anchor : GridBagConstraints.LINE_END))
label(text : "Outbound quantity", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx : 100))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:3,
anchor : GridBagConstraints.LINE_END))
gridLayout(rows:4, cols:1)
label(text : "Speed vs Anonymity")
def lengthTable = new Hashtable()
lengthTable.put(1, new JLabel("Max Speed"))
lengthTable.put(3, new JLabel("Max Anonymity"))
tunnelLengthSlider = slider(minimum : 1, maximum : 3, value : bind {model.tunnelLength},
majorTickSpacing : 1, snapToTicks: true, paintTicks: true, labelTable : lengthTable,
paintLabels : true)
label(text: "Reliability vs Resource Usage")
def quantityTable = new Hashtable()
quantityTable.put(1, new JLabel("Min Resources"))
quantityTable.put(6, new JLabel("Max Reliability"))
tunnelQuantitySlider = slider(minimum : 1, maximum : 6, value : bind {model.tunnelQuantity},
majorTickSpacing : 1, snapToTicks : true, paintTicks: true, labelTable : quantityTable,
paintLabels : true)
}
Core core = application.context.get("core")
@ -210,6 +229,14 @@ class OptionsView {
constraints : gbc(gridx : 2, gridy: 2, anchor : GridBagConstraints.LINE_START), customFontAction)
fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize},
constraints : gbc(gridx : 3, gridy : 2, anchor : GridBagConstraints.LINE_END))
label(text : "Font style", constraints: gbc(gridx: 0, gridy: 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
panel(constraints : gbc(gridx: 2, gridy: 3, gridwidth: 2, anchor:GridBagConstraints.LINE_END)) {
fontStyleBoldCheckbox = checkBox(selected : bind {model.fontStyleBold})
label(text: "Bold")
fontStyleItalicCheckbox = checkBox(selected : bind {model.fontStyleItalic})
label(text: "Italic")
}
}
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),

View File

@ -0,0 +1,66 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SignView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
def mainFrame
def dialog
def p
def plainTextArea
def signedTextArea
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Sign Text", true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label("Enter text to be signed")
}
panel (constraints : BorderLayout.CENTER) {
gridLayout(rows : 2, cols: 1)
scrollPane {
plainTextArea = textArea(rows : 10, columns : 50, editable : true, lineWrap: true, wrapStyleWord : true)
}
scrollPane {
signedTextArea = textArea(rows : 10, columns : 50, editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Sign", signAction)
button(text : "Copy To Clipboard", copyAction)
button(text : "Dismiss", closeAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@ -0,0 +1,74 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import javax.swing.event.ChangeListener
import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class WatchedDirectoryView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
WatchedDirectoryModel model
def dialog
def p
def mainFrame
def autoWatchCheckbox
def syncIntervalField
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Watched Directory Configuration", true)
dialog.setResizable(false)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label("Configuration for directory " + model.directory.directory.toString())
}
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Auto-watch directory using operating system", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoWatchCheckbox = checkBox(selected : bind {model.autoWatch}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Directory sync frequency (seconds, 0 means never)", enabled : bind {!model.autoWatch}, constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
syncIntervalField = textField(text : bind {model.syncInterval}, columns: 4, enabled : bind {!model.autoWatch},
constraints: gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END, insets : [0,10,0,0]))
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Save", saveAction)
button(text : "Cancel", cancelAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
autoWatchCheckbox.addChangeListener({e ->
model.autoWatch = autoWatchCheckbox.model.isSelected()
} as ChangeListener)
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@ -0,0 +1,78 @@
package com.muwire.gui.wizard
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class WizardView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
WizardModel model
def dialog
def p
void initUI() {
dialog = new JDialog(model.parent, "Setup Wizard", true)
p = builder.panel {
borderLayout()
panel (id : "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout()
model.steps.each {
it.buildUI(builder)
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2)
panel {
button(text : "Cancel", cancelAction)
}
panel {
button(text : "Previous", enabled : bind {model.previousButtonEnabled}, previousAction)
button(text : "Next", enabled : bind {model.nextButtonEnabled}, nextAction)
button(text : "Finish", enabled : bind {model.finishButtonEnabled}, finishAction)
}
}
}
}
void updateLayout() {
model.previousButtonEnabled = model.currentStep > 0
model.nextButtonEnabled = model.steps.size() > (model.currentStep + 1)
model.finishButtonEnabled = model.steps.size() == (model.currentStep + 1)
String constraints = model.steps[model.currentStep].getConstraint()
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, constraints)
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(model.parent)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
void hide() {
dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -1,10 +1,24 @@
package com.muwire.gui
import griffon.swing.SwingGriffonApplication
import java.util.logging.Level
import java.util.logging.LogManager
import griffon.swing.SwingGriffonApplication
import groovy.util.logging.Log
@Log
class Launcher {
public static void main(String[] args) {
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)
}
}
SwingGriffonApplication.main(args)
}
}

View File

@ -2,13 +2,16 @@ package com.muwire.gui
import com.muwire.core.util.DataUtil
import java.awt.Font
class UISettings {
String lnf
boolean showMonitor
String font
boolean autoFontSize
int fontSize
int fontSize, fontStyle
int mainFrameX, mainFrameY
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
@ -33,6 +36,7 @@ class UISettings {
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
fontStyle = Integer.parseInt(props.getProperty("fontStyle", String.valueOf(Font.PLAIN)))
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
certificateWarning = Boolean.parseBoolean(props.getProperty("certificateWarning","true"))
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
@ -41,6 +45,9 @@ class UISettings {
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
maxChatLines = Integer.parseInt(props.getProperty("maxChatLines","-1"))
mainFrameX = Integer.parseInt(props.getProperty("mainFrameX","1024"))
mainFrameY = Integer.parseInt(props.getProperty("mainFrameY","768"))
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
openTabs = DataUtil.readEncodedSet(props, "openTabs")
}
@ -62,8 +69,12 @@ class UISettings {
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
props.setProperty("groupByFile", String.valueOf(groupByFile))
props.setProperty("maxChatLines", String.valueOf(maxChatLines))
props.setProperty("fontStyle", String.valueOf(fontStyle))
if (font != null)
props.setProperty("font", font)
props.setProperty("mainFrameX", String.valueOf(mainFrameX))
props.setProperty("mainFrameY", String.valueOf(mainFrameY))
DataUtil.writeEncodedSet(searchHistory, "searchHistory", props)
DataUtil.writeEncodedSet(openTabs, "openTabs", props)

Some files were not shown because too many files have changed in this diff Show More