Compare commits

..

446 Commits

Author SHA1 Message Date
Zlatin Balevsky
9a44603d2f prevent duplicate feed subscriptions 2020-03-10 21:30:27 +00:00
Zlatin Balevsky
38a027c308 context menu on the feed items table 2020-03-10 21:15:59 +00:00
Zlatin Balevsky
2ba81ccc84 context menu for feeds table 2020-03-10 20:50:00 +00:00
Zlatin Balevsky
0408349c07 size columns 2020-03-10 20:04:28 +00:00
Zlatin Balevsky
95cb7f3214 auto-download feed items functionality 2020-03-10 19:48:36 +00:00
Zlatin Balevsky
69810d7203 fix variable name 2020-03-10 19:35:39 +00:00
Zlatin Balevsky
f202fa34f3 auto-publish shared files functionality 2020-03-10 19:12:49 +00:00
Zlatin Balevsky
c082e25c81 individual feed configuration panel 2020-03-10 18:48:20 +00:00
Zlatin Balevsky
2bb07ff7b5 do not trim feed items if setting is negative 2020-03-10 17:53:17 +00:00
Zlatin Balevsky
ff952890bc populate new feeds from defaults 2020-03-10 17:51:14 +00:00
Zlatin Balevsky
fc393619d8 options for feeds 2020-03-10 17:47:08 +00:00
Zlatin Balevsky
2882c73876 enable button when switching to chat window 2020-03-10 17:05:44 +00:00
Zlatin Balevsky
cbb1de046b fetch certificates functionality 2020-03-10 16:53:28 +00:00
Zlatin Balevsky
a272a45928 persist the right number of feed items 2020-03-10 16:41:31 +00:00
Zlatin Balevsky
3133581363 view comment functionality 2020-03-10 16:35:02 +00:00
Zlatin Balevsky
c3d0dce281 store last update attempt and do not retry active feeds 2020-03-10 16:05:32 +00:00
Zlatin Balevsky
8f710e68c2 download feed item action 2020-03-10 15:06:42 +00:00
Zlatin Balevsky
15430d6c03 manual update and unsubscribe actions 2020-03-10 13:51:07 +00:00
Zlatin Balevsky
166b71f128 fix NPE when logging is enabled 2020-03-10 13:47:31 +00:00
Zlatin Balevsky
d724986ec6 proper method name 2020-03-10 13:34:33 +00:00
Zlatin Balevsky
198c5b5538 fix json parsing 2020-03-10 13:06:47 +00:00
Zlatin Balevsky
96d71ed08f fix method name 2020-03-10 13:06:36 +00:00
Zlatin Balevsky
bb7385688c it always points to the innermost closure 2020-03-10 12:55:09 +00:00
Zlatin Balevsky
e70bec3a51 hook up feed subscription 2020-03-10 12:44:25 +00:00
Zlatin Balevsky
ed04c40420 return an empty set if no items are found 2020-03-10 12:43:49 +00:00
Zlatin Balevsky
e9f00c2995 subscribe button in search tab 2020-03-10 12:16:18 +00:00
Zlatin Balevsky
fd75d8229b fix feed checkbox for local results 2020-03-10 12:15:52 +00:00
Zlatin Balevsky
0ff9ca8572 wip on feed items table 2020-03-10 11:50:55 +00:00
Zlatin Balevsky
a07f01b641 utility method to check if an infohash is shared 2020-03-10 11:50:09 +00:00
Zlatin Balevsky
b9333913c6 hook up feeds table to feed items table 2020-03-10 10:47:05 +00:00
Zlatin Balevsky
fcb5c573f9 wip on feeds table 2020-03-10 10:39:18 +00:00
Zlatin Balevsky
1610766e01 wip on feeds table 2020-03-10 07:33:29 +00:00
Zlatin Balevsky
e2a9db8056 add an IDLE status to feeds for display purposes 2020-03-10 07:32:45 +00:00
Zlatin Balevsky
a0cb214e2b placeholder feeds panel 2020-03-10 06:22:40 +00:00
Zlatin Balevsky
f2bf921d4c parse feed flag in results 2020-03-10 06:06:57 +00:00
Zlatin Balevsky
aa0fcfb7de fix capitalization in event name 2020-03-10 05:50:54 +00:00
Zlatin Balevsky
48cfce71a8 emit event on publishing 2020-03-10 05:47:42 +00:00
Zlatin Balevsky
8798ea38e8 button for publishing, column in the shared files table 2020-03-10 01:39:55 +00:00
Zlatin Balevsky
17cd60afe3 deleting of feeds 2020-03-10 00:58:43 +00:00
Zlatin Balevsky
c10c1118e8 feed client 2020-03-09 19:28:42 +00:00
Zlatin Balevsky
28425e93dc persist only as many items as configured to keep 2020-03-09 18:53:43 +00:00
Zlatin Balevsky
032338bb48 Persist feed metadata and items on successful fetch. Register feed manager for various events 2020-03-09 18:31:10 +00:00
Zlatin Balevsky
12e56b1c9a events associated with updating a feed 2020-03-09 17:37:17 +00:00
Zlatin Balevsky
57c75978b6 wip on feed manager deserialization 2020-03-08 20:19:37 +00:00
Zlatin Balevsky
bfe198e1a6 represenation of a feed 2020-03-08 19:38:48 +00:00
Zlatin Balevsky
8e274f940e Feed item representation and serialization 2020-03-08 19:30:04 +00:00
Zlatin Balevsky
9f3942c1c7 settings to disable or not advertise file feed 2020-03-08 17:15:00 +00:00
Zlatin Balevsky
d60d57ee43 wip on server side feed handling 2020-03-08 17:04:11 +00:00
Zlatin Balevsky
8e3a433afb persist shared file on publish/unpublish 2020-03-08 16:06:28 +00:00
Zlatin Balevsky
49cf56fabb UI Publish & Unpublish events 2020-03-08 16:01:50 +00:00
Zlatin Balevsky
2b6565d107 unpublish method 2020-03-08 16:01:23 +00:00
Zlatin Balevsky
366a2ef841 published flag and timestamp in shared files 2020-03-08 15:46:36 +00:00
Zlatin Balevsky
bcd24e56ac TODO updates for plugin 2020-03-07 15:48:20 +00:00
Zlatin Balevsky
c7d1f0c23c Connect to i2p router after creating the Core object, should help with plugin init issues #39 2020-02-23 18:29:09 +00:00
Zlatin Balevsky
853b9f67fc Release 0.6.10 2020-02-23 15:42:03 +00:00
Zlatin Balevsky
a505a2449a persist SharedFile on change of comments #35 2020-02-18 02:14:32 +00:00
Zlatin Balevsky
c11d81c6c3 Release 0.6.9 2020-02-16 16:33:33 +00:00
Zlatin Balevsky
ee5e90c4ab ignore events from old persister service, prevents duplicate entries during migration #35 2020-02-14 18:20:39 +00:00
Zlatin Balevsky
64d2a87d26 more occurrences of SharedFile::getInfoHash #35 2020-02-14 17:53:09 +00:00
Zlatin Balevsky
f0304dbe7d fix copy-hash-to-clipboard #35 2020-02-14 16:14:36 +00:00
Zlatin Balevsky
bdad8d9309 make extended signatures mandatory 2020-02-14 15:34:21 +00:00
Zlatin Balevsky
8c110bbae5 more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:24:39 +00:00
Zlatin Balevsky
2cc1e384bc more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:20:01 +00:00
Zlatin Balevsky
9337d1b74d chase down references to missing infoHash #35 2020-02-14 01:48:02 +00:00
Zlatin Balevsky
16ed5dd346 chase down some usages of deprecated getInfoHash method #35 2020-02-14 01:32:38 +00:00
Zlatin Balevsky
7b55fc9ed8 working uploads #35 2020-02-14 01:15:10 +00:00
Zlatin Balevsky
d5c8050572 wip on separate hashlist storage #35 2020-02-14 00:37:07 +00:00
Zlatin Balevsky
83546d68d2 Merge pull request #37 from LoveIsGrief/change-persister
Introduce persister that uses a directory structure
2020-01-25 14:36:41 +00:00
a891c83518 Only persist downloaded files if sharing thereof is enabled
Otherwise we might inadvertently share downloads
2020-01-25 15:25:48 +01:00
aa56cc23c0 Cache base 64 path hash
Can't do it in constructor without an ugly try/catch
 therefore this is done on demand
2020-01-25 15:20:38 +01:00
a2b37ef567 Persist downloaded files 2020-01-25 15:06:12 +01:00
4bc04ae631 Revert "Reduce log levels in Connection"
This reverts commit dcd233b7
2020-01-25 15:01:21 +01:00
56da9a16b0 Set FileLoadedEvent::source in the subclass
Setting it in the super class means we don't set the right value for every case
2020-01-25 15:00:48 +01:00
2935ee1a1d Remove unnecessary executor
It was doing nothing but starting and stopping
2020-01-25 14:49:59 +01:00
855183397b Remove TODO
There's already an issue open https://github.com/zlatinb/muwire/issues/35
2020-01-22 21:35:54 +01:00
e27704c1af Make sure migration from PersisterService works
this.getClass() and this.class kept resolving to Class.
Using a string is much simpler

mkdirs() is also necessary because the directory structure doesn't exist
 when persistFile is called the first time
2020-01-22 20:59:05 +01:00
5c18b4a141 Add more logs PersisterFolderService 2020-01-22 15:12:22 +01:00
dcd233b7ad Reduce log levels in Connection
Too verbose
2020-01-22 15:12:01 +01:00
7cee8a28ba FileLoadedEvent should include class when coming from old persister
Otherwise the new PersisterFolderService won't migrate
2020-01-22 15:07:00 +01:00
7446fc949a Remove UIPersistFilesEvent
Hashing is done per file now and those are triggered by individual events
2020-01-22 13:00:55 +01:00
598ab90f63 Clear up the event path when starting up the old and new persisters
The new persister won't load anything until the old one has finished
2020-01-22 12:36:34 +01:00
043028c296 Introduce PersisterFolderService to replace PersisterService
An attempt at automatically migrate from PersisterService was made, but the events aren't triggered in the right order.
We need to make sure that we don't trigger the "AllFilesLoadedEvent" before the migration is done
2020-01-21 23:34:33 +01:00
cd1757fac3 Use Java 11
Java9 isn't available on Ubuntu anymore, which would make development harder
2020-01-19 21:46:47 +01:00
9d4b365e63 Log the time it take to persist files and hashes 2020-01-19 21:43:03 +01:00
Zlatin Balevsky
b12d57e30a fix bracket 2020-01-14 20:27:21 +00:00
Zlatin Balevsky
f33d1b6db3 move the docker documentation to the wiki 2020-01-14 20:26:47 +00:00
Zlatin Balevsky
9e451460da change to my repo 2020-01-14 19:24:32 +00:00
Zlatin Balevsky
ffa52c129a Merge pull request #33 from LoveIsGrief/32-docker-image
Docker image
2020-01-14 19:21:35 +00:00
b779fb75a0 docker: Remove incompletes warning from README
#32 - Docker image
2020-01-14 20:11:34 +01:00
fbe6b53278 docker: Make sure build directories are ignored
#32 - Docker image
2020-01-14 19:20:11 +01:00
b2bd95788d docker: Try minimizing size using add-pkg and del-pkg
As described in https://github.com/jlesage/docker-baseimage-gui#addingremoving-packages

#32 - Docker image
2020-01-14 19:19:47 +01:00
83d4a2624b docker: Add bisentenialwrug/muwire to README
To be replaced later by @zlatinb's repo

#32 - Docker image
2020-01-14 18:47:28 +01:00
03e20e21aa Remove unnecessary quotes from properties files
There doesn't seem to be a special treatment of them
 in properties files

#32 - Docker image
2020-01-14 18:42:51 +01:00
8a08955675 Remove quotes from i2cp.tcp.port setting
For some reason it really doesn't like that and
 subsequently can't connect to the host

#32 - Docker image
2020-01-14 17:52:52 +01:00
4ec54ebe54 docker: Quote the IP-address in i2p.properties
#32 - Docker image
2020-01-14 17:36:45 +01:00
758af6f48e docker: Make sure APP_HOME is editable by the user
Otherwise MuWire won't be able to write into the home

#32 - Docker image
2020-01-14 17:14:41 +01:00
a7bdd47fcd docker: Add more files to ignore
Helps with build speed on the local machine

#32 - Docker image
2020-01-14 17:00:07 +01:00
f7caa77a18 docker: Include the MuWire icon for the webview
#32 - Docker image
2020-01-14 16:59:39 +01:00
7641f64536 docker: Add default MuWire.properties without nickname
#32 - Docker image
2020-01-14 16:59:13 +01:00
02baaace48 Merge branch 'master' of https://github.com/zlatinb/muwire into 32-docker-image 2020-01-14 16:48:12 +01:00
Zlatin Balevsky
d90067ff39 prompt for nickname even if MuWire.properties exists so that docker can ship a MuWire.properties #32 2020-01-14 14:17:18 +00:00
c910a215f5 Add the /incompletes docker volume
It won't be used by default though

#32 - Docker image
2020-01-14 13:07:37 +01:00
65e073b1b9 Use defaults for the i2p.properties
This will help writing custom properties
 as not everthing will have to be specified in them

#32 - Docker image
2020-01-14 12:29:05 +01:00
489a7518c3 Attempt to reduce size a bit more
- Ignore the cruft when building
 - Remove the correct temporary directory

#32 - Docker image
2020-01-14 01:09:39 +01:00
3733e48bbd Force set the port
The default isn't used in the code.
That should be fixed, but I'm too tired right now

#32 - Docker image
2020-01-14 00:29:33 +01:00
c3723a1348 Try to minimize image size
#32 - Docker image
2020-01-14 00:15:01 +01:00
0e0f52bc77 Retry: Set a home directory for the "app" user
Apparently it's done differently in the parent image,
 so we just overwrite it.

Hopefully now the app user will have a home

#32 - Docker image
2020-01-13 23:38:04 +01:00
60b9e990cf Set a home directory for the "app" user
#32 - Docker image
2020-01-13 21:34:50 +01:00
28ad0ae30f Add --name to docker run command
#32 - Docker image
2020-01-13 20:29:28 +01:00
9142de85cd Correct the link to the i2cp_config.png
#32 - Docker image
2020-01-13 19:51:20 +01:00
4eb31c11e3 Write README and cleanup inconsistencies
#32 - Docker image
2020-01-13 18:42:30 +01:00
e8afe358a5 First Dockerfile with GUI that starts
It doesn't continue yet as it seems to be waiting for a connection
 to I2P... or something else 🤷#32 - Docker image
2020-01-13 17:07:56 +01:00
Zlatin Balevsky
3db4317fc1 more items 2020-01-01 11:26:59 +00:00
Zlatin Balevsky
5ad2b28527 more items 2020-01-01 09:19:46 +00:00
Zlatin Balevsky
3036765f81 translations 2019-12-27 12:33:22 +01:00
Zlatin Balevsky
8f9b1e5a8b supress exceptions if client is stopped 2019-12-24 17:05:36 +00:00
Zlatin Balevsky
e6d59a2438 stop host persister on shutdown 2019-12-24 05:53:02 +00:00
Zlatin Balevsky
32609b4779 get rid of dependency on groovy-all 2019-12-23 21:16:24 +00:00
Zlatin Balevsky
74ac4cfecf remove size filter which was left over from grails experiments 2019-12-23 20:54:16 +00:00
Zlatin Balevsky
69173c4156 update TODO 2019-12-23 20:09:07 +00:00
Zlatin Balevsky
6283287bee prevent empty input from sharing the I2P working dir 2019-12-22 22:17:57 +00:00
Zlatin Balevsky
8e3f76f68c move plugin build instructions to the wiki 2019-12-22 16:51:40 +00:00
Zlatin Balevsky
574294fdc6 update readme 2019-12-22 16:14:42 +00:00
Zlatin Balevsky
8bd41546cd proper uploader equality check 2019-12-21 23:15:39 +00:00
Zlatin Balevsky
ba5425c958 extra check for stopped cache client 2019-12-21 15:56:09 +00:00
Zlatin Balevsky
22580f002c separate update url from main plugin url 2019-12-21 13:46:42 +00:00
Zlatin Balevsky
5c773cec80 more css changes from zzz 2019-12-20 16:19:00 +00:00
Zlatin Balevsky
7df00e6709 delete duplicate translation 2019-12-19 20:14:24 +00:00
Zlatin Balevsky
5c05bd2562 If a result is for a shared file, display it as Downloaded 2019-12-19 20:12:02 +00:00
Zlatin Balevsky
9df1d043e4 do not initialize the update client if running as a plugin 2019-12-19 18:35:44 +00:00
Zlatin Balevsky
6ea1a15641 do not initialize the update client if running as a plugin 2019-12-19 18:30:07 +00:00
Zlatin Balevsky
c0575facec add Downloaded string 2019-12-19 13:12:01 +00:00
Zlatin Balevsky
09168844e0 css tweaks from zzz 2019-12-19 12:53:51 +00:00
Zlatin Balevsky
e21d482393 Release 0.6.8 2019-12-19 06:04:18 +00:00
Zlatin Balevsky
f5fc3e40c2 add incomplete translations 2019-12-18 11:26:06 +00:00
Zlatin Balevsky
796a0138fa change Trust Users title for clearer translation; add headers for the two tables 2019-12-18 11:25:36 +00:00
Zlatin Balevsky
505b4ddb06 comment clairfying verb/noun for Pause and Query 2019-12-18 10:57:56 +00:00
Zlatin Balevsky
a35216ff56 some translations 2019-12-18 08:23:56 +00:00
Zlatin Balevsky
fba92fe9b9 missing strings 2019-12-17 20:21:01 +00:00
Zlatin Balevsky
1cc511b0ae initialize root node in the init function so that it can be translated 2019-12-17 20:19:38 +00:00
Zlatin Balevsky
fa94c8ebfa persist files after unsharing and pause to let event propagate 2019-12-17 17:24:50 +00:00
Zlatin Balevsky
88b68a3c5c move controls to the right of the tree nodes 2019-12-17 17:14:30 +00:00
Zlatin Balevsky
b3e0d2ee7a display tree in order it arrives from servlet 2019-12-17 15:38:24 +00:00
Zlatin Balevsky
ce293cbda8 sort file tree servlet side 2019-12-17 15:28:49 +00:00
Zlatin Balevsky
3abc617e9f css changes from zzz 2019-12-17 14:25:42 +00:00
Zlatin Balevsky
67ee634f20 separate link text for a single certificate 2019-12-17 13:51:34 +00:00
Zlatin Balevsky
503d54927f fix directories with special characters in them in file tree view 2019-12-17 13:22:31 +00:00
Zlatin Balevsky
5788329e1a add ability to set css class to sortable tables. Make certificates table certificates class 2019-12-17 12:52:33 +00:00
Zlatin Balevsky
f0ffc68122 more strings 2019-12-17 12:43:49 +00:00
Zlatin Balevsky
3d710cebe5 no <pre> if there is no reason 2019-12-17 12:39:35 +00:00
Zlatin Balevsky
7d67573c92 rename 2019-12-17 11:40:47 +00:00
Zlatin Balevsky
3acc676448 table to div.right 2019-12-17 11:38:40 +00:00
Zlatin Balevsky
2bf03b6b84 use div.centercomment for trust comments 2019-12-17 10:30:07 +00:00
Zlatin Balevsky
b8ba6df4d5 link to BrowseHost page 2019-12-17 10:21:48 +00:00
Zlatin Balevsky
9fa7fa07b4 whitespace between links 2019-12-17 10:19:47 +00:00
Zlatin Balevsky
1c7253ea0a more _t 2019-12-17 10:16:17 +00:00
Zlatin Balevsky
d947ad2997 more table->span.right 2019-12-17 10:15:00 +00:00
Zlatin Balevsky
dd0bd6f5f8 use pre.comment for trust reasons 2019-12-17 07:09:37 +00:00
Zlatin Balevsky
f05b6d0b40 word-wrap pre.comment pt.2 2019-12-17 07:05:24 +00:00
Zlatin Balevsky
906c69a482 word-wrap pre.comment 2019-12-17 07:04:13 +00:00
Zlatin Balevsky
5375b7aec0 add class to <pre> blocks 2019-12-16 22:25:10 +00:00
Zlatin Balevsky
ea5da2431a remove ;, thanks to jshint and zzz 2019-12-16 21:35:57 +00:00
Zlatin Balevsky
14b3a9ac9e add more js strings 2019-12-16 21:14:44 +00:00
Zlatin Balevsky
40bbef4583 remove unneeded files 2019-12-16 20:51:22 +00:00
Zlatin Balevsky
f811653247 remove unnecessary strings from Util._x 2019-12-16 20:03:30 +00:00
Zlatin Balevsky
f321000071 sort tables by default 2019-12-16 19:26:24 +00:00
Zlatin Balevsky
6eb85283cd sort tables by default 2019-12-16 19:19:09 +00:00
Zlatin Balevsky
2973759cd9 sort tables by default 2019-12-16 19:16:06 +00:00
Zlatin Balevsky
fe945a9941 sort tables by default 2019-12-16 19:12:48 +00:00
Zlatin Balevsky
5f7e949310 clear tables when closing current search 2019-12-16 19:07:01 +00:00
Zlatin Balevsky
11edb2cb3c convert tables to div.right 2019-12-16 18:48:28 +00:00
Zlatin Balevsky
ff1f801155 convert tables to div.right 2019-12-16 18:43:31 +00:00
Zlatin Balevsky
0a98083c64 convert tables to div.right 2019-12-16 18:39:53 +00:00
Zlatin Balevsky
75b2852f6e convert table to div.right for links 2019-12-16 18:30:39 +00:00
Zlatin Balevsky
5774cdee94 move comment box to the center 2019-12-16 18:11:30 +00:00
Zlatin Balevsky
2b0f4e52ca ellipsis on overflow, input alignment fixes from zzz 2019-12-16 17:51:10 +00:00
Zlatin Balevsky
1d20dc917b make the comment box 50% of available space in table view 2019-12-16 17:28:38 +00:00
Zlatin Balevsky
63e3b3710c bottom table id based on view type 2019-12-16 16:22:51 +00:00
Zlatin Balevsky
0878b89082 different ids for the top table based on view type 2019-12-16 16:05:52 +00:00
Zlatin Balevsky
fecf0ecae8 put trusted and distrusted tables on top of one another 2019-12-16 14:56:54 +00:00
Zlatin Balevsky
fec8d4ef9f Done->Downloaded Pieces 2019-12-16 14:51:22 +00:00
Zlatin Balevsky
067ac8582a Lists->Subscriptions 2019-12-16 14:47:52 +00:00
Zlatin Balevsky
31cac25a23 remove fetch link, make the file name a link 2019-12-16 14:43:38 +00:00
Zlatin Balevsky
6bcc44e01e align comment textarea to the right 2019-12-16 14:32:44 +00:00
Zlatin Balevsky
31652b34d7 column sizing, tags, other changes from zzz 2019-12-16 14:24:27 +00:00
Zlatin Balevsky
41a15fc7d5 clear Speed and ETA columns for finished downloads 2019-12-16 14:22:12 +00:00
Zlatin Balevsky
da3d7d7a50 herf->href 2019-12-16 13:43:16 +00:00
Zlatin Balevsky
3a079d9f21 expand root by default, expand until there is more than one child 2019-12-16 13:16:39 +00:00
Zlatin Balevsky
ba0c85fe07 do not show unshare/comment/certify links for directories that are not shared 2019-12-16 13:01:56 +00:00
Zlatin Balevsky
ecb2283886 comment out help section 2019-12-16 09:18:27 +00:00
Zlatin Balevsky
cf9a18cee5 style init page 2019-12-16 05:11:20 +00:00
Zlatin Balevsky
982a93a04b get rid of static headers in trust list view 2019-12-16 04:58:40 +00:00
Zlatin Balevsky
58137d11d1 space out trust links in search view 2019-12-16 01:49:59 +00:00
Zlatin Balevsky
d87bec927d space out links in trust users view 2019-12-16 01:32:00 +00:00
Zlatin Balevsky
dc8dd96495 space out links in trust lists view 2019-12-16 01:26:43 +00:00
Zlatin Balevsky
add9fb6feb revision is an integer 2019-12-16 01:15:17 +00:00
Zlatin Balevsky
c500e95ab6 register for correct event 2019-12-16 01:05:31 +00:00
Zlatin Balevsky
477c3285d2 do not display empty files table 2019-12-15 23:36:01 +00:00
Zlatin Balevsky
1f5b112bfe fix distrusting 2019-12-15 23:31:07 +00:00
Zlatin Balevsky
b0d09853e4 pause after publishing all trust events 2019-12-15 23:28:00 +00:00
Zlatin Balevsky
b96d997037 do not show empty tables 2019-12-15 23:22:40 +00:00
Zlatin Balevsky
a631ec1e14 do not display empty or stale tables in trust list view 2019-12-15 23:14:24 +00:00
Zlatin Balevsky
62a06bc891 do not show empty or stale tables after closing browses 2019-12-15 22:46:42 +00:00
Zlatin Balevsky
3534b23194 correct string 2019-12-15 22:20:29 +00:00
Zlatin Balevsky
c561ae9140 get possible sources from browse host 2019-12-15 22:19:26 +00:00
Zlatin Balevsky
5926457eb5 send redirect after manual browse input 2019-12-15 22:14:09 +00:00
Zlatin Balevsky
37c93e352b Make Downloading a link 2019-12-15 19:39:07 +00:00
Zlatin Balevsky
be8fecda39 Change Downloading to a link 2019-12-15 19:29:47 +00:00
Zlatin Balevsky
7ec6257ac0 implement closing browses 2019-12-15 19:02:51 +00:00
Zlatin Balevsky
c4ea58c330 add Sources column to group-by-file view 2019-12-15 18:47:52 +00:00
Zlatin Balevsky
a482fe5c93 turn browse link into browsing link 2019-12-15 18:40:19 +00:00
Zlatin Balevsky
2ee84848c4 make Browsing a link to the browse page 2019-12-15 16:55:11 +00:00
Zlatin Balevsky
e29d7f6872 do not display active searches table if it's empty 2019-12-15 16:47:07 +00:00
Zlatin Balevsky
5ded824ef2 display tables side by side 2019-12-15 16:42:16 +00:00
Zlatin Balevsky
c607560cb8 space between trust action links 2019-12-15 16:42:03 +00:00
Zlatin Balevsky
8b341bb125 tell the user the directories will be created 2019-12-15 16:17:56 +00:00
Zlatin Balevsky
6bc5a9075b rewrite welcome jsp to a servlet, add sanity check of inputs 2019-12-15 16:16:11 +00:00
Zlatin Balevsky
6b1d2bc5ce sanitize the i2p tunnel settings 2019-12-15 15:36:03 +00:00
Zlatin Balevsky
0cbbaf6a63 localized error messages 2019-12-15 15:33:15 +00:00
Zlatin Balevsky
3363b99675 sanitize integer and file input 2019-12-15 15:13:44 +00:00
Zlatin Balevsky
4ab4785539 display errors on invalid config input 2019-12-15 15:06:18 +00:00
Zlatin Balevsky
e595fa97e8 change some strings for easier translation 2019-12-15 14:53:35 +00:00
Zlatin Balevsky
65a7088463 can squeeze a few more characters 2019-12-15 13:39:20 +00:00
Zlatin Balevsky
2d5bd653c1 do not display number of results if it's zero 2019-12-15 13:35:16 +00:00
Zlatin Balevsky
a864343c05 get rid of senders and results columns, use ellipsis for very long search strings 2019-12-15 13:29:03 +00:00
Zlatin Balevsky
696b348469 link to translation instructions 2019-12-15 12:41:08 +00:00
Zlatin Balevsky
b08333c5ea download details view 2019-12-15 11:34:04 +00:00
Zlatin Balevsky
0cf368c1af uploads icon 2019-12-15 10:00:26 +00:00
Zlatin Balevsky
62ab957892 clear finished uploads link 2019-12-15 08:57:14 +00:00
Zlatin Balevsky
2b9e722165 clear finished downloads link 2019-12-15 08:29:55 +00:00
Zlatin Balevsky
8cf4b23762 ability to stop a search 2019-12-15 07:58:16 +00:00
Zlatin Balevsky
1285c68521 uploads page 2019-12-15 03:26:55 +00:00
Zlatin Balevsky
daa9e0bafc servlet side of uploader page 2019-12-15 02:45:14 +00:00
Zlatin Balevsky
8efd9c2c88 headers for the sections 2019-12-14 21:42:42 +00:00
Zlatin Balevsky
918549f164 hook up to translations 2019-12-14 21:18:43 +00:00
Zlatin Balevsky
e30a4666cb wip on configuration page 2019-12-14 20:49:54 +00:00
Zlatin Balevsky
26167abc08 fix connections count on settings page 2019-12-14 20:34:57 +00:00
Zlatin Balevsky
93f7c67f37 wip on configuration page 2019-12-14 20:27:13 +00:00
Zlatin Balevsky
f9a0a5e08a wip on settings page 2019-12-14 19:30:11 +00:00
Zlatin Balevsky
d8ae275df2 remove debug println 2019-12-14 19:25:29 +00:00
Zlatin Balevsky
fce879be5d hook up configuration page, under construction 2019-12-14 19:02:15 +00:00
Zlatin Balevsky
0b58e22714 start work on configuration page 2019-12-14 17:47:52 +00:00
Zlatin Balevsky
dd230c4dfc automatic resume of failed downloads 2019-12-14 14:16:29 +00:00
Zlatin Balevsky
fba0b001c0 pause/resume/retry links 2019-12-14 14:02:25 +00:00
Zlatin Balevsky
6978c7b992 switch certify and comment links 2019-12-14 10:17:19 +00:00
Zlatin Balevsky
7355e76e1b add fetch link to shared file table view 2019-12-14 09:09:03 +00:00
Zlatin Balevsky
5147cf21a0 hook up the downloaded content servlet 2019-12-13 23:57:51 +00:00
Zlatin Balevsky
e8dd7d710d pause to give a chance to the event to propagate 2019-12-13 13:10:11 +00:00
Zlatin Balevsky
fc9114eaa5 use Collator for comparing strings 2019-12-13 12:21:11 +00:00
Zlatin Balevsky
20b7104c41 wrong formatting for ETA and progress 2019-12-13 11:37:20 +00:00
Zlatin Balevsky
570616951a sortable certificate table, add extra parameter to Table object 2019-12-13 11:27:29 +00:00
Zlatin Balevsky
e075bfac55 auto-refresh the files table if revision changed 2019-12-13 08:28:00 +00:00
Zlatin Balevsky
b6411a555c hide links on root node 2019-12-13 08:27:35 +00:00
Zlatin Balevsky
d395475727 sortable shared files table 2019-12-13 08:04:19 +00:00
Zlatin Balevsky
8ae0a16b8a sortable trust list tables 2019-12-13 02:23:42 +00:00
Zlatin Balevsky
38fcdfc97a sorting of trust subscriptions table 2019-12-13 00:04:10 +00:00
Zlatin Balevsky
a0fb07cf99 Link helper class 2019-12-12 22:31:42 +00:00
Zlatin Balevsky
3747f9a5d5 sortable tables on trust users page 2019-12-12 21:43:54 +00:00
Zlatin Balevsky
3a738f8f62 sorting downloads table 2019-12-12 17:26:56 +00:00
Zlatin Balevsky
ca56363438 server-side of downloads sorting support 2019-12-12 13:19:25 +00:00
Zlatin Balevsky
e06cb05e2a fix glitch in sorting when new results arrive 2019-12-12 01:16:42 +00:00
Zlatin Balevsky
8ab2dd7900 sort all tables on search page 2019-12-12 00:44:49 +00:00
Zlatin Balevsky
26116d313a avoid an exception 2019-12-11 22:34:16 +00:00
Zlatin Balevsky
738f177d6c update certificate hooks to new architecture 2019-12-11 22:28:54 +00:00
Zlatin Balevsky
62c4579bbd preserve expanded comment state during updates 2019-12-11 21:47:03 +00:00
Zlatin Balevsky
18d84685ec wip on rewriting search page for sortable tables. Some features do not yet work 2019-12-11 20:45:12 +00:00
Zlatin Balevsky
c05a7a021c table styling and caret on the file tree from zzz 2019-12-11 14:40:49 +00:00
Zlatin Balevsky
a9935eba62 wip on restructuring search xhr 2019-12-11 14:38:42 +00:00
Zlatin Balevsky
e3d80bf809 remove fonts 2019-12-11 14:38:05 +00:00
Zlatin Balevsky
a59a1d3f30 sort active searches 2019-12-11 10:55:51 +00:00
Zlatin Balevsky
37ed75a3e8 sort table of active browses 2019-12-11 07:42:42 +00:00
Zlatin Balevsky
cd4b600ba2 working sorting of the browse host results 2019-12-10 23:56:39 +00:00
Zlatin Balevsky
fcd6dbcfbd wip on sortable tables 2019-12-10 23:24:11 +00:00
Zlatin Balevsky
f3ab15bd74 certificates in browse host page 2019-12-10 21:33:36 +00:00
Zlatin Balevsky
cddaad0f29 move certificate code in a separate file 2019-12-10 20:33:38 +00:00
Zlatin Balevsky
ecb597e0a0 preserve shown/hidden certificate comment state 2019-12-10 17:20:10 +00:00
Zlatin Balevsky
ec2a934f73 wip on show/hide certificate comments 2019-12-10 16:54:21 +00:00
Zlatin Balevsky
e1d630fdee wip on showing comments in certificates 2019-12-10 16:13:59 +00:00
Zlatin Balevsky
5807672503 proper ignore pattern 2019-12-10 16:04:42 +00:00
Zlatin Balevsky
2fadb314d3 css and layout changes from zzz 2019-12-10 15:35:54 +00:00
Zlatin Balevsky
ec5c15ff64 importing of certificates 2019-12-10 15:34:51 +00:00
Zlatin Balevsky
c169a7613f wip on importing certificates 2019-12-10 14:59:30 +00:00
Zlatin Balevsky
0f762968ae show fetched certificates in a table 2019-12-10 14:32:35 +00:00
Zlatin Balevsky
8e6517e7d8 content serving servlet, thx to zzz 2019-12-10 12:50:38 +00:00
Zlatin Balevsky
6946bff7f9 hook up periodic certificate update function 2019-12-10 12:47:56 +00:00
Zlatin Balevsky
37dcedb99b remove .orig 2019-12-10 12:26:00 +00:00
Zlatin Balevsky
afb92b0e4e translation updates, images, thanks zzz 2019-12-10 12:24:56 +00:00
Zlatin Balevsky
7c39dff34f wip on rendering certs table 2019-12-10 12:21:20 +00:00
Zlatin Balevsky
e41c122d2d show/hide links for certificates in group-by-sender view 2019-12-10 08:53:27 +00:00
Zlatin Balevsky
117c5eaf67 wip on showing certificates 2019-12-10 08:12:45 +00:00
Zlatin Balevsky
10fab2b47f certification in view by table 2019-12-09 23:21:24 +00:00
Zlatin Balevsky
3f71df3d29 certification support in tree view 2019-12-09 23:00:40 +00:00
Zlatin Balevsky
813e211200 send certified status to the UI 2019-12-09 17:22:30 +00:00
Zlatin Balevsky
1adb130fba ability to certify directories 2019-12-09 17:04:11 +00:00
Zlatin Balevsky
f69d4027db ability to certify files 2019-12-09 16:19:45 +00:00
Zlatin Balevsky
e0d006ec69 translate more strings 2019-12-09 15:39:18 +00:00
Zlatin Balevsky
81d8af57ed Translation infrastructure, thanks to zzz 2019-12-09 15:17:13 +00:00
Zlatin Balevsky
42c48a8e37 certificate backend 2019-12-09 14:26:39 +00:00
Zlatin Balevsky
3b1349b643 ability to force refresh lists 2019-12-09 10:06:56 +00:00
Zlatin Balevsky
0250ea329c fix some nevers and nulls 2019-12-09 09:54:10 +00:00
Zlatin Balevsky
b722c64ad8 comments in trust actions 2019-12-09 09:51:02 +00:00
Zlatin Balevsky
effa3b567e persist subscription lists 2019-12-09 09:20:07 +00:00
Zlatin Balevsky
64f198d599 fix live updating on trust action 2019-12-09 09:06:45 +00:00
Zlatin Balevsky
131b2defbb trust actions 2019-12-09 09:02:41 +00:00
Zlatin Balevsky
df5aab67ac hook up lists page 2019-12-09 08:22:42 +00:00
Zlatin Balevsky
fdc030904c wip on trust lists 2019-12-09 08:19:55 +00:00
Zlatin Balevsky
2a4fae8de4 wip on trust lists 2019-12-09 07:47:47 +00:00
Zlatin Balevsky
662b065116 wip on trust subscriptions 2019-12-09 07:37:04 +00:00
Zlatin Balevsky
300938fa44 wip on trust lists page 2019-12-09 07:04:35 +00:00
Zlatin Balevsky
086e27876d shut down more services explicitly 2019-12-09 05:38:41 +00:00
Zlatin Balevsky
247c62bfb4 hook up trust page 2019-12-09 05:23:40 +00:00
Zlatin Balevsky
a13315c324 describe the textbox 2019-12-09 04:38:14 +00:00
Zlatin Balevsky
65f40ef23a trust/neutral/distrust links 2019-12-09 04:32:35 +00:00
Zlatin Balevsky
96a611ff78 xhr fixes 2019-12-09 00:09:55 +00:00
Zlatin Balevsky
0f4119b74f submit trust functionality 2019-12-08 23:47:49 +00:00
Zlatin Balevsky
6847329093 trust buttons, submitting doesn't work yet 2019-12-08 23:25:06 +00:00
Zlatin Balevsky
9d2bcf70c7 display trust status in results 2019-12-08 22:30:38 +00:00
Zlatin Balevsky
aa33709f04 fix display of query 2019-12-08 21:08:49 +00:00
Zlatin Balevsky
eacaedaf3d automatically update active browse if the revision has changed 2019-12-08 21:05:29 +00:00
Zlatin Balevsky
f9c428cfcd update comment indexing 2019-12-08 20:48:15 +00:00
Zlatin Balevsky
aa1ede46d2 Redesign the XHR architecture by splitting the requests. Separate requests are issued for the status table, then a request is triggered when a user clicks on a search. 2019-12-08 20:41:54 +00:00
Zlatin Balevsky
3c43244631 wip on trust users view 2019-12-08 18:11:12 +00:00
Zlatin Balevsky
b468a6f19b update web.xml 2019-12-08 17:39:04 +00:00
Zlatin Balevsky
cfdc750ac0 post method 2019-12-08 17:36:30 +00:00
Zlatin Balevsky
6f8b006227 post method 2019-12-08 17:35:53 +00:00
Zlatin Balevsky
3f4bf986f3 remove stray orig file, update gitignore 2019-12-08 16:55:47 +00:00
Zlatin Balevsky
bef1033e12 plugin must compile with java 8 2019-12-08 16:41:45 +00:00
Zlatin Balevsky
13061d60a4 add local status to trust list xml 2019-12-08 15:13:30 +00:00
Zlatin Balevsky
5c6917a7e6 wip on trust views 2019-12-08 14:57:21 +00:00
Zlatin Balevsky
2ec15cfbbc remove jsp from urls, thanks to zzz 2019-12-08 13:45:26 +00:00
Zlatin Balevsky
1325a8dc65 resolve conflicts, fix quotes, thanks zzz 2019-12-08 13:03:21 +00:00
Zlatin Balevsky
b5d8fcf25b missed tx/config 2019-12-08 12:51:25 +00:00
Zlatin Balevsky
c22ff0678e mark script executable 2019-12-08 12:44:11 +00:00
Zlatin Balevsky
07051b813a translation infrastructure, thanks to zzz 2019-12-08 12:41:45 +00:00
Zlatin Balevsky
5c22af6576 add link to sidebar 2019-12-08 12:33:46 +00:00
Zlatin Balevsky
c3e1298ea3 browse links from search results 2019-12-08 12:31:02 +00:00
Zlatin Balevsky
949b616fdd fix xml, placeholders for browse links 2019-12-08 11:43:21 +00:00
Zlatin Balevsky
2b1d95e2ef pass sender's b64 and browse status from endpoint 2019-12-08 11:35:30 +00:00
Zlatin Balevsky
3d967da110 move browses table to top of page 2019-12-08 11:17:13 +00:00
Zlatin Balevsky
66fde32b64 comments support in browse host 2019-12-08 10:44:18 +00:00
Zlatin Balevsky
80a89a5ac0 download functionality 2019-12-08 09:38:34 +00:00
Zlatin Balevsky
c59e038c2a wip on browse host 2019-12-08 07:48:59 +00:00
Zlatin Balevsky
844bd8fd6e comments in shared files are encoded 2019-12-08 00:26:17 +00:00
Zlatin Balevsky
7d9ebb5b0b server side of browse host 2019-12-07 23:35:16 +00:00
Zlatin Balevsky
7fd7444dbf unshare directories to make sure files do not end up in the negative tree 2019-12-07 20:48:45 +00:00
Zlatin Balevsky
13af6cce22 stray println 2019-12-07 20:37:24 +00:00
Zlatin Balevsky
458dbec5fd display a refresh link if the table needs updating 2019-12-07 20:23:22 +00:00
Zlatin Balevsky
2137d6d30b comments in table view 2019-12-07 19:34:20 +00:00
Zlatin Balevsky
b28de0c119 add unshare link 2019-12-07 19:16:48 +00:00
Zlatin Balevsky
0fd4695b7c wip on table view 2019-12-07 18:53:32 +00:00
Zlatin Balevsky
74dddc4da4 wip on table view 2019-12-07 18:07:00 +00:00
Zlatin Balevsky
8bff987d30 implement adding comments to files 2019-12-07 17:19:13 +00:00
Zlatin Balevsky
de8684bafc fix multiline comments by not adding <br> tags in the servlet and using <pre> tag in the browser 2019-12-07 15:15:45 +00:00
Zlatin Balevsky
905f559aa9 proper <br /> tags 2019-12-07 14:23:50 +00:00
Zlatin Balevsky
c7f57c0b15 update sidebar, add sidebar to shared files, <br> in comments, thanks to zzz 2019-12-07 13:36:33 +00:00
Zlatin Balevsky
0f0f46f425 rename Files.jsp 2019-12-07 13:10:17 +00:00
Zlatin Balevsky
d6a3c8b24c re-add zzz's changes to FilesServlet 2019-12-07 13:04:31 +00:00
Zlatin Balevsky
8c661ca1ae unescape file names, this fixes unsharing of files with html characters 2019-12-07 12:59:43 +00:00
Zlatin Balevsky
f579c8754f more sidebar work thanks to zzz 2019-12-07 12:18:01 +00:00
Zlatin Balevsky
5c17536683 unsharing of directories 2019-12-07 12:14:49 +00:00
Zlatin Balevsky
8536353c26 unshare individual files 2019-12-07 11:20:56 +00:00
Zlatin Balevsky
84375c0201 fix typo and collapsing 2019-12-07 10:16:28 +00:00
Zlatin Balevsky
9c0c187a18 base64 encode the div ids to account for special characters in names 2019-12-07 10:13:17 +00:00
Zlatin Balevsky
8ae735e5c0 get tree structure to display, no collapsing yet 2019-12-07 09:31:56 +00:00
Zlatin Balevsky
8224dda3fd sidebar, servlet and styling improvements from zzz 2019-12-06 18:26:44 +00:00
Zlatin Balevsky
c852d7474e base64 encoding function 2019-12-06 17:25:18 +00:00
Zlatin Balevsky
71685d2052 clear hashing span when not hashing 2019-12-06 17:20:41 +00:00
Zlatin Balevsky
e57e513ca1 wip on sharing files 2019-12-06 17:02:40 +00:00
Zlatin Balevsky
aa4fb14540 wip on sharing and unsharing of files server-side 2019-12-06 15:58:02 +00:00
Zlatin Balevsky
5f74abc944 hook up files servlet and file manager 2019-12-06 13:44:15 +00:00
Zlatin Balevsky
c4135389a4 wip on shared files display page 2019-12-06 13:16:32 +00:00
Zlatin Balevsky
a6e0834722 add a single-level list traversal of the tree 2019-12-06 12:47:08 +00:00
Zlatin Balevsky
bc628b9c00 layout and escaping, thanks zzz 2019-12-06 11:02:53 +00:00
Zlatin Balevsky
9b2669a8b8 update to new api 2019-12-06 10:51:35 +00:00
Zlatin Balevsky
a0f70f7677 add traversal of the file tree 2019-12-06 10:51:07 +00:00
Zlatin Balevsky
23b2c912e2 genericize file tree 2019-12-06 10:08:27 +00:00
Zlatin Balevsky
ecfd4180c0 update test 2019-12-06 10:07:32 +00:00
Zlatin Balevsky
42489ba6b2 add support for showing/hiding comments 2019-12-06 01:34:35 +00:00
Zlatin Balevsky
61207f893d cancelled downloads do not count as downloading 2019-12-05 22:11:39 +00:00
Zlatin Balevsky
4e32359718 refresh downloads on cancel 2019-12-05 22:08:35 +00:00
Zlatin Balevsky
8d4af48eca cancel downloads via ajax too 2019-12-05 21:50:06 +00:00
Zlatin Balevsky
693f63534d download via ajax for group-by-file view as well 2019-12-05 21:37:17 +00:00
Zlatin Balevsky
b057e848d0 use ajax for starting downloads 2019-12-05 21:18:29 +00:00
Zlatin Balevsky
0114224d1f various html fixes, version the js, thanks zzz 2019-12-05 13:40:37 +00:00
Zlatin Balevsky
beab2be713 null checks on unitialized core, html escaping, move scriptst to <head>, thanks zzz 2019-12-05 12:19:10 +00:00
Zlatin Balevsky
edd4a1ff4b move download js into a separate file 2019-12-05 11:31:19 +00:00
Zlatin Balevsky
85814b7544 move search javascript into a separate file 2019-12-05 11:24:31 +00:00
Zlatin Balevsky
d46fbd66f0 move connection count into a separate js file, thanks zzz 2019-12-05 10:38:24 +00:00
Zlatin Balevsky
06bd9c80e8 move connection count refreshing into the header 2019-12-04 23:34:21 +00:00
Zlatin Balevsky
54b8628435 convert download servlet to xml and page to ajax 2019-12-04 23:15:50 +00:00
Zlatin Balevsky
b37a548771 Some refactoring thanks to zzz plus some wip on migrating downloads page to an xml-based servlet 2019-12-04 22:12:34 +00:00
Zlatin Balevsky
a14689acff set debug parameter to javac task 2019-12-04 19:37:58 +00:00
Zlatin Balevsky
a73bc956bf set the plugin icon 2019-12-04 19:23:43 +00:00
Zlatin Balevsky
d595a768b8 put images in images/ 2019-12-04 19:15:07 +00:00
Zlatin Balevsky
0fd6421fae bundle images in war 2019-12-04 19:03:05 +00:00
Zlatin Balevsky
6e9a36461a get mwClient from application scope 2019-12-04 19:02:51 +00:00
Zlatin Balevsky
d115f54812 copy icon from i2p source tree 2019-12-04 19:00:48 +00:00
Zlatin Balevsky
f627f661f2 add bote's css and images 2019-12-04 18:57:29 +00:00
Zlatin Balevsky
0e7ec3dfb3 move css to its own file 2019-12-04 11:27:08 +00:00
Zlatin Balevsky
0188bd34a9 add download buttons 2019-12-04 10:55:18 +00:00
Zlatin Balevsky
a2becfa6e2 implement grouping by file 2019-12-04 07:45:51 +00:00
Zlatin Balevsky
ea32af9b91 align tables 2019-12-04 05:17:16 +00:00
Zlatin Balevsky
c74c26e4c6 construct a tree structure to match XML received from servlet; populate tables from it 2019-12-04 02:48:28 +00:00
Zlatin Balevsky
382e21225b display list of senders 2019-12-03 23:59:51 +00:00
Zlatin Balevsky
81c406cbf6 refresh search results and connection count with ajax 2019-12-03 23:00:39 +00:00
Zlatin Balevsky
d9eb46d65c update min java version for plugin 2019-12-03 18:17:55 +00:00
Zlatin Balevsky
dadfed20f1 proper plugin build number 2019-12-03 16:25:38 +00:00
Zlatin Balevsky
6dad29a772 instructions for building the plugin 2019-12-03 16:05:26 +00:00
Zlatin Balevsky
884253fe29 easier running instructions 2019-12-03 12:07:39 +00:00
Zlatin Balevsky
a5eccbdc2b sleep a bit to give event chance to propagate 2019-12-03 06:04:14 +00:00
Zlatin Balevsky
d0318e3e83 display direct and possible sources. Pass possible sources to core 2019-12-03 06:00:56 +00:00
Zlatin Balevsky
d1c308f118 access mwClient from the application context 2019-12-02 14:12:23 +00:00
Zlatin Balevsky
3871170e44 show number of connections 2019-12-01 02:51:00 +00:00
Zlatin Balevsky
95dd5c4a7c downloads display, starting and stopping 2019-11-30 23:34:59 +00:00
Zlatin Balevsky
0bff4b55a5 format the results as table, add download buttons 2019-11-30 21:55:41 +00:00
Zlatin Balevsky
a2022415c2 add display of search results grouped by sender 2019-11-30 19:54:50 +00:00
Zlatin Balevsky
2b8bd8144f basic display of how many senders and results have arrived 2019-11-30 19:09:55 +00:00
Zlatin Balevsky
7bf520ac8c skeleton of search manager 2019-11-30 18:16:25 +00:00
Zlatin Balevsky
ad8983e889 wait for client manager to load before connecting 2019-11-30 17:32:02 +00:00
Zlatin Balevsky
d0b62af32e change url to point to servlet 2019-11-30 17:31:43 +00:00
Zlatin Balevsky
bc8e259974 update readme for web ui and version 2019-11-30 15:43:04 +00:00
Zlatin Balevsky
ff0a4661fd offload start to a thread, display wait page while the tunnel is opening 2019-11-30 14:56:04 +00:00
Zlatin Balevsky
9151df6816 kill i2p session on shutdown 2019-11-30 14:27:40 +00:00
Zlatin Balevsky
9c0878408b redirect to I2P log system 2019-11-30 14:07:58 +00:00
Zlatin Balevsky
61baa53076 _logManager cannot be set on RouterContexts (i.e. when running as plugin) 2019-11-30 13:26:20 +00:00
Zlatin Balevsky
b2841ee9ab fix redirects 2019-11-30 13:20:48 +00:00
Zlatin Balevsky
9edea17fb7 switch to using a servlet instead of bean 2019-11-30 13:07:47 +00:00
Zlatin Balevsky
ac17618f0c fix incomplete location setting 2019-11-30 10:50:58 +00:00
Zlatin Balevsky
e94ed4eafa init nickname and download locations 2019-11-30 10:22:19 +00:00
Zlatin Balevsky
8c33a5e62f hook up the mw client app with the jsp 2019-11-30 08:28:22 +00:00
Zlatin Balevsky
f9f1017e5b initialize core if nickname etc. is provided 2019-11-30 06:50:44 +00:00
Zlatin Balevsky
5d2d831b9e pass MW home to the client 2019-11-30 06:21:32 +00:00
Zlatin Balevsky
562d9a0f4a move i2p core dependency one level down, exclude core dependencies from plugin 2019-11-30 03:44:57 +00:00
Zlatin Balevsky
b981f9199b pass version to MW client app and get it to run 2019-11-30 03:16:10 +00:00
Zlatin Balevsky
efef0f3734 include a servlet as well as pre-compiled jsps 2019-11-29 18:00:32 +00:00
Zlatin Balevsky
cd0b860210 skeleton of client app 2019-11-29 17:02:15 +00:00
Zlatin Balevsky
9cb0655cfa get a buildable i2p plugin for mw 2019-11-29 16:49:44 +00:00
Zlatin Balevsky
3775f28af7 add jsp-based webui 2019-11-29 16:40:02 +00:00
Zlatin Balevsky
c33b824871 remove grails webui 2019-11-29 16:37:57 +00:00
Zlatin Balevsky
cf396b739e ability to chat from browse window 2019-11-29 03:41:59 +00:00
Zlatin Balevsky
631963f43c browse host by full nickname 2019-11-29 02:26:34 +00:00
Zlatin Balevsky
06cedb4f41 add buttons to copy short and full nickname to clipboard 2019-11-29 02:19:47 +00:00
Zlatin Balevsky
7a0c60a164 exit if user refuses to choose a nickname 2019-11-28 16:39:38 +00:00
Zlatin Balevsky
4c038ad932 set the geoip.dir property to load geoip 2019-11-27 16:00:56 +00:00
Zlatin Balevsky
f6dd38685a display country and strictness in I2P status 2019-11-27 15:38:51 +00:00
Zlatin Balevsky
2eab0f0567 make the chat monitor a separate frame so that it does not dissappear when MW is minimized 2019-11-26 18:55:20 +00:00
322 changed files with 49130 additions and 25953 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
# Dot directories
.gradle/
.idea/
.git/
# Build directories
build/
**/build/
# We execute COPY . .
# Modifying these files would unnecessarily invalidate the build context
Dockerfile

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@
.gradle
.project
.classpath
**/*.rej
**/*.orig

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
lang_map = he: iw, id: in, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
[I2P.MuWire]
file_filter = webui/locale/messages_<lang>.po
source_file = webui/locale/messages_en.po
source_lang = en
minimum_perc = 10

64
Dockerfile Normal file
View File

@@ -0,0 +1,64 @@
FROM jlesage/baseimage-gui:alpine-3.10-glibc
# Docker image version is provided via build arg.
ARG DOCKER_IMAGE_VERSION=unknown
# JDK version
ARG JDK=11
# Important directories
ARG TMP_DIR=/muwire-tmp
ENV APP_HOME=/muwire
# Define working directory.
WORKDIR $TMP_DIR
# Put sources into dir
COPY . .
# Install final dependencies
RUN add-pkg openjdk${JDK}-jre
# Build and untar in future distribution dir
RUN add-pkg --virtual openjdk${JDK}-jdk \
&& ./gradlew --no-daemon clean assemble \
&& mkdir -p ${APP_HOME} \
# Extract to ${APP_HOME and ignore the first dir
# First dir in tar is the "MuWire-<version>"
&& tar -C ${APP_HOME} --strip 1 -xvf gui/build/distributions/MuWire*.tar \
# Cleanup
&& rm -rf "${TMP_DIR}" /root/.gradle /root/.java \
&& del-pkg openjdk${JDK}-jdk
WORKDIR ${APP_HOME}
# Maximize only the main/initial window.
RUN \
sed-patch 's/<application type="normal">/<application type="normal" title="MuWire">/' \
/etc/xdg/openbox/rc.xml
# Generate and install favicons.
RUN \
APP_ICON_URL=https://github.com/zlatinb/muwire/raw/master/gui/griffon-app/resources/MuWire-128x128.png && \
install_app_icon.sh "$APP_ICON_URL"
# Add files.
COPY docker/rootfs/ /
# Set environment variables.
ENV APP_NAME="MuWire" \
S6_KILL_GRACETIME=8000
# Define mountable directories.
VOLUME ["$APP_HOME/.MuWire"]
VOLUME ["/incompletes"]
VOLUME ["/output"]
# Metadata.
LABEL \
org.label-schema.name="muwire" \
org.label-schema.description="Docker container for MuWire" \
org.label-schema.version="$DOCKER_IMAGE_VERSION" \
org.label-schema.vcs-url="https://github.com/zlatinb/muwire" \
org.label-schema.schema-version="1.0"

View File

@@ -2,11 +2,11 @@
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.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
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.6.6 is avaiable for download at https://muwire.com. You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various documentation.
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
### Building
## Building
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
@@ -21,25 +21,35 @@ If you want to run the unit tests, type
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
### Running the GUI
## Running the GUI
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z-all.jar` in a terminal or command prompt.
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`
### Running the CLI
## Running the CLI
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
The CLI is under active development and doesn't have all the features of the GUI.
### Web UI
## Running the Web UI / Plugin
If you are a Grails/Scala/JRuby/Kotlin developer and are interested in building a Web UI for MuWire, please get in touch. The MuWire core is written in Groovy and should be easy to integrate with any JVM-based language.
There is a Web-based UI under development. It is intended to be run as a plugin to the Java I2P router. Instructions how to build it are available at the wiki [Plugin] page.
### GPG Fingerprint
## Docker
MuWire is available as a Docker image. For more information see the [Docker] page.
## Translations
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
## GPG Fingerprint
```
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
@@ -53,3 +63,7 @@ You can find the full key at https://keybase.io/zlatinb
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[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

43
TODO.md
View File

@@ -1,8 +1,6 @@
# TODO List
Not in any particular order yet
### Big Items
### Network
##### Bloom Filters
@@ -12,15 +10,34 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability
##### Web UI, REST Interface, etc.
### Core
Basically any non-gui non-cli user interface
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items
* Wrapper of some kind for in-place upgrades
* 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
* Public Feed feature
### 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
### Swing GUI
* I2P Status panel - display message when connected to external router
* Search box - left identation
### Web UI/Plugin
* HTML 5 media players
* Remove versions from jar names
* Security: POST nonces, CSP headers
* Upload files from browser to plugin via drag-and-drop
* Check permissions, display better errors when sharing local folders

View File

@@ -2,8 +2,9 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
compile 'org.codehaus.groovy:groovy-all:2.4.15'
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'
}
compileGroovy {

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.7"
private static final String MW_VERSION = "0.6.10"
private static volatile Core core

View File

@@ -3,6 +3,7 @@ package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@@ -72,7 +73,7 @@ class FilesModel {
sharedFiles.each {
long size = it.getCachedLength()
boolean comment = it.comment != null
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size())
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)

View File

@@ -21,7 +21,6 @@ import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
class FilesView extends BasicWindow {
private final FilesModel model
@@ -84,7 +83,6 @@ class FilesView extends BasicWindow {
Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
core.eventBus.publish(new UIPersistFilesEvent())
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
} )
Button addCommentButton = new Button("Add Comment", {

View File

@@ -2,6 +2,7 @@ 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}"

View File

@@ -1,5 +1,8 @@
package com.muwire.core
import com.muwire.core.files.PersisterDoneEvent
import com.muwire.core.files.PersisterFolderService
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
@@ -29,9 +32,18 @@ import com.muwire.core.filecert.CertificateManager
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.filecert.UIImportCertificateEvent
import com.muwire.core.filefeeds.FeedClient
import com.muwire.core.filefeeds.FeedFetchEvent
import com.muwire.core.filefeeds.FeedItemFetchedEvent
import com.muwire.core.filefeeds.FeedManager
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.filefeeds.UIFilePublishedEvent
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
import com.muwire.core.filefeeds.UIFeedDeletedEvent
import com.muwire.core.filefeeds.UIFeedUpdateEvent
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileManager
@@ -41,7 +53,7 @@ import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@@ -74,10 +86,8 @@ import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.streaming.I2PSocketManager
import net.i2p.client.streaming.I2PSocketManagerFactory
import net.i2p.client.streaming.I2PSocketOptions
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Destination
import net.i2p.data.PrivateKey
import net.i2p.data.Signature
@@ -91,13 +101,16 @@ public class Core {
final EventBus eventBus
final Persona me
final String version;
final File home
final Properties i2pOptions
final MuWireSettings muOptions
private final TrustService trustService
private final TrustSubscriber trustSubscriber
private final I2PSession i2pSession;
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
private final PersisterFolderService persisterFolderService
private final HostCache hostCache
private final ConnectionManager connectionManager
private final CacheClient cacheClient
@@ -113,6 +126,8 @@ public class Core {
final CertificateManager certificateManager
final ChatServer chatServer
final ChatManager chatManager
final FeedManager feedManager
private final FeedClient feedClient
private final Router router
@@ -122,26 +137,27 @@ public class Core {
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
this.version = myVersion
this.muOptions = props
i2pOptions = new Properties()
def i2pOptionsFile = new File(home,"i2p.properties")
// Read defaults
def defaultI2PFile = getClass()
.getClassLoader().getResource("defaults/i2p.properties");
defaultI2PFile.withInputStream { i2pOptions.load(it) }
def i2pOptionsFile = new File(home, "i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
}
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
&& i2pOptions.hasProperty("i2np.udp.port")
)) {
Random r = new Random()
int port = r.nextInt(60000) + 4000
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
@@ -150,15 +166,18 @@ public class Core {
}
if (!props.embeddedRouter) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
}
} else {
log.info("launching embedded router")
Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("geoip.dir", home.getAbsolutePath() + File.separator + "geoip")
routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
@@ -185,10 +204,9 @@ public class Core {
// options like tunnel length and quantity
I2PSession i2pSession
I2PSocketManager socketManager
keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
socketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
}
socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000)
@@ -254,7 +272,17 @@ public class Core {
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info "initializing folder persistence service"
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
eventBus.register(FileLoadedEvent.class, persisterFolderService)
eventBus.register(FileHashedEvent.class, persisterFolderService)
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
eventBus.register(UICommentEvent.class, persisterFolderService)
eventBus.register(UIFilePublishedEvent.class, persisterFolderService)
eventBus.register(UIFileUnpublishedEvent.class, persisterFolderService)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@@ -274,10 +302,13 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
if (!props.plugin) {
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)
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("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
@@ -294,6 +325,19 @@ public class Core {
register(TrustEvent.class, chatServer)
}
log.info("initializing feed manager")
feedManager = new FeedManager(eventBus, home)
eventBus.with {
register(FeedItemFetchedEvent.class, feedManager)
register(FeedFetchEvent.class, feedManager)
register(UIFeedConfigurationEvent.class, feedManager)
register(UIFeedDeletedEvent.class, feedManager)
}
log.info("initializing feed client")
feedClient = new FeedClient(i2pConnector, eventBus, me, feedManager)
eventBus.register(UIFeedUpdateEvent.class, feedClient)
log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
@@ -305,6 +349,7 @@ public class Core {
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)
@@ -313,7 +358,7 @@ public class Core {
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
@@ -363,6 +408,7 @@ public class Core {
}
public void startServices() {
i2pSession.connect()
hasherService.start()
trustService.start()
trustService.waitForLoad()
@@ -372,7 +418,9 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
updateClient?.start()
feedManager.start()
feedClient.start()
}
public void shutdown() {
@@ -382,8 +430,16 @@ public class Core {
}
log.info("saving settings")
saveMuSettings()
log.info("shutting down host cache")
hostCache.stop()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down trust service")
trustService.stop()
log.info("shutting down persister service")
persisterService.stop()
log.info("shutting down persisterFolder service")
persisterFolderService.stop()
log.info("shutting down download manager")
downloadManager.shutdown()
log.info("shutting down connection acceptor")
@@ -398,12 +454,20 @@ public class Core {
chatServer.stop()
log.info("shutting down chat manager")
chatManager.shutdown()
log.info("shutting down feed manager")
feedManager.stop()
log.info("shutting down feed client")
feedClient.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
log.info("killing i2p session")
i2pSession.destroySession()
if (router != null) {
log.info("shutting down embedded router")
router.shutdown(0)
}
log.info("shutting down event bus");
eventBus.shutdown()
log.info("shutdown complete")
}
@@ -411,6 +475,11 @@ public class Core {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
public void saveI2PSettings() {
File f = new File(home, "i2p.properties")
f.withOutputStream { i2pOptions.store(it, "I2P Options") }
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
@@ -436,7 +505,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.7")
Core core = new Core(props, home, "0.6.10")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -2,6 +2,7 @@ package com.muwire.core
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
@@ -12,7 +13,7 @@ import groovy.util.logging.Log
class EventBus {
private Map handlers = new HashMap()
private final Executor executor = Executors.newSingleThreadExecutor {r ->
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
def rv = new Thread(r)
rv.setDaemon(true)
rv.setName("event-bus")
@@ -53,4 +54,8 @@ class EventBus {
log.info("Unregistering $handler for type $eventType")
handlers[eventType]?.remove(handler)
}
void shutdown() {
executor.shutdownNow()
}
}

View File

@@ -31,6 +31,16 @@ class MuWireSettings {
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean fileFeed
boolean advertiseFeed
boolean autoPublishSharedFiles
boolean defaultFeedAutoDownload
int defaultFeedUpdateInterval
int defaultFeedItemsToKeep
boolean defaultFeedSequential
boolean startChatServer
int maxChatConnections
boolean advertiseChat
@@ -41,6 +51,7 @@ class MuWireSettings {
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
boolean plugin
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
@@ -76,10 +87,21 @@ class MuWireSettings {
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
plugin = Boolean.valueOf(props.getProperty("plugin","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"))
// feed settings
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
advertiseFeed = Boolean.valueOf(props.getProperty("advertiseFeed","true"))
autoPublishSharedFiles = Boolean.valueOf(props.getProperty("autoPublishSharedFiles", "false"))
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", "60"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
@@ -130,10 +152,21 @@ class MuWireSettings {
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("plugin", String.valueOf(plugin))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
// feed settings
props.setProperty("fileFeed", String.valueOf(fileFeed))
props.setProperty("advertiseFeed", String.valueOf(advertiseFeed))
props.setProperty("autoPublishSharedFiles", String.valueOf(autoPublishSharedFiles))
props.setProperty("defaultFeedAutoDownload", String.valueOf(defaultFeedAutoDownload))
props.setProperty("defaultFeedItemsToKeep", String.valueOf(defaultFeedItemsToKeep))
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))

View File

@@ -255,7 +255,6 @@ abstract class Connection implements Closeable {
return
}
// TODO: make this mandatory at some point
byte[] sig2 = null
long queryTime = 0
if (search.sig2 != null) {
@@ -278,8 +277,10 @@ abstract class Connection implements Closeable {
return
}
}
} else
} else {
log.info("no extended signature in query")
return
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,

View File

@@ -15,9 +15,11 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.chat.ChatServer
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.filefeeds.FeedItems
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel
@@ -161,6 +163,9 @@ class ConnectionAcceptor {
case (byte)'I':
processIRC(e)
break
case (byte)'F':
processFEED(e)
break
default:
throw new Exception("Invalid read $read")
}
@@ -310,6 +315,9 @@ class ConnectionAcceptor {
boolean chat = false
if (headers.containsKey('Chat'))
chat = Boolean.parseBoolean(headers['Chat'])
boolean feed = false
if (headers.containsKey('Feed'))
feed = Boolean.parseBoolean(headers['Feed'])
byte [] personaBytes = Base64.decode(headers['Sender'])
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
@@ -329,6 +337,7 @@ class ConnectionAcceptor {
def json = slurper.parse(payload)
results[i] = ResultsParser.parse(sender, resultsUUID, json)
results[i].chat = chat
results[i].feed = feed
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException bad) {
@@ -369,13 +378,21 @@ class ConnectionAcceptor {
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.running.get() && 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))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit(browser, System.currentTimeMillis(), "Browse Host");
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
@@ -519,5 +536,56 @@ class ConnectionAcceptor {
throw new Exception("Invalid IRC connection")
chatServer.handle(e)
}
private void processFEED(Endpoint e) {
try {
byte[] EED = new byte[5];
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(EED);
if (EED != "EED\r\n".getBytes(StandardCharsets.US_ASCII))
throw new Exception("Invalid FEED connection")
OutputStream os = e.getOutputStream()
Map<String, String> headers = DataUtil.readAllHeaders(dis)
if (!headers.containsKey("Persona"))
throw new Exception("Persona header missing")
Persona requestor = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
if (requestor.destination != e.destination)
throw new Exception("Requestor persona mismatch")
if (!settings.fileFeed) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
long timestamp = 0
if (headers.containsKey("Timestamp")) {
timestamp = Long.parseLong(headers['Timestamp'])
}
List<SharedFile> published = fileManager.getPublishedSince(timestamp)
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${published.size()}\r\n".getBytes(StandardCharsets.US_ASCII));
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
published.each {
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = FeedItems.sharedFileToObj(it, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
dos.close()
} finally {
e.close()
}
}
}

View File

@@ -1,6 +1,7 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
@@ -62,11 +63,6 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
File incompletes = muSettings.incompleteLocation
if (incompletes == null)
incompletes = new File(home, "incompletes")
incompletes.mkdirs()
def size = e.result[0].size
def infohash = e.result[0].infohash
@@ -79,12 +75,29 @@ public class DownloadManager {
destinations.addAll(e.sources)
destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
doDownload(infohash, e.target, size, pieceSize, e.sequential, destinations)
def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations,
incompletes, pieces)
downloaders.put(infohash, downloader)
}
public void onUIDownloadFeedItemEvent(UIDownloadFeedItemEvent e) {
Set<Destination> singleSource = new HashSet<>()
singleSource.add(e.item.getPublisher().getDestination())
doDownload(e.item.getInfoHash(), e.target, e.item.getSize(), e.item.getPieceSize(),
e.sequential, singleSource)
}
private void doDownload(InfoHash infoHash, File target, long size, int pieceSize,
boolean sequential, Set<Destination> destinations) {
File incompletes = muSettings.incompleteLocation
if (incompletes == null)
incompletes = new File(home, "incompletes")
incompletes.mkdirs()
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
def downloader = new Downloader(eventBus, this, me, target, size,
infoHash, pieceSize, connector, destinations,
incompletes, pieces)
downloaders.put(infoHash, downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))

View File

@@ -92,6 +92,22 @@ public class Downloader {
public synchronized InfoHash getInfoHash() {
infoHash
}
public File getFile() {
file
}
public int getNPieces() {
nPieces
}
public int getPieceSize() {
pieceSize
}
public long getLength() {
length
}
private synchronized void setInfoHash(InfoHash infoHash) {
this.infoHash = infoHash
@@ -249,6 +265,10 @@ public class Downloader {
}
active
}
public int getTotalWorkers() {
return activeWorkers.size();
}
public void resume() {
paused = false
@@ -385,8 +405,9 @@ public class Downloader {
}
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this))
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this,
infoHash: getInfoHash()))
}
endpoint?.close()

View File

@@ -10,17 +10,20 @@ import net.i2p.crypto.DSAEngine
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import net.i2p.data.SigningPublicKey
import net.i2p.data.Base64
class Certificate {
private final byte version
private final InfoHash infoHash
private final Name name, comment
private final long timestamp
private final Persona issuer
final Name name, comment
final long timestamp
final Persona issuer
private final byte[] sig
private volatile byte [] payload
private String base64;
Certificate(InputStream is) {
version = (byte) (is.read() & 0xFF)
if (version > Constants.FILE_CERT_VERSION)
@@ -131,6 +134,15 @@ class Certificate {
os.write(payload)
}
public String toBase64() {
if (base64 == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
return base64;
}
@Override
public int hashCode() {
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)

View File

@@ -32,7 +32,8 @@ class CertificateClient {
fetcherThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING,
user : e.host, infoHash : e.infoHash))
endpoint = connector.connect(e.host.destination)
String infoHashString = Base64.encode(e.infoHash.getRoot())
@@ -62,7 +63,8 @@ class CertificateClient {
int count = Integer.parseInt(headers['Count'])
// start pulling the certs
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count,
user : e.host, infoHash : e.infoHash))
DataInputStream dis = new DataInputStream(is)
for (int i = 0; i < count; i++) {
@@ -77,11 +79,14 @@ class CertificateClient {
continue
}
if (cert.infoHash == e.infoHash)
eventBus.publish(new CertificateFetchedEvent(certificate : cert))
eventBus.publish(new CertificateFetchedEvent(certificate : cert, user : e.host, infoHash : e.infoHash))
}
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.DONE, count : count,
user : e.host, infoHash : e.infoHash))
} catch (Exception bad) {
log.log(Level.WARNING,"Fetching certificates failed", bad)
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED,
user : e.host, infoHash : e.infoHash))
} finally {
endpoint?.close()
}

View File

@@ -1,8 +1,12 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchEvent extends Event {
CertificateFetchStatus status
int count
Persona user
InfoHash infoHash
}

View File

@@ -1,7 +1,11 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchedEvent extends Event {
Certificate certificate
Persona user
InfoHash infoHash
}

View File

@@ -70,7 +70,7 @@ class CertificateManager {
}
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
InfoHash infoHash = e.sharedFile.getInfoHash()
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
String name = e.sharedFile.getFile().getName()
long timestamp = System.currentTimeMillis()
@@ -119,7 +119,7 @@ class CertificateManager {
added
}
boolean hasLocalCertificate(InfoHash infoHash) {
public boolean hasLocalCertificate(InfoHash infoHash) {
if (!byInfoHash.containsKey(infoHash))
return false
Set<Certificate> set = byInfoHash.get(infoHash)
@@ -130,6 +130,13 @@ class CertificateManager {
return false
}
public boolean isImported(Certificate certificate) {
Set<Certificate> forInfoHash = byInfoHash.get(certificate.infoHash)
if (forInfoHash == null)
return false
forInfoHash.contains(certificate)
}
Set<Certificate> getByInfoHash(InfoHash infoHash) {
Set<Certificate> rv = new HashSet<>()
if (byInfoHash.containsKey(infoHash))

View File

@@ -0,0 +1,110 @@
package com.muwire.core.filefeeds
import java.util.logging.Level
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.zip.GZIPInputStream
import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
@Log
class FeedClient {
private final I2PConnector connector
private final EventBus eventBus
private final Persona me
private final FeedManager feedManager
private final ExecutorService feedFetcher = Executors.newCachedThreadPool()
private final Timer feedUpdater = new Timer("feed-updater", true)
FeedClient(I2PConnector connector, EventBus eventBus, Persona me, FeedManager feedManager) {
this.connector = connector
this.eventBus = eventBus
this.me = me
this.feedManager = feedManager
}
private void start() {
feedUpdater.schedule({updateAnyFeeds()} as TimerTask, 60000, 60000)
}
private void stop() {
feedUpdater.cancel()
feedFetcher.shutdown()
}
private void updateAnyFeeds() {
feedManager.getFeedsToUpdate().each { feed ->
feedFetcher.execute({updateFeed(feed)} as Runnable)
}
}
void onUIFeedUpdateEvent(UIFeedUpdateEvent e) {
Feed feed = feedManager.getFeed(e.host)
if (feed == null) {
log.severe("UI request to update non-existent feed " + e.host.getHumanReadableName())
return
}
feedFetcher.execute({updateFeed(feed)} as Runnable)
}
private void updateFeed(Feed feed) {
log.info("updating feed " + feed.getPublisher().getHumanReadableName())
Endpoint endpoint = null
try {
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.CONNECTING))
feed.setLastUpdateAttempt(System.currentTimeMillis())
endpoint = connector.connect(feed.getPublisher().getDestination())
OutputStream os = endpoint.getOutputStream()
os.write("FEED\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Timestamp:${feed.getLastUpdated()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code $code")
// parse all headers
Map<String,String> headers = DataUtil.readAllHeaders(is)
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int items = Integer.parseInt(headers['Count'])
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FETCHING, totalItems: items))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
for (int i = 0; i < items; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
FeedItem item = FeedItems.objToFeedItem(json, feed.getPublisher())
eventBus.publish(new FeedItemFetchedEvent(item: item))
}
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "Feed update failed", bad)
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FAILED))
} finally {
endpoint?.close()
}
}
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.Persona
class FeedFetchEvent extends Event {
Persona host
FeedFetchStatus status
int totalItems
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedItemFetchedEvent extends Event {
FeedItem item
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedItemLoadedEvent extends Event {
FeedItem item
}

View File

@@ -0,0 +1,79 @@
package com.muwire.core.filefeeds
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class FeedItems {
public static def sharedFileToObj(SharedFile sf, int certificates) {
def json = [:]
json.type = "FeedItem"
json.version = 1
json.name = Base64.encode(DataUtil.encodei18nString(sf.getFile().getName()))
json.infoHash = Base64.encode(sf.getRoot())
json.size = sf.getCachedLength()
json.pieceSize = sf.getPieceSize()
if (sf.getComment() != null)
json.comment = sf.getComment()
json.certificates = certificates
json.timestamp = sf.getPublishedTimestamp()
json
}
public static FeedItem objToFeedItem(def obj, Persona publisher) throws InvalidFeedItemException {
if (obj.timestamp == null)
throw new InvalidFeedItemException("No timestamp");
if (obj.name == null)
throw new InvalidFeedItemException("No name");
if (obj.size == null || obj.size <= 0 || obj.size > FileHasher.MAX_SIZE)
throw new InvalidFeedItemException("length missing or invalid ${obj.size}")
if (obj.pieceSize == null || obj.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || obj.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
throw new InvalidFeedItemException("piece size missing or invalid ${obj.pieceSize}")
if (obj.infoHash == null)
throw new InvalidFeedItemException("Infohash missing")
InfoHash infoHash
try {
infoHash = new InfoHash(Base64.decode(obj.infoHash))
} catch (Exception bad) {
throw new InvalidFeedItemException("Invalid infohash", bad)
}
String name
try {
name = DataUtil.readi18nString(Base64.decode(obj.name))
} catch (Exception bad) {
throw new InvalidFeedItemException("Invalid name", bad)
}
int certificates = 0
if (obj.certificates != null)
certificates = obj.certificates
new FeedItem(publisher, obj.timestamp, name, obj.size, obj.pieceSize, infoHash, certificates, obj.comment)
}
public static def feedItemToObj(FeedItem item) {
def json = [:]
json.type = "FeedItem"
json.version = 1
json.name = Base64.encode(DataUtil.encodei18nString(item.getName()))
json.infoHash = Base64.encode(item.getInfoHash().getRoot())
json.size = item.getSize()
json.pieceSize = item.getPieceSize()
json.timestamp = item.getTimestamp()
json.certificates = item.getCertificates()
json.comment = item.getComment()
json
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedLoadedEvent extends Event {
Feed feed
}

View File

@@ -0,0 +1,225 @@
package com.muwire.core.filefeeds
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.Collectors
import com.muwire.core.EventBus
import com.muwire.core.Persona
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
@Log
class FeedManager {
private final EventBus eventBus
private final File metadataFolder, itemsFolder
private final Map<Persona, Feed> feeds = new ConcurrentHashMap<>()
private final Map<Persona, Set<FeedItem>> feedItems = new ConcurrentHashMap<>()
private final ExecutorService persister = Executors.newSingleThreadExecutor({r ->
new Thread(r, "feed persister")
} as ThreadFactory)
FeedManager(EventBus eventBus, File home) {
this.eventBus = eventBus
File feedsFolder = new File(home, "filefeeds")
if (!feedsFolder.exists())
feedsFolder.mkdir()
this.metadataFolder = new File(feedsFolder, "metadata")
if (!metadataFolder.exists())
metadataFolder.mkdir()
this.itemsFolder = new File(feedsFolder, "items")
if (!itemsFolder.exists())
itemsFolder.mkdir()
}
public Feed getFeed(Persona persona) {
feeds.get(persona)
}
public Set<FeedItem> getFeedItems(Persona persona) {
feedItems.getOrDefault(persona, Collections.emptySet())
}
public List<Feed> getFeedsToUpdate() {
long now = System.currentTimeMillis()
feeds.values().stream().
filter({Feed f -> !f.getStatus().isActive()}).
filter({Feed f -> f.getLastUpdateAttempt() + f.getUpdateInterval() <= now})
.collect(Collectors.toList())
}
void start() {
log.info("starting feed manager")
persister.submit({loadFeeds()} as Runnable)
persister.submit({loadItems()} as Runnable)
}
void stop() {
persister.shutdown()
}
private void loadFeeds() {
def slurper = new JsonSlurper()
Files.walk(metadataFolder.toPath()).
filter( { it.getFileName().toString().endsWith(".json")}).
forEach( {
def parsed = slurper.parse(it.toFile())
Persona publisher = new Persona(new ByteArrayInputStream(Base64.decode(parsed.publisher)))
Feed feed = new Feed(publisher)
feed.setUpdateInterval(parsed.updateInterval)
feed.setLastUpdated(parsed.lastUpdated)
feed.setLastUpdateAttempt(parsed.lastUpdateAttempt)
feed.setItemsToKeep(parsed.itemsToKeep)
feed.setAutoDownload(parsed.autoDownload)
feed.setSequential(parsed.sequential)
feed.setStatus(FeedFetchStatus.IDLE)
feeds.put(feed.getPublisher(), feed)
eventBus.publish(new FeedLoadedEvent(feed : feed))
})
}
private void loadItems() {
def slurper = new JsonSlurper()
feeds.keySet().each { persona ->
File itemsFile = getItemsFile(feeds[persona])
if (!itemsFile.exists())
return // no items yet?
itemsFile.eachLine { line ->
def parsed = slurper.parseText(line)
FeedItem item = FeedItems.objToFeedItem(parsed, persona)
Set<FeedItem> items = feedItems.get(persona)
if (items == null) {
items = new ConcurrentHashSet<>()
feedItems.put(persona, items)
}
items.add(item)
eventBus.publish(new FeedItemLoadedEvent(item : item))
}
}
}
void onFeedItemFetchedEvent(FeedItemFetchedEvent e) {
Set<FeedItem> set = feedItems.get(e.item.getPublisher())
if (set == null) {
set = new ConcurrentHashSet<>()
feedItems.put(e.getItem().getPublisher(), set)
}
set.add(e.item)
}
void onFeedFetchEvent(FeedFetchEvent e) {
Feed feed = feeds.get(e.host)
if (feed == null) {
log.severe("Fetching non-existent feed " + e.host.getHumanReadableName())
return
}
feed.setStatus(e.status)
if (e.status.isActive())
return
if (e.status == FeedFetchStatus.FINISHED) {
feed.setStatus(FeedFetchStatus.IDLE)
feed.setLastUpdated(e.getTimestamp())
}
// save feed items, then save feed. This will save partial fetches too
// which is ok because the items are stored in a Set
persister.submit({saveFeedItems(e.host)} as Runnable)
persister.submit({saveFeedMetadata(feed)} as Runnable)
}
void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) {
feeds.put(e.feed.getPublisher(), e.feed)
persister.submit({saveFeedMetadata(e.feed)} as Runnable)
}
void onUIFeedDeletedEvent(UIFeedDeletedEvent e) {
Feed f = feeds.get(e.host)
if (f == null) {
log.severe("Deleting a non-existing feed " + e.host.getHumanReadableName())
return
}
persister.submit({deleteFeed(f)} as Runnable)
}
private void saveFeedItems(Persona publisher) {
Set<FeedItem> set = feedItems.get(publisher)
if (set == null)
return // can happen if nothing was published
Feed feed = feeds[publisher]
if (feed == null) {
log.severe("Persisting items for non-existing feed " + publisher.getHumanReadableName())
return
}
if (feed.getItemsToKeep() == 0)
return
List<FeedItem> list = new ArrayList<>(set)
if (feed.getItemsToKeep() > 0 && list.size() > feed.getItemsToKeep()) {
log.info("will persist ${feed.getItemsToKeep()}/${list.size()} items")
list.sort({l, r ->
Long.compare(r.getTimestamp(), l.getTimestamp())
} as Comparator<FeedItem>)
list = list[0..feed.getItemsToKeep() - 1]
}
File itemsFile = getItemsFile(feed)
itemsFile.withPrintWriter { writer ->
list.each { item ->
def obj = FeedItems.feedItemToObj(item)
def json = JsonOutput.toJson(obj)
writer.println(json)
}
}
}
private void saveFeedMetadata(Feed feed) {
File metadataFile = getMetadataFile(feed)
metadataFile.withPrintWriter { writer ->
def json = [:]
json.publisher = feed.getPublisher().toBase64()
json.itemsToKeep = feed.getItemsToKeep()
json.lastUpdated = feed.getLastUpdated()
json.updateInterval = feed.getUpdateInterval()
json.autoDownload = feed.isAutoDownload()
json.sequential = feed.isSequential()
json.lastUpdateAttempt = feed.getLastUpdateAttempt()
json = JsonOutput.toJson(json)
writer.println(json)
}
}
private void deleteFeed(Feed feed) {
feeds.remove(feed.getPublisher())
feedItems.remove(feed.getPublisher())
getItemsFile(feed).delete()
getMetadataFile(feed).delete()
}
private File getItemsFile(Feed feed) {
return new File(itemsFolder, feed.getPublisher().destination.toBase32() + ".json")
}
private File getMetadataFile(Feed feed) {
return new File(metadataFolder, feed.getPublisher().destination.toBase32() + ".json")
}
}

View File

@@ -0,0 +1,9 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class UIDownloadFeedItemEvent extends Event {
FeedItem item
File target
boolean sequential
}

View File

@@ -0,0 +1,12 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
/**
* Emitted when configuration of a feed changes.
* The object should already contain the updated values.
*/
class UIFeedConfigurationEvent extends Event {
Feed feed
boolean newFeed
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.Persona
class UIFeedDeletedEvent extends Event {
Persona host
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.Persona
class UIFeedUpdateEvent extends Event {
Persona host
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.SharedFile
class UIFilePublishedEvent extends Event {
SharedFile sf
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.SharedFile
class UIFileUnpublishedEvent extends Event {
SharedFile sf
}

View File

@@ -0,0 +1,168 @@
package com.muwire.core.files
import com.muwire.core.DownloadedFile
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service
import com.muwire.core.SharedFile
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.data.Destination
import java.util.stream.Collectors
abstract class BasePersisterService extends Service{
protected static FileLoadedEvent fromJson(def json) {
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
throw new IllegalArgumentException()
if (!(json.hashList instanceof List))
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
List hashList = (List) json.hashList
ByteArrayOutputStream baos = new ByteArrayOutputStream()
hashList.each {
byte [] hash = Base64.decode it.toString()
if (hash == null)
throw new IllegalArgumentException()
baos.write hash
}
byte[] hashListBytes = baos.toByteArray()
InfoHash ih = InfoHash.fromHashList(hashListBytes)
byte [] root = Base64.decode(json.infoHash.toString())
if (root == null)
throw new IllegalArgumentException()
if (!Arrays.equals(root, ih.getRoot()))
return null
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
}
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
sf.setComment(json.comment)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
}
protected static FileLoadedEvent fromJsonLite(json) {
if (json.file == null || json.length == null || json.root == null)
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
byte[] root = Base64.decode(json.root)
InfoHash ih = new InfoHash(root)
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
boolean published = false
long publishedTimestamp = -1
if (json.published != null && json.published) {
published = true
publishedTimestamp = json.publishedTimestamp
}
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
if (published)
df.publish(publishedTimestamp)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
}
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
sf.setComment(json.comment)
if (published)
sf.publish(publishedTimestamp)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
}
protected static toJson(SharedFile sf) {
def json = [:]
json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength()
json.root = Base64.encode(sf.getRoot())
json.pieceSize = sf.getPieceSize()
json.comment = sf.getComment()
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (!sf.searches.isEmpty()) {
Set searchers = new HashSet<>()
sf.searches.each {
def search = [:]
if (it.searcher != null)
search.searcher = it.searcher.toBase64()
search.timestamp = it.timestamp
search.query = it.query
searchers.add(search)
}
json.searchers = searchers
}
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
}
if (sf.isPublished()) {
json.published = true
json.publishedTimestamp = sf.getPublishedTimestamp()
}
json
}
}

View File

@@ -2,6 +2,7 @@ package com.muwire.core.files
import com.muwire.core.DownloadedFile
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.download.Downloader
import net.i2p.data.Destination
@@ -9,4 +10,5 @@ import net.i2p.data.Destination
class FileDownloadedEvent extends Event {
Downloader downloader
DownloadedFile downloadedFile
InfoHash infoHash
}

View File

@@ -1,16 +1,18 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
class FileHashedEvent extends Event {
SharedFile sharedFile
InfoHash infoHash
String error
@Override
public String toString() {
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
super.toString() + " sharedFile " + sharedFile?.file?.getAbsolutePath() + " error: $error"
}
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.files;
import java.io.File;
public interface FileListCallback<T> {
public void onFile(File f, T value);
public void onDirectory(File f);
}

View File

@@ -1,9 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
class FileLoadedEvent extends Event {
SharedFile loadedFile
InfoHash infoHash
String source
}

View File

@@ -1,5 +1,8 @@
package com.muwire.core.files
import java.util.stream.Collectors
import java.util.stream.Stream
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
@@ -24,7 +27,7 @@ class FileManager {
final Map<String, Set<File>> nameToFiles = new HashMap<>()
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree negativeTree = new FileTree()
final FileTree<Void> negativeTree = new FileTree<>()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
@@ -32,7 +35,7 @@ class FileManager {
this.eventBus = eventBus
for (String negative : settings.negativeFileTree) {
negativeTree.add(new File(negative))
negativeTree.add(new File(negative), null)
}
}
@@ -75,7 +78,7 @@ class FileManager {
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
InfoHash infoHash = sf.getInfoHash()
InfoHash infoHash = new InfoHash(sf.getRoot())
Set<SharedFile> existing = rootToFiles.get(infoHash)
if (existing == null) {
log.info("adding new root")
@@ -88,7 +91,7 @@ class FileManager {
negativeTree.remove(sf.file)
String parent = sf.getFile().getParent()
if (parent != null && settings.watchedDirectories.contains(parent)) {
negativeTree.add(sf.file.getParentFile())
negativeTree.add(sf.file.getParentFile(),null)
}
saveNegativeTree()
@@ -117,7 +120,7 @@ class FileManager {
void onFileUnsharedEvent(FileUnsharedEvent e) {
SharedFile sf = e.unsharedFile
InfoHash infoHash = sf.getInfoHash()
InfoHash infoHash = new InfoHash(sf.getRoot())
Set<SharedFile> existing = rootToFiles.get(infoHash)
if (existing != null) {
existing.remove(sf)
@@ -128,7 +131,7 @@ class FileManager {
fileToSharedFile.remove(sf.file)
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
negativeTree.add(sf.file)
negativeTree.add(sf.file,null)
saveNegativeTree()
}
@@ -190,6 +193,10 @@ class FileManager {
Set<SharedFile> getSharedFiles(byte []root) {
return rootToFiles.get(new InfoHash(root))
}
boolean isShared(InfoHash infoHash) {
rootToFiles.containsKey(infoHash)
}
void onSearchEvent(SearchEvent e) {
// hash takes precedence
@@ -254,4 +261,13 @@ class FileManager {
settings.negativeFileTree.clear()
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
}
public List<SharedFile> getPublishedSince(long timestamp) {
synchronized(fileToSharedFile) {
fileToSharedFile.values().stream().
filter({sf -> sf.isPublished()}).
filter({sf -> sf.getPublishedTimestamp() >= timestamp}).
collect(Collectors.toList())
}
}
}

View File

@@ -2,12 +2,12 @@ package com.muwire.core.files
import java.util.concurrent.ConcurrentHashMap
class FileTree {
class FileTree<T> {
private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
synchronized void add(File file) {
synchronized void add(File file, T value) {
List<File> path = new ArrayList<>()
path.add(file)
while (file.getParentFile() != null) {
@@ -29,6 +29,7 @@ class FileTree {
}
current = existing
}
current.value = value;
}
synchronized boolean remove(File file) {
@@ -45,13 +46,63 @@ class FileTree {
true
}
public static class TreeNode {
synchronized void traverse(FileTreeCallback<T> callback) {
doTraverse(root, callback);
}
synchronized void traverse(File from, FileTreeCallback<T> callback) {
if (from == null) {
doTraverse(root, callback);
} else {
TreeNode node = fileToNode.get(from);
if (node == null)
return
doTraverse(node, callback);
}
}
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
boolean leave = false
if (node.file != null) {
if (node.file.isFile())
callback.onFile(node.file, node.value)
else {
leave = true
callback.onDirectoryEnter(node.file)
}
}
node.children.each {
doTraverse(it, callback)
}
if (leave)
callback.onDirectoryLeave()
}
synchronized void list(File parent, FileListCallback<T> callback) {
TreeNode<T> node
if (parent == null)
node = root
else
node = fileToNode.get(parent)
node.children.each {
if (it.file.isFile())
callback.onFile(it.file, it.value)
else
callback.onDirectory(it.file)
}
}
public static class TreeNode<T> {
TreeNode parent
File file
T value;
final Set<TreeNode> children = new HashSet<>()
public int hashCode() {
file.hashCode()
Objects.hash(file)
}
public boolean equals(Object o) {

View File

@@ -0,0 +1,9 @@
package com.muwire.core.files;
import java.io.File;
public interface FileTreeCallback<T> {
public void onDirectoryEnter(File file);
public void onDirectoryLeave();
public void onFile(File file, T value);
}

View File

@@ -65,7 +65,8 @@ class HasherService {
} else {
eventBus.publish new FileHashingEvent(hashingFile: f)
def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash.getRoot(), FileHasher.getPieceSize(f.length())),
infoHash : hash)
}
}
}

View File

@@ -0,0 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
/**
* Should be triggered by the old PersisterService
* once it has finished reading the old file
*
* @see PersisterService
*/
class PersisterDoneEvent extends Event{
}

View File

@@ -0,0 +1,195 @@
package com.muwire.core.files
import com.muwire.core.*
import com.muwire.core.filefeeds.UIFilePublishedEvent
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
/**
* A persister that stores information about the files shared using
* individual JSON files in directories.
*
* The absolute path's 32bit hash to the shared file is used
* to build the directory and filename.
*
* This persister only starts working once the old persister has finished loading
* @see PersisterFolderService#getJsonPath
*/
@Log
class PersisterFolderService extends BasePersisterService {
final static int CUT_LENGTH = 6
private final Core core;
final File location
final EventBus listener
final int interval
final Timer timer
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterFolderService(Core core, File location, EventBus listener) {
this.core = core;
this.location = location
this.listener = listener
this.interval = interval
timer = new Timer("file-folder persister timer", true)
}
void stop() {
timer.cancel()
persisterExecutor.shutdown()
}
void onPersisterDoneEvent(PersisterDoneEvent persisterDoneEvent) {
log.info("Old persister done")
load()
}
void onFileHashedEvent(FileHashedEvent hashedEvent) {
if (core.getMuOptions().getAutoPublishSharedFiles() && hashedEvent.sharedFile != null)
hashedEvent.sharedFile.publish(System.currentTimeMillis())
persistFile(hashedEvent.sharedFile, hashedEvent.infoHash)
}
void onFileDownloadedEvent(FileDownloadedEvent downloadedEvent) {
if (core.getMuOptions().getShareDownloadedFiles()) {
if (core.getMuOptions().getAutoPublishSharedFiles())
downloadedEvent.downloadedFile.publish(System.currentTimeMillis())
persistFile(downloadedEvent.downloadedFile, downloadedEvent.infoHash)
}
}
/**
* Get rid of the json and hashlists of unshared files
* @param unsharedEvent
*/
void onFileUnsharedEvent(FileUnsharedEvent unsharedEvent) {
def jsonPath = getJsonPath(unsharedEvent.unsharedFile)
def jsonFile = jsonPath.toFile()
if(jsonFile.isFile()){
jsonFile.delete()
}
def hashListPath = getHashListPath(unsharedEvent.unsharedFile)
def hashListFile = hashListPath.toFile()
if (hashListFile.isFile())
hashListFile.delete()
}
void onFileLoadedEvent(FileLoadedEvent loadedEvent) {
if(loadedEvent.source == "PersisterService"){
log.info("Migrating persisted file from PersisterService: "
+ loadedEvent.loadedFile.file.absolutePath.toString())
persistFile(loadedEvent.loadedFile, loadedEvent.infoHash)
}
}
void onUICommentEvent(UICommentEvent e) {
persistFile(e.sharedFile,null)
}
void onUIFilePublishedEvent(UIFilePublishedEvent e) {
persistFile(e.sf, null)
}
void onUIFileUnpublishedEvent(UIFileUnpublishedEvent e) {
persistFile(e.sf, null)
}
void load() {
log.fine("Loading...")
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isDirectory()) {
try {
_load()
}
catch (IllegalArgumentException e) {
log.log(Level.WARNING, "couldn't load files", e)
}
} else {
location.mkdirs()
listener.publish(new AllFilesLoadedEvent())
}
loaded = true
}
/**
* Loads every JSON into memory
*/
private void _load() {
int loaded = 0
def slurper = new JsonSlurper()
Files.walk(location.toPath())
.filter({
it.getFileName().toString().endsWith(".json")
})
.forEach({
def parsed = slurper.parse it.toFile()
def event = fromJsonLite parsed
if (event == null) return
log.fine("loaded file $event.loadedFile.file")
listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
})
listener.publish(new AllFilesLoadedEvent())
}
private void persistFile(SharedFile sf, InfoHash ih) {
persisterExecutor.submit({
def jsonPath = getJsonPath(sf)
def startTime = System.currentTimeMillis()
jsonPath.parent.toFile().mkdirs()
jsonPath.toFile().withPrintWriter { writer ->
def json = toJson sf
json = JsonOutput.toJson(json)
writer.println json
}
if (ih != null) {
def hashListPath = getHashListPath(sf)
hashListPath.toFile().bytes = ih.hashList
}
log.fine("Time(ms) to write json+hashList: " + (System.currentTimeMillis() - startTime))
} as Runnable)
}
private Path getJsonPath(SharedFile sf){
def pathHash = sf.getB64PathHash()
return Paths.get(
location.getAbsolutePath(),
pathHash.substring(0, CUT_LENGTH),
pathHash.substring(CUT_LENGTH) + ".json"
)
}
private Path getHashListPath(SharedFile sf) {
def pathHash = sf.getB64PathHash()
return Paths.get(
location.getAbsolutePath(),
pathHash.substring(0, CUT_LENGTH),
pathHash.substring(CUT_LENGTH) + ".hashlist"
)
}
InfoHash loadInfoHash(SharedFile sf) {
def path = getHashListPath(sf)
InfoHash.fromHashList(path.toFile().bytes)
}
}

View File

@@ -1,40 +1,24 @@
package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service
import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
class PersisterService extends Service {
class PersisterService extends BasePersisterService {
final File location
final EventBus listener
final int interval
final Timer timer
final FileManager fileManager
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location
@@ -51,10 +35,6 @@ class PersisterService extends Service {
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
@@ -69,6 +49,7 @@ class PersisterService extends Service {
def event = fromJson parsed
if (event != null) {
log.fine("loaded file $event.loadedFile.file")
event.source = "PersisterService"
listener.publish event
loaded++
if (loaded % 10 == 0)
@@ -76,126 +57,18 @@ class PersisterService extends Service {
}
}
}
listener.publish(new AllFilesLoadedEvent())
} catch (IllegalArgumentException|NumberFormatException e) {
// Backup the old hashes
location.renameTo(
new File(location.absolutePath + ".bak")
)
listener.publish(new PersisterDoneEvent())
} catch (IllegalArgumentException e) {
log.log(Level.WARNING, "couldn't load files",e)
}
} else {
listener.publish(new AllFilesLoadedEvent())
listener.publish(new PersisterDoneEvent())
}
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
loaded = true
}
private static FileLoadedEvent fromJson(def json) {
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
throw new IllegalArgumentException()
if (!(json.hashList instanceof List))
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
List hashList = (List) json.hashList
ByteArrayOutputStream baos = new ByteArrayOutputStream()
hashList.each {
byte [] hash = Base64.decode it.toString()
if (hash == null)
throw new IllegalArgumentException()
baos.write hash
}
byte[] hashListBytes = baos.toByteArray()
InfoHash ih = InfoHash.fromHashList(hashListBytes)
byte [] root = Base64.decode(json.infoHash.toString())
if (root == null)
throw new IllegalArgumentException()
if (!Arrays.equals(root, ih.getRoot()))
return null
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df)
}
SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf)
}
private void persistFiles() {
persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
}
}
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
} as Runnable)
}
private def toJson(File f, SharedFile sf) {
def json = [:]
json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength()
InfoHash ih = sf.getInfoHash()
json.infoHash = sf.getB64EncodedHashRoot()
json.pieceSize = sf.getPieceSize()
json.hashList = sf.getB64EncodedHashList()
json.comment = sf.getComment()
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (!sf.searches.isEmpty()) {
Set searchers = new HashSet<>()
sf.searches.each {
def search = [:]
if (it.searcher != null)
search.searcher = it.searcher.toBase64()
search.timestamp = it.timestamp
search.query = it.query
searchers.add(search)
}
json.searchers = searchers
}
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
}
json
}
}

View File

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

View File

@@ -1,5 +1,7 @@
package com.muwire.core.hostcache
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionManager
@@ -27,6 +29,7 @@ class CacheClient {
final long interval
final MuWireSettings settings
final Timer timer
private final AtomicBoolean stopped = new AtomicBoolean();
public CacheClient(EventBus eventBus, HostCache cache,
ConnectionManager manager, I2PSession session,
@@ -47,9 +50,12 @@ class CacheClient {
void stop() {
timer.cancel()
stopped.set(true)
}
private void queryIfNeeded() {
if (stopped.get())
return
if (!manager.getConnections().isEmpty())
return
if (!cache.getHosts(1).isEmpty())
@@ -65,7 +71,12 @@ class CacheClient {
options.setSendLeaseSet(true)
CacheServers.getCacheServers().each {
log.info "Querying hostcache ${it.toBase32()}"
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
try {
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
} catch (Exception e) {
if (!stopped.get())
throw e
}
}
}

View File

@@ -35,7 +35,7 @@ class BrowseManager {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
eventBus.publish(new BrowseStatusEvent(host : e.host, status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
@@ -55,8 +55,10 @@ class BrowseManager {
int results = Integer.parseInt(headers['Count'])
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
// at this stage, start pulling the results
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING, totalResults : results))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FETCHING, totalResults : results))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
@@ -67,14 +69,15 @@ class BrowseManager {
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
result.chat = chat
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}

View File

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

View File

@@ -77,18 +77,19 @@ class ResultsSender {
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
infohash : new InfoHash(it.getRoot()),
pieceSize : pieceSize,
uuid : uuid,
browse : settings.browseFiles,
sources : suggested,
comment : comment,
certificates : certificates,
chat : chatServer.running.get() && settings.advertiseChat
chat : chatServer.running.get() && settings.advertiseChat,
feed : settings.fileFeed && settings.advertiseFeed
)
uiResultEvents << uiResultEvent
}
@@ -119,7 +120,7 @@ class ResultsSender {
me.write(os)
os.writeShort((short)results.length)
results.each {
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
@@ -138,10 +139,12 @@ class ResultsSender {
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.running.get() && 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))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each {
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
@@ -170,7 +173,7 @@ class ResultsSender {
obj.type = "Result"
obj.version = 2
obj.name = encodedName
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
obj.infohash = Base64.encode(sf.getRoot())
obj.size = sf.getCachedLength()
obj.pieceSize = sf.getPieceSize()

View File

@@ -18,7 +18,8 @@ class UIResultEvent extends Event {
boolean browse
int certificates
boolean chat
boolean feed
@Override
public String toString() {
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"

View File

@@ -10,8 +10,8 @@ import net.i2p.util.ConcurrentHashSet
class RemoteTrustList {
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
private final Persona persona
private final Set<TrustEntry> good, bad
final Persona persona
final Set<TrustEntry> good, bad
volatile long timestamp
volatile boolean forceUpdate
Status status = Status.NEW

View File

@@ -130,8 +130,8 @@ class TrustService extends Service {
}
public static class TrustEntry {
private final Persona persona
private final String reason
final Persona persona
final String reason
TrustEntry(Persona persona, String reason) {
this.persona = persona
this.reason = reason

View File

@@ -26,7 +26,7 @@ class TrustSubscriber {
private final I2PConnector i2pConnector
private final MuWireSettings settings
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
private final Object waitLock = new Object()
private volatile boolean shutdown
@@ -50,7 +50,7 @@ class TrustSubscriber {
thread?.interrupt()
updateThreads.shutdownNow()
}
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
if (!e.subscribe) {
remoteTrustLists.remove(e.persona.destination)
@@ -62,6 +62,10 @@ class TrustSubscriber {
}
}
}
public boolean isSubscribed(Persona p) {
remoteTrustLists.containsKey(p.destination)
}
private void checkLoop() {
try {

View File

@@ -83,7 +83,7 @@ class UpdateClient {
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (e.downloadedFile.infoHash != updateInfoHash)
if (e.infoHash != updateInfoHash)
return
updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))

View File

@@ -12,6 +12,7 @@ import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager
import com.muwire.core.files.PersisterFolderService
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
@@ -22,6 +23,7 @@ import net.i2p.data.Base64
public class UploadManager {
private final EventBus eventBus
private final FileManager fileManager
private final PersisterFolderService persisterService
private final MeshManager meshManager
private final DownloadManager downloadManager
private final MuWireSettings props
@@ -34,9 +36,11 @@ public class UploadManager {
public UploadManager(EventBus eventBus, FileManager fileManager,
MeshManager meshManager, DownloadManager downloadManager,
PersisterFolderService persisterService,
MuWireSettings props) {
this.eventBus = eventBus
this.fileManager = fileManager
this.persisterService = persisterService
this.meshManager = meshManager
this.downloadManager = downloadManager
this.props = props
@@ -162,7 +166,7 @@ public class UploadManager {
InfoHash fullInfoHash
if (downloader == null) {
fullInfoHash = sharedFiles.iterator().next().infoHash
fullInfoHash = persisterService.loadInfoHash(sharedFiles.iterator().next())
} else {
byte [] hashList = downloader.getInfoHash().getHashList()
if (hashList != null && hashList.length > 0)

View File

@@ -10,9 +10,9 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
public DownloadedFile(File file, byte[] root, int pieceSize, Set<Destination> sources)
throws IOException {
super(file, infoHash, pieceSize);
super(file, root, pieceSize);
this.sources = sources;
}

View File

@@ -68,11 +68,19 @@ public class Persona {
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32);
return humanReadableName;
}
public Destination getDestination() {
return destination;
}
public String toBase64() throws DataFormatException, IOException {
public String toBase64() {
if (base64 == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
write(baos);
try {
write(baos);
} catch (Exception impossible) {
throw new RuntimeException(impossible);
}
base64 = Base64.encode(baos.toByteArray());
}
return base64;

View File

@@ -2,7 +2,10 @@ package com.muwire.core;
import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -16,44 +19,49 @@ import net.i2p.data.Base64;
public class SharedFile {
private final File file;
private final InfoHash infoHash;
private final byte[] root;
private final int pieceSize;
private final String cachedPath;
private final long cachedLength;
private String b64PathHash;
private final String b64EncodedFileName;
private final String b64EncodedHashRoot;
private final List<String> b64EncodedHashList;
private volatile String comment;
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
private volatile boolean published;
private volatile long publishedTimestamp;
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
public SharedFile(File file, byte[] root, int pieceSize) throws IOException {
this.file = file;
this.infoHash = infoHash;
this.root = root;
this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length();
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
List<String> b64List = new ArrayList<String>();
byte[] tmp = new byte[32];
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
b64List.add(Base64.encode(tmp));
}
this.b64EncodedHashList = b64List;
}
public File getFile() {
return file;
}
public InfoHash getInfoHash() {
return infoHash;
public byte[] getPathHash() throws NoSuchAlgorithmException {
MessageDigest digester = MessageDigest.getInstance("SHA-256");
digester.update(file.getAbsolutePath().getBytes());
return digester.digest();
}
public String getB64PathHash() throws NoSuchAlgorithmException {
if(b64PathHash == null){
b64PathHash = Base64.encode(getPathHash());
}
return b64PathHash;
}
public byte[] getRoot() {
return root;
}
public int getPieceSize() {
@@ -73,14 +81,6 @@ public class SharedFile {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() {
return cachedPath;
}
@@ -116,10 +116,28 @@ public class SharedFile {
public void addDownloader(String name) {
downloaders.add(name);
}
public void publish(long timestamp) {
published = true;
publishedTimestamp = timestamp;
}
public void unpublish() {
published = false;
publishedTimestamp = 0;
}
public boolean isPublished() {
return published;
}
public long getPublishedTimestamp() {
return publishedTimestamp;
}
@Override
public int hashCode() {
return file.hashCode() ^ infoHash.hashCode();
return file.hashCode() ^ Arrays.hashCode(root);
}
@Override
@@ -127,7 +145,7 @@ public class SharedFile {
if (!(o instanceof SharedFile))
return false;
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
return file.equals(other.file) && Arrays.equals(root, other.root);
}
public static class SearchEntry {

View File

@@ -0,0 +1,81 @@
package com.muwire.core.filefeeds;
import com.muwire.core.Persona;
public class Feed {
private final Persona publisher;
private int updateInterval;
private long lastUpdated;
private volatile long lastUpdateAttempt;
private int itemsToKeep;
private boolean autoDownload;
private boolean sequential;
private FeedFetchStatus status;
public Feed(Persona publisher) {
this.publisher = publisher;
this.status = FeedFetchStatus.IDLE;
}
public int getUpdateInterval() {
return updateInterval;
}
public void setUpdateInterval(int updateInterval) {
this.updateInterval = updateInterval;
}
public long getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated(long lastUpdated) {
this.lastUpdated = lastUpdated;
}
public int getItemsToKeep() {
return itemsToKeep;
}
public void setItemsToKeep(int itemsToKeep) {
this.itemsToKeep = itemsToKeep;
}
public boolean isAutoDownload() {
return autoDownload;
}
public void setAutoDownload(boolean autoDownload) {
this.autoDownload = autoDownload;
}
public Persona getPublisher() {
return publisher;
}
public void setStatus(FeedFetchStatus status) {
this.status = status;
}
public FeedFetchStatus getStatus() {
return status;
}
public void setSequential(boolean sequential) {
this.sequential = sequential;
}
public boolean isSequential() {
return sequential;
}
public void setLastUpdateAttempt(long lastUpdateAttempt) {
this.lastUpdateAttempt = lastUpdateAttempt;
}
public long getLastUpdateAttempt() {
return lastUpdateAttempt;
}
}

View File

@@ -0,0 +1,19 @@
package com.muwire.core.filefeeds;
public enum FeedFetchStatus {
IDLE(false),
CONNECTING(true),
FETCHING(true),
FINISHED(false),
FAILED(false);
private final boolean active;
FeedFetchStatus(boolean active) {
this.active = active;
}
public boolean isActive() {
return active;
}
}

View File

@@ -0,0 +1,79 @@
package com.muwire.core.filefeeds;
import java.util.Objects;
import com.muwire.core.InfoHash;
import com.muwire.core.Persona;
public class FeedItem {
private final Persona publisher;
private final long timestamp;
private final String name;
private final long size;
private final int pieceSize;
private final InfoHash infoHash;
private final int certificates;
private final String comment;
public FeedItem(Persona publisher, long timestamp, String name, long size, int pieceSize, InfoHash infoHash,
int certificates, String comment) {
super();
this.publisher = publisher;
this.timestamp = timestamp;
this.name = name;
this.size = size;
this.pieceSize = pieceSize;
this.infoHash = infoHash;
this.certificates = certificates;
this.comment = comment;
}
public Persona getPublisher() {
return publisher;
}
public long getTimestamp() {
return timestamp;
}
public String getName() {
return name;
}
public long getSize() {
return size;
}
public int getPieceSize() {
return pieceSize;
}
public InfoHash getInfoHash() {
return infoHash;
}
public int getCertificates() {
return certificates;
}
public String getComment() {
return comment;
}
@Override
public int hashCode() {
return Objects.hash(publisher, timestamp, name, infoHash);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FeedItem))
return false;
FeedItem other = (FeedItem)o;
return Objects.equals(publisher, other.publisher) &&
timestamp == other.timestamp &&
Objects.equals(name, other.name) &&
Objects.equals(infoHash, other.infoHash);
}
}

View File

@@ -0,0 +1,30 @@
package com.muwire.core.filefeeds;
public class InvalidFeedItemException extends Exception {
public InvalidFeedItemException() {
super();
}
public InvalidFeedItemException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
public InvalidFeedItemException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public InvalidFeedItemException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public InvalidFeedItemException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}

View File

@@ -63,7 +63,7 @@ public class DataUtil {
((int)header[2] & 0xFF);
}
static String readi18nString(byte [] encoded) {
public static String readi18nString(byte [] encoded) {
if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);

View File

@@ -0,0 +1,8 @@
inbound.nickname=MuWire
outbound.nickname=MuWire
inbound.length=3
inbound.quantity=4
outbound.length=3
outbound.quantity=4
i2cp.tcp.host=127.0.0.1
i2cp.tcp.port=7654

View File

@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null, null)
acceptor.start()
Thread.sleep(100)
}

View File

@@ -10,8 +10,8 @@ class FileTreeTest {
File b = new File(a, "b")
File c = new File(b, "c")
FileTree tree = new FileTree()
tree.add(c)
FileTree<Void> tree = new FileTree<>()
tree.add(c,null)
assert tree.root.children.size() == 1
assert tree.fileToNode.size() == 3
@@ -28,15 +28,110 @@ class FileTreeTest {
File c = new File(b, "c")
File d = new File(b, "d")
FileTree tree = new FileTree()
tree.add(c)
FileTree<Void> tree = new FileTree<>()
tree.add(c,null)
assert tree.fileToNode.size() == 3
tree.add(d)
tree.add(d, null)
assert tree.fileToNode.size() == 4
tree.remove(d)
assert tree.fileToNode.size() == 3
}
@Test
public void testTraverse() {
Stack stack = new Stack()
Set<String> values = new HashSet<>()
StringBuilder sb = new StringBuilder()
def cb = new FileTreeCallback<String>() {
@Override
public void onDirectoryEnter(File file) {
stack.push(file)
}
@Override
public void onDirectoryLeave() {
stack.pop()
}
@Override
public void onFile(File file, String value) {
values.add(value)
}
}
File a = new File("a")
a.createNewFile()
File b = new File("b")
b.mkdir()
File c = new File(b, "c")
c.createNewFile()
File d = new File(b, "d")
d.mkdir()
File e = new File(d, "e")
e.createNewFile()
FileTree<String> tree = new FileTree<>()
tree.add(a, "a")
tree.add(b, "b")
tree.add(c, "c")
tree.add(d, "d")
tree.add(e, "e")
tree.traverse(cb)
assert stack.isEmpty()
assert values.size() == 3
assert values.contains("a")
assert values.contains("c")
assert values.contains("e")
}
@Test
public void testList() {
Set<File> directories = new HashSet<>()
Set<String> values = new HashSet<>()
def cb = new FileListCallback<String>() {
@Override
public void onDirectory(File file) {
directories.add(file)
}
@Override
public void onFile(File file, String value) {
values.add(value)
}
}
File a = new File("a")
a.createNewFile()
File b = new File("b")
b.mkdir()
File c = new File(b, "c")
c.createNewFile()
FileTree<String> tree = new FileTree<>()
tree.add(a, "a")
tree.add(b, "b")
tree.add(c, "c")
tree.list(null, cb)
assert directories.size() == 1
assert directories.contains(b)
assert values.size() == 1
assert values.contains("a")
directories.clear()
values.clear()
tree.list(b, cb)
assert directories.isEmpty()
assert values.size() == 1
assert values.contains("c")
}
}

View File

@@ -0,0 +1,26 @@
#!/usr/bin/with-contenv sh
#
# Add the app user to the password and group databases. This is needed just to
# make sure that mapping between the user/group ID and its name is possible.
#
set -e # Exit immediately if a command exits with a non-zero status.
set -u # Treat unset variables as an error.
cp /defaults/passwd /etc/passwd
cp /defaults/group /etc/group
cp /defaults/shadow /etc/shadow
chown root:shadow /etc/shadow
chmod 640 /etc/shadow
echo "$APP_USER:x:$USER_ID:$GROUP_ID::${APP_HOME:-/dev/null}:/sbin/nologin" >> /etc/passwd
echo "$APP_USER:x:$GROUP_ID:" >> /etc/group
# Make sure APP_HOME is editable by the user
if [[ -n "$APP_HOME" ]] ; then
chown -R "$APP_USER" "$APP_HOME"
chmod -R u+rw "$APP_HOME"
fi
# vim:ft=sh:ts=4:sw=4:et:sts=4

View File

@@ -0,0 +1,34 @@
#This file is UTF-8
#Tue Jan 14 12:08:47 GMT 2020
meshExpiration=60
autoDownloadUpdate=true
hostHopelessInterval=1440
uploadSlotsPerUser=-1
downloadLocation=/output
allowTrustLists=true
embeddedRouter=false
incompleteLocation=/incompletes
outBw=128
searchExtraHop=false
shareHiddenFiles=false
advertiseChat=true
totalUploadSlots=-1
hostClearInterval=15
searchComments=true
downloadSequentialRatio=0.8
maxChatConnectios=-1
trustListInterval=1
crawlerResponse=REGISTERED
browseFiles=true
lastUpdateCheck=1579003533112
hostRejectInterval=1
inBw=256
leaf=false
updateCheckInterval=24
plugin=false
downloadRetryInterval=60
speedSmoothSeconds=60
allowUntrusted=true
shareDownloadedFiles=true
startChatServer=false
updateType=jar

View File

@@ -0,0 +1 @@
i2cp.tcp.host=172.17.0.1

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Explicitly define HOME otherwise it might not have been set
export HOME=/muwire
echo "Starting MuWire"
exec /muwire/bin/MuWire

View File

@@ -1,6 +1,6 @@
group = com.muwire
version = 0.6.7
i2pVersion = 0.9.43
version = 0.6.10
i2pVersion = 0.9.44
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
@@ -16,6 +16,6 @@ author = zab@mail.i2p
signer = zab@mail.i2p
keystorePassword=changeit
websiteURL=http://muwire.i2p
updateURLsu3=http://muwire.i2p/MuWire.su3
updateURLsu3=http://muwire.i2p/MuWire-update.su3
pack200=true

View File

@@ -126,4 +126,9 @@ mvcGroups {
view = 'com.muwire.gui.ChatMonitorView'
controller = 'com.muwire.gui.ChatMonitorController'
}
'feed-configuration' {
model = 'com.muwire.gui.FeedConfigurationModel'
view = 'com.muwire.gui.FeedConfigurationView'
controller = 'com.muwire.gui.FeedConfigurationController'
}
}

View File

@@ -47,6 +47,7 @@ class BrowseController {
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.chatActionEnabled = e.chat
model.results << e
model.resultCount = model.results.size()
view.resultsTable.model.fireTableDataChanged()
@@ -112,8 +113,19 @@ class BrowseController {
return
def params = [:]
params['result'] = result
params['host'] = result.getSender()
params['infoHash'] = result.getInfohash()
params['name'] = result.getName()
params['core'] = core
mvcGroup.createMVCGroup("fetch-certificates", params)
}
@ControllerAction
void chat() {
dismiss()
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
mainFrameGroup.controller.startChat(model.host)
mainFrameGroup.view.showChatWindow.call()
}
}

View File

@@ -0,0 +1,36 @@
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.filefeeds.UIFeedConfigurationEvent
@ArtifactProviderFor(GriffonController)
class FeedConfigurationController {
@MVCMember @Nonnull
FeedConfigurationModel model
@MVCMember @Nonnull
FeedConfigurationView view
@ControllerAction
void save() {
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.core.eventBus.publish(new UIFeedConfigurationEvent(feed : model.feed))
cancel()
}
@ControllerAction
void cancel() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -28,7 +28,7 @@ class FetchCertificatesController {
core.eventBus.with {
register(CertificateFetchEvent.class, this)
register(CertificateFetchedEvent.class, this)
publish(new UIFetchCertificatesEvent(host : model.result.sender, infoHash : model.result.infohash))
publish(new UIFetchCertificatesEvent(host : model.host, infoHash : model.infoHash))
}
}

View File

@@ -23,6 +23,8 @@ class I2PStatusController {
Router router = core.router
model.networkStatus = router._context.commSystem().status.toStatusString()
model.floodfill = router._context.netDb().floodfillEnabled()
model.myCountry = router._context.commSystem().getOurCountry()
model.strictCountry = router._context.commSystem().isInStrictCountry()
model.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()

View File

@@ -3,17 +3,15 @@ package com.muwire.gui
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import groovy.json.StringEscapeUtils
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
@@ -28,15 +26,18 @@ import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.filefeeds.Feed
import com.muwire.core.filefeeds.FeedItem
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.filefeeds.UIFeedDeletedEvent
import com.muwire.core.filefeeds.UIFeedUpdateEvent
import com.muwire.core.filefeeds.UIFilePublishedEvent
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList
@@ -87,8 +88,19 @@ class MainFrameController {
search = search.trim()
if (search.length() == 0)
return
if (search.length() > 128)
search = search.substring(0,128)
if (search.length() > 128) {
try {
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(search)))
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
return
} catch (Exception notPersona) {
search = search.substring(0,128)
}
}
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
@@ -358,7 +370,6 @@ class MainFrameController {
sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
}
@ControllerAction
@@ -485,6 +496,121 @@ class MainFrameController {
startChat(p)
}
@ControllerAction
void copyShort() {
copy(model.core.me.getHumanReadableName())
}
@ControllerAction
void copyFull() {
copy(model.core.me.toBase64())
}
private void copy(String s) {
StringSelection selection = new StringSelection(s)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
@ControllerAction
void publish() {
def selectedFiles = view.selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return
if (model.publishButtonText == "Unpublish") {
selectedFiles.each {
it.unpublish()
model.core.eventBus.publish(new UIFileUnpublishedEvent(sf : it))
}
} else {
long now = System.currentTimeMillis()
selectedFiles.stream().filter({!it.isPublished()}).forEach({
it.publish(now)
model.core.eventBus.publish(new UIFilePublishedEvent(sf : it))
})
}
view.refreshSharedFiles()
}
@ControllerAction
void updateFileFeed() {
Feed feed = view.selectedFeed()
if (feed == null)
return
model.core.eventBus.publish(new UIFeedUpdateEvent(host: feed.getPublisher()))
}
@ControllerAction
void unsubscribeFileFeed() {
Feed feed = view.selectedFeed()
if (feed == null)
return
model.core.eventBus.publish(new UIFeedDeletedEvent(host : feed.getPublisher()))
runInsideUIAsync {
model.feeds.remove(feed)
model.feedItems.clear()
view.refreshFeeds()
}
}
@ControllerAction
void configureFileFeed() {
Feed feed = view.selectedFeed()
if (feed == null)
return
def params = [:]
params['core'] = core
params['feed'] = feed
mvcGroup.createMVCGroup("feed-configuration", params)
}
@ControllerAction
void downloadFeedItem() {
List<FeedItem> items = view.selectedFeedItems()
if (items == null || items.isEmpty())
return
Feed f = model.core.getFeedManager().getFeed(items.get(0).getPublisher())
items.each {
if (!model.canDownload(it.getInfoHash()))
return
File target = new File(application.context.get("muwire-settings").downloadLocation, it.getName())
model.core.eventBus.publish(new UIDownloadFeedItemEvent(item : it, target : target, sequential : f.isSequential()))
}
view.showDownloadsWindow.call()
}
@ControllerAction
void viewFeedItemComment() {
List<FeedItem> items = view.selectedFeedItems()
if (items == null || items.size() != 1)
return
FeedItem item = items.get(0)
String groupId = Base64.encode(item.getInfoHash().getRoot())
Map<String, Object> params = new HashMap<>()
params['text'] = DataUtil.readi18nString(Base64.decode(item.getComment()))
params['name'] = item.getName()
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
@ControllerAction
void viewFeedItemCertificates() {
List<FeedItem> items = view.selectedFeedItems()
if (items == null || items.size() != 1)
return
FeedItem item = items.get(0)
def params = [:]
params['core'] = core
params['host'] = item.getPublisher()
params['infoHash'] = item.getInfoHash()
params['name'] = item.getName()
mvcGroup.createMVCGroup("fetch-certificates", params)
}
void startChat(Persona p) {
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
def params = [:]
@@ -505,4 +631,4 @@ class MainFrameController {
core = e.getNewValue()
})
}
}
}

View File

@@ -122,7 +122,38 @@ class OptionsController {
model.outBw = text
settings.outBw = Integer.valueOf(text)
}
// feed saving
boolean fileFeed = view.fileFeedCheckbox.model.isSelected()
model.fileFeed = fileFeed
settings.fileFeed = fileFeed
boolean advertiseFeed = view.advertiseFeedCheckbox.model.isSelected()
model.advertiseFeed = advertiseFeed
settings.advertiseFeed = advertiseFeed
boolean autoPublishSharedFiles = view.autoPublishSharedFilesCheckbox.model.isSelected()
model.autoPublishSharedFiles = autoPublishSharedFiles
settings.autoPublishSharedFiles = autoPublishSharedFiles
boolean defaultFeedAutoDownload = view.defaultFeedAutoDownloadCheckbox.model.isSelected()
model.defaultFeedAutoDownload = defaultFeedAutoDownload
settings.defaultFeedAutoDownload = defaultFeedAutoDownload
boolean defaultFeedSequential = view.defaultFeedSequentialCheckbox.model.isSelected()
model.defaultFeedSequential = defaultFeedSequential
settings.defaultFeedSequential = defaultFeedSequential
String defaultFeedItemsToKeep = view.defaultFeedItemsToKeepField.text
model.defaultFeedItemsToKeep = defaultFeedItemsToKeep
settings.defaultFeedItemsToKeep = Integer.parseInt(defaultFeedItemsToKeep)
String defaultFeedUpdateInterval = view.defaultFeedUpdateIntervalField.text
model.defaultFeedUpdateInterval = defaultFeedUpdateInterval
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval)
// trust saving
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
model.onlyTrusted = onlyTrusted

View File

@@ -12,6 +12,8 @@ import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.filefeeds.Feed
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
@@ -107,6 +109,22 @@ class SearchTabController {
mvcGroup.createMVCGroup("browse", groupId, params)
}
@ControllerAction
void subscribe() {
def sender = view.selectedSender()
if (sender == null)
return
Feed feed = new Feed(sender)
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
feed.setSequential(core.muOptions.defaultFeedSequential)
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval * 60 * 1000)
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed: true))
mvcGroup.parentGroup.view.showFeedsWindow.call()
}
@ControllerAction
void chat() {
def sender = view.selectedSender()
@@ -139,7 +157,9 @@ class SearchTabController {
return
def params = [:]
params['result'] = event
params['host'] = event.getSender()
params['infoHash'] = event.getInfohash()
params['name'] = event.getName()
params['core'] = core
mvcGroup.createMVCGroup("fetch-certificates", params)
}

View File

@@ -44,6 +44,8 @@ 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")
@@ -53,25 +55,7 @@ class Ready extends AbstractLifecycleHandler {
props.incompleteLocation = new File(home, "incompletes")
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
props.updateType = System.getProperty("updateType","jar")
def 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 || 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
}
props.setNickname(nickname)
props.setNickname(selectNickname())
def portableDownloads = System.getProperty("portable.downloads")
@@ -98,23 +82,49 @@ class Ready extends AbstractLifecycleHandler {
Core core
try {
core = new Core(props, home, metadata["application.version"])
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)
application.getPropertyChangeListeners("core").each {
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
}
core.eventBus.publish(new UILoadedEvent())
} catch (Exception bad) {
log.log(Level.SEVERE,"couldn't initialize core",bad)
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)
application.getPropertyChangeListeners("core").each {
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
}
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
}
core.eventBus.publish(new UILoadedEvent())
nickname
}
}

View File

@@ -15,6 +15,7 @@ class BrowseModel {
@Observable boolean downloadActionEnabled
@Observable boolean viewCommentActionEnabled
@Observable boolean viewCertificatesActionEnabled
@Observable boolean chatActionEnabled
@Observable int totalResults
@Observable int resultCount

View File

@@ -0,0 +1,26 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.filefeeds.Feed
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class FeedConfigurationModel {
Core core
Feed feed
@Observable boolean autoDownload
@Observable boolean sequential
@Observable int updateInterval
@Observable int itemsToKeep
void mvcGroupInit(Map<String, String> args) {
autoDownload = feed.isAutoDownload()
sequential = feed.isSequential()
updateInterval = feed.getUpdateInterval() / 60000
itemsToKeep = feed.getItemsToKeep()
}
}

View File

@@ -1,6 +1,9 @@
package com.muwire.gui
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.filecert.CertificateFetchStatus
import com.muwire.core.filefeeds.FeedItem
import com.muwire.core.search.UIResultEvent
import griffon.core.artifact.GriffonModel
@@ -9,7 +12,9 @@ import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class FetchCertificatesModel {
UIResultEvent result
Persona host
InfoHash infoHash
String name
@Observable CertificateFetchStatus status
@Observable int totalCertificates

View File

@@ -16,6 +16,8 @@ class I2PStatusModel {
@Observable int ssuConnections
@Observable String networkStatus
@Observable boolean floodfill
@Observable String myCountry
@Observable boolean strictCountry
@Observable int participatingTunnels
@Observable int activePeers
@Observable int receiveBps

View File

@@ -28,6 +28,12 @@ import com.muwire.core.content.ContentControlEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.filecert.CertificateCreatedEvent
import com.muwire.core.filefeeds.Feed
import com.muwire.core.filefeeds.FeedFetchEvent
import com.muwire.core.filefeeds.FeedItemFetchedEvent
import com.muwire.core.filefeeds.FeedLoadedEvent
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@@ -61,6 +67,7 @@ import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
@@ -89,6 +96,8 @@ class MainFrameModel {
def trusted = []
def distrusted = []
def subscriptions = []
def feeds = []
def feedItems = []
boolean sessionRestored
@@ -103,6 +112,14 @@ class MainFrameModel {
@Observable boolean previewButtonEnabled
@Observable String resumeButtonText
@Observable boolean addCommentButtonEnabled
@Observable boolean publishButtonEnabled
@Observable String publishButtonText
@Observable boolean updateFileFeedButtonEnabled
@Observable boolean unsubscribeFileFeedButtonEnabled
@Observable boolean configureFileFeedButtonEnabled
@Observable boolean downloadFeedItemButtonEnabled
@Observable boolean viewFeedItemCommentButtonEnabled
@Observable boolean viewFeedItemCertificatesButtonEnabled
@Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled
@@ -118,6 +135,7 @@ class MainFrameModel {
@Observable boolean downloadsPaneButtonEnabled
@Observable boolean uploadsPaneButtonEnabled
@Observable boolean monitorPaneButtonEnabled
@Observable boolean feedsPaneButtonEnabled
@Observable boolean trustPaneButtonEnabled
@Observable boolean chatPaneButtonEnabled
@@ -125,7 +143,7 @@ class MainFrameModel {
@Observable Downloader downloader
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new ConcurrentHashSet<>()
@Observable volatile Core core
@@ -215,6 +233,10 @@ class MainFrameModel {
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
core.eventBus.register(SearchEvent.class, this)
core.eventBus.register(CertificateCreatedEvent.class, this)
core.eventBus.register(FeedLoadedEvent.class, this)
core.eventBus.register(FeedFetchEvent.class, this)
core.eventBus.register(FeedItemFetchedEvent.class, this)
core.eventBus.register(UIFeedConfigurationEvent.class, this)
core.muOptions.watchedKeywords.each {
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
@@ -253,11 +275,13 @@ class MainFrameModel {
distrusted.addAll(core.trustService.bad.values())
resumeButtonText = "Retry"
publishButtonText = "Publish"
searchesPaneButtonEnabled = false
downloadsPaneButtonEnabled = true
uploadsPaneButtonEnabled = true
monitorPaneButtonEnabled = true
feedsPaneButtonEnabled = true
trustPaneButtonEnabled = true
chatPaneButtonEnabled = true
@@ -363,6 +387,8 @@ class MainFrameModel {
}
void onFileLoadedEvent(FileLoadedEvent e) {
if (e.source == "PersisterService")
return
runInsideUIAsync {
shared << e.loadedFile
loadedFiles = shared.size()
@@ -649,4 +675,41 @@ class MainFrameModel {
int requests
boolean finished
}
void onFeedLoadedEvent(FeedLoadedEvent e) {
runInsideUIAsync {
feeds << e.feed
view.refreshFeeds()
}
}
void onFeedFetchEvent(FeedFetchEvent e) {
runInsideUIAsync {
view.refreshFeeds()
}
}
void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) {
if (!e.newFeed)
return
runInsideUIAsync {
if (feeds.contains(e.feed))
return
feeds << e.feed
view.refreshFeeds()
}
}
void onFeedItemFetchedEvent(FeedItemFetchedEvent e) {
Feed feed = core.feedManager.getFeed(e.item.getPublisher())
if (feed == null || !feed.isAutoDownload())
return
if (!canDownload(e.item.getInfoHash()))
return
if (core.fileManager.isShared(e.item.getInfoHash()))
return
File target = new File(core.getMuOptions().getDownloadLocation(), e.item.getName())
core.eventBus.publish(new UIDownloadFeedItemEvent(item : e.item, target : target, sequential : feed.isSequential()))
}
}

View File

@@ -50,6 +50,15 @@ class OptionsModel {
@Observable String inBw
@Observable String outBw
// feed options
@Observable boolean fileFeed
@Observable boolean advertiseFeed
@Observable boolean autoPublishSharedFiles
@Observable boolean defaultFeedAutoDownload
@Observable String defaultFeedItemsToKeep
@Observable boolean defaultFeedSequential
@Observable String defaultFeedUpdateInterval
// trust options
@Observable boolean onlyTrusted
@Observable boolean searchExtraHop
@@ -105,6 +114,14 @@ class OptionsModel {
inBw = String.valueOf(settings.inBw)
outBw = String.valueOf(settings.outBw)
}
fileFeed = settings.fileFeed
advertiseFeed = settings.advertiseFeed
autoPublishSharedFiles = settings.autoPublishSharedFiles
defaultFeedAutoDownload = settings.defaultFeedAutoDownload
defaultFeedItemsToKeep = String.valueOf(settings.defaultFeedItemsToKeep)
defaultFeedSequential = settings.defaultFeedSequential
defaultFeedUpdateInterval = String.valueOf(settings.defaultFeedUpdateInterval)
onlyTrusted = !settings.allowUntrusted()
searchExtraHop = settings.searchExtraHop

View File

@@ -25,6 +25,7 @@ class SearchTabModel {
@Observable boolean viewCommentActionEnabled
@Observable boolean viewCertificatesActionEnabled
@Observable boolean chatActionEnabled
@Observable boolean subscribeActionEnabled
@Observable boolean groupedByFile
Core core

View File

@@ -1,6 +1,7 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
import griffon.core.artifact.GriffonModel
@@ -21,6 +22,6 @@ class SharedFileModel {
public void mvcGroupInit(Map<String,String> args) {
searchers.addAll(sf.getSearches())
downloaders.addAll(sf.getDownloaders())
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(new InfoHash(sf.getRoot()),[]))
}
}

View File

@@ -68,6 +68,7 @@ class BrowseView {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
button(text : "Chat", enabled : bind {model.chatActionEnabled}, chatAction)
button(text : "Dismiss", dismissAction)
label(text : "Download sequentially")
sequentialDownloadCheckbox = checkBox()

View File

@@ -5,6 +5,7 @@ import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JFrame
import javax.swing.SwingConstants
import java.awt.BorderLayout
@@ -20,18 +21,15 @@ class ChatMonitorView {
@MVCMember @Nonnull
ChatMonitorModel model
def mainFrame
def dialog
def panel
def window
def roomsTable
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.getAsInt("row-height")
dialog = new JDialog(mainFrame, "Chat Monitor", false)
dialog.setResizable(true)
panel = builder.panel {
window = builder.frame (visible : false, locationRelativeTo : null,
defaultCloseOperation : JFrame.DISPOSE_ON_CLOSE,
iconImage : builder.imageIcon("/MuWire-48x48.png").image){
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label("Chat rooms with unread messages")
@@ -53,15 +51,12 @@ class ChatMonitorView {
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(panel)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
window.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
window.pack()
window.setVisible(true)
}
}

View File

@@ -0,0 +1,73 @@
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.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class FeedConfigurationView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
FeedConfigurationModel model
def dialog
def p
def mainFrame
def autoDownloadCheckbox
def sequentialCheckbox
def itemsToKeepField
def updateIntervalField
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Feed Configuration", true)
dialog.setResizable(false)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label("Configuration for feed " + model.feed.getPublisher().getHumanReadableName())
}
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Automatically download files from feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoDownloadCheckbox = checkBox(selected : bind {model.autoDownload}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Download files from feed sequentially", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
sequentialCheckbox = checkBox(selected : bind {model.sequential}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
label(text : "Feed items to store on disk (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
itemsToKeepField = textField(text : bind {model.itemsToKeep}, constraints:gbc(gridx :1, gridy:2, anchor : GridBagConstraints.LINE_END))
label(text : "Feed refresh frequency in minutes", constraints : gbc(gridx: 0, gridy : 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
updateIntervalField = textField(text : bind {model.updateInterval}, constraints:gbc(gridx :1, gridy:3, anchor : GridBagConstraints.LINE_END))
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Save", saveAction)
button(text : "Cancel", cancelAction)
}
}
}
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

@@ -38,7 +38,7 @@ class FetchCertificatesView {
void initUI() {
int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.result.name, true)
dialog = new JDialog(mainFrame, model.name, true)
dialog.setResizable(true)
p = builder.panel {

View File

@@ -45,6 +45,10 @@ class I2PStatusView {
label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:2, anchor : GridBagConstraints.LINE_END))
label(text : "Our Country", constraints : gbc(gridx: 0, gridy: 3, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.myCountry}, constraints : gbc(gridx : 1, gridy: 3, anchor : GridBagConstraints.LINE_END))
label(text : "Strict Country", constraints : gbc(gridx:0, gridy:4, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.strictCountry}, constraints : gbc(gridx : 1, gridy : 4, anchor : GridBagConstraints.LINE_END))
}
panel(border : titledBorder(title : "Connections", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {

View File

@@ -35,9 +35,13 @@ import javax.swing.tree.TreePath
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader
import com.muwire.core.filefeeds.Feed
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 java.awt.BorderLayout
@@ -75,6 +79,8 @@ class MainFrameView {
def lastSharedSortEvent
def trustTablesSortEvents = [:]
def expansionListener = new TreeExpansions()
def lastFeedsSortEvent
def lastFeedItemsSortEvent
UISettings settings
@@ -150,6 +156,7 @@ class MainFrameView {
button(text: "Uploads", enabled : bind{model.uploadsPaneButtonEnabled}, actionPerformed : showUploadsWindow)
if (settings.showMonitor)
button(text: "Monitor", enabled: bind{model.monitorPaneButtonEnabled},actionPerformed : showMonitorWindow)
button(text: "Feeds", enabled: bind {model.feedsPaneButtonEnabled}, actionPerformed : showFeedsWindow)
button(text: "Trust", enabled:bind{model.trustPaneButtonEnabled},actionPerformed : showTrustWindow)
button(text: "Chat", enabled : bind{model.chatPaneButtonEnabled}, actionPerformed : showChatWindow)
}
@@ -289,8 +296,9 @@ class MainFrameView {
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
Core core = application.context.get("core")
core.certificateManager.hasLocalCertificate(it.getInfoHash())
core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
})
closureColumn(header : "Published", preferredWidth : 50, type : Boolean, read : {row -> row.isPublished()})
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
}
@@ -315,9 +323,11 @@ class MainFrameView {
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
}
panel {
button(text : "Share", actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction)
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)
}
panel {
panel {
@@ -424,6 +434,56 @@ class MainFrameView {
}
}
}
panel(constraints : "feeds window") {
gridLayout(rows : 2, cols : 1)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text: "Subscriptions")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "feeds-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.feeds) {
closureColumn(header : "Publisher", preferredWidth: 350, type : String, read : {it.getPublisher().getHumanReadableName()})
closureColumn(header : "Files", preferredWidth: 10, type : Integer, read : {model.core.feedManager.getFeedItems(it.getPublisher()).size()})
closureColumn(header : "Last Updated", type : Long, read : {it.getLastUpdated()})
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus()})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Update", enabled : bind {model.updateFileFeedButtonEnabled}, updateFileFeedAction)
button(text : "Unsubscribe", enabled : bind {model.unsubscribeFileFeedButtonEnabled}, unsubscribeFileFeedAction)
button(text : "Configure", enabled : bind {model.configureFileFeedButtonEnabled}, configureFileFeedAction)
}
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text : "Published Files")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "feed-items-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.feedItems) {
closureColumn(header : "Name", preferredWidth: 350, type : String, read : {it.getName()})
closureColumn(header : "Size", preferredWidth: 10, type : Long, read : {it.getSize()})
closureColumn(header : "Comment", preferredWidth: 10, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Certificates", preferredWidth: 10, type : Integer, read : {it.getCertificates()})
closureColumn(header : "Downloaded", preferredWidth: 10, type : Boolean, read : {
InfoHash ih = it.getInfoHash()
model.core.fileManager.isShared(ih)
})
closureColumn(header: "Date", type : Long, read : {it.getTimestamp()})
}
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadFeedItemButtonEnabled}, downloadFeedItemAction)
button(text : "View Comment", enabled : bind {model.viewFeedItemCommentButtonEnabled}, viewFeedItemCommentAction)
button(text : "View Certificates", enabled : bind {model.viewFeedItemCertificatesButtonEnabled}, viewFeedItemCertificatesAction )
}
}
}
panel(constraints : "trust window") {
gridLayout(rows : 2, cols : 1)
panel {
@@ -499,7 +559,11 @@ class MainFrameView {
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : bind {model.me}, constraints: BorderLayout.CENTER)
panel (constraints : BorderLayout.WEST) {
label(text : bind {model.me})
button(text : "Copy Short", copyShortAction)
button(text : "Copy Full", copyFullAction)
}
panel (constraints : BorderLayout.EAST) {
label("Connections:")
label(text : bind {model.connections})
@@ -677,14 +741,30 @@ class MainFrameView {
if (selectedFiles == null || selectedFiles.isEmpty())
return
model.addCommentButtonEnabled = true
model.publishButtonEnabled = true
boolean unpublish = true
selectedFiles.each {
unpublish &= it.isPublished()
}
model.publishButtonText = unpublish ? "Unpublish" : "Publish"
})
def sharedFilesTree = builder.getVariable("shared-files-tree")
sharedFilesTree.addMouseListener(sharedFilesMouseListener)
sharedFilesTree.addTreeSelectionListener({
def selectedNode = sharedFilesTree.getLastSelectedPathComponent()
model.addCommentButtonEnabled = selectedNode != null
model.publishButtonEnabled = selectedNode != null
def selectedFiles = selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return
boolean unpublish = true
selectedFiles.each {
unpublish &= it.isPublished()
}
model.publishButtonText = unpublish ? "Unpublish" : "Publish"
})
sharedFilesTree.addTreeExpansionListener(expansionListener)
@@ -740,6 +820,98 @@ class MainFrameView {
}
})
// feeds table
def feedsTable = builder.getVariable("feeds-table")
feedsTable.rowSorter.addRowSorterListener({evt -> lastFeedsSortEvent = evt})
feedsTable.rowSorter.setSortsOnUpdates(true)
feedsTable.setDefaultRenderer(Integer.class, centerRenderer)
feedsTable.setDefaultRenderer(Long.class, new DateRenderer())
selectionModel = feedsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
Feed selectedFeed = selectedFeed()
if (selectedFeed == null) {
model.updateFileFeedButtonEnabled = false
model.unsubscribeFileFeedButtonEnabled = false
model.configureFileFeedButtonEnabled = false
return
}
model.unsubscribeFileFeedButtonEnabled = true
model.configureFileFeedButtonEnabled = true
model.updateFileFeedButtonEnabled = !selectedFeed.getStatus().isActive()
def items = model.core.feedManager.getFeedItems(selectedFeed.getPublisher())
model.feedItems.clear()
model.feedItems.addAll(items)
def feedItemsTable = builder.getVariable("feed-items-table")
int selectedItemRow = feedItemsTable.getSelectedRow()
feedItemsTable.model.fireTableDataChanged()
if (selectedItemRow >= 0 && selectedItemRow < items.size())
feedItemsTable.selectionModel.setSelectionInterval(selectedItemRow, selectedItemRow)
})
feedsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if(e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
showFeedsPopupMenu(e)
}
@Override
public void mouseReleased(MouseEvent e) {
if(e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
showFeedsPopupMenu(e)
}
})
// feed items table
def feedItemsTable = builder.getVariable("feed-items-table")
feedItemsTable.rowSorter.addRowSorterListener({evt -> lastFeedItemsSortEvent = evt})
feedItemsTable.rowSorter.setSortsOnUpdates(true)
feedItemsTable.setDefaultRenderer(Integer.class, centerRenderer)
feedItemsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
feedItemsTable.columnModel.getColumn(5).setCellRenderer(new DateRenderer())
selectionModel = feedItemsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener({
List<FeedItem> selectedItems = selectedFeedItems()
if (selectedItems == null || selectedItems.isEmpty()) {
model.downloadFeedItemButtonEnabled = false
model.viewFeedItemCommentButtonEnabled = false
model.viewFeedItemCertificatesButtonEnabled = false
return
}
model.downloadFeedItemButtonEnabled = true
model.viewFeedItemCommentButtonEnabled = false
model.viewFeedItemCertificatesButtonEnabled = false
if (selectedItems.size() == 1) {
FeedItem item = selectedItems.get(0)
model.viewFeedItemCommentButtonEnabled = item.getComment() != null
model.viewFeedItemCertificatesButtonEnabled = item.getCertificates() > 0
}
})
feedItemsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
List<FeedItem> selectedItems = selectedFeedItems()
if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
showFeedItemsPopupMenu(e)
else if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 &&
selectedItems != null && selectedItems.size() == 1 &&
model.canDownload(selectedItems.get(0).getInfoHash())) {
mvcGroup.controller.downloadFeedItem()
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
showFeedItemsPopupMenu(e)
}
})
// subscription table
def subscriptionTable = builder.getVariable("subscription-table")
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
@@ -907,7 +1079,7 @@ class MainFrameView {
String roots = ""
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
SharedFile selected = iterator.next()
String root = Base64.encode(selected.infoHash.getRoot())
String root = Base64.encode(selected.getRoot())
roots += root
if (iterator.hasNext())
roots += "\n"
@@ -1002,6 +1174,52 @@ class MainFrameView {
showPopupMenu(menu, e)
}
void showFeedsPopupMenu(MouseEvent e) {
Feed feed = selectedFeed()
if (feed == null)
return
JPopupMenu menu = new JPopupMenu()
if (model.updateFileFeedButtonEnabled) {
JMenuItem update = new JMenuItem("Update")
update.addActionListener({mvcGroup.controller.updateFileFeed()})
menu.add(update)
}
JMenuItem unsubscribe = new JMenuItem("Unsubscribe")
unsubscribe.addActionListener({mvcGroup.controller.unsubscribeFileFeed()})
menu.add(unsubscribe)
JMenuItem configure = new JMenuItem("Configure")
configure.addActionListener({mvcGroup.controller.configureFileFeed()})
menu.add(configure)
showPopupMenu(menu,e)
}
void showFeedItemsPopupMenu(MouseEvent e) {
List<FeedItem> items = selectedFeedItems()
if (items == null || items.isEmpty())
return
// TODO: finish
JPopupMenu menu = new JPopupMenu()
if (model.downloadFeedItemButtonEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.controller.downloadFeedItem()})
menu.add(download)
}
if (model.viewFeedItemCommentButtonEnabled) {
JMenuItem viewComment = new JMenuItem("View Comment")
viewComment.addActionListener({mvcGroup.controller.viewFeedItemComment()})
menu.add(viewComment)
}
if (model.viewFeedItemCertificatesButtonEnabled) {
JMenuItem viewCertificates = new JMenuItem("View Certificates")
viewCertificates.addActionListener({mvcGroup.controller.viewFeedItemCertificates()})
menu.add(viewCertificates)
}
showPopupMenu(menu, e)
}
def selectedUploader() {
def uploadsTable = builder.getVariable("uploads-table")
int selectedRow = uploadsTable.getSelectedRow()
@@ -1052,6 +1270,7 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
@@ -1064,6 +1283,7 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = false
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
@@ -1076,6 +1296,7 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = false
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
@@ -1088,6 +1309,20 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = false
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showFeedsWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"feeds window")
model.searchesPaneButtonEnabled = true
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = false
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
@@ -1100,6 +1335,7 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = false
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
@@ -1112,6 +1348,7 @@ class MainFrameView {
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
model.monitorPaneButtonEnabled = true
model.feedsPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = false
chatNotificator.mainWindowActivated()
@@ -1168,6 +1405,43 @@ class MainFrameView {
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
public void refreshFeeds() {
JTable feedsTable = builder.getVariable("feeds-table")
int selectedFeed = feedsTable.getSelectedRow()
feedsTable.model.fireTableDataChanged()
if (selectedFeed >= 0)
feedsTable.selectionModel.setSelectionInterval(selectedFeed, selectedFeed)
JTable feedItemsTable = builder.getVariable("feed-items-table")
feedItemsTable.model.fireTableDataChanged()
}
Feed selectedFeed() {
JTable feedsTable = builder.getVariable("feeds-table")
int row = feedsTable.getSelectedRow()
if (row < 0)
return null
if (lastFeedsSortEvent != null)
row = feedsTable.rowSorter.convertRowIndexToModel(row)
model.feeds[row]
}
List<FeedItem> selectedFeedItems() {
JTable feedItemsTable = builder.getVariable("feed-items-table")
int [] selectedRows = feedItemsTable.getSelectedRows()
if (selectedRows.length == 0)
return null
List<FeedItem> rv = new ArrayList<>()
if (lastFeedItemsSortEvent != null) {
for (int i = 0; i < selectedRows.length; i++) {
selectedRows[i] = feedItemsTable.rowSorter.convertRowIndexToModel(selectedRows[i])
}
}
for (int selectedRow : selectedRows)
rv.add(model.feedItems[selectedRow])
rv
}
private void closeApplication() {
Core core = application.getContext().get("core")

View File

@@ -32,6 +32,7 @@ class OptionsView {
def i
def u
def bandwidth
def feed
def trust
def chat
@@ -66,6 +67,14 @@ class OptionsView {
def inBwField
def outBwField
def fileFeedCheckbox
def advertiseFeedCheckbox
def autoPublishSharedFilesCheckbox
def defaultFeedAutoDownloadCheckbox
def defaultFeedItemsToKeepField
def defaultFeedSequentialCheckbox
def defaultFeedUpdateIntervalField
def allowUntrustedCheckbox
def searchExtraHopCheckbox
@@ -257,6 +266,32 @@ class OptionsView {
}
panel(constraints : gbc(gridx: 0, gridy: 1, weighty: 100))
}
feed = builder.panel {
gridBagLayout()
panel (border : titledBorder(title : "General Feed Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Enable file feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
fileFeedCheckbox = checkBox(selected : bind {model.fileFeed}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Advertise feed in search results", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
advertiseFeedCheckbox = checkBox(selected : bind {model.advertiseFeed}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
label(text : "Automatically publish shared files", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoPublishSharedFilesCheckbox = checkBox(selected : bind {model.autoPublishSharedFiles}, constraints : gbc(gridx: 1, gridy : 2, anchor : GridBagConstraints.LINE_END))
}
panel (border : titledBorder(title : "Default Settings For New Feeds", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Automatically download files from feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
defaultFeedAutoDownloadCheckbox = checkBox(selected : bind {model.defaultFeedAutoDownload}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Download files from feed sequentially", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
defaultFeedSequentialCheckbox = checkBox(selected : bind {model.defaultFeedSequential}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
label(text : "Feed items to store on disk (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
defaultFeedItemsToKeepField = textField(text : bind {model.defaultFeedItemsToKeep}, constraints:gbc(gridx :1, gridy:2, anchor : GridBagConstraints.LINE_END))
label(text : "Feed refresh frequency in minutes", constraints : gbc(gridx: 0, gridy : 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
defaultFeedUpdateIntervalField = textField(text : bind {model.defaultFeedUpdateInterval}, constraints:gbc(gridx :1, gridy:3, anchor : GridBagConstraints.LINE_END))
}
panel(constraints : gbc(gridx: 0, gridy : 2, weighty: 100))
}
trust = builder.panel {
gridBagLayout()
panel (border : titledBorder(title : "Trust Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
@@ -311,6 +346,7 @@ class OptionsView {
if (core.router != null) {
tabbedPane.addTab("Bandwidth", bandwidth)
}
tabbedPane.addTab("Feed", feed)
tabbedPane.addTab("Trust", trust)
tabbedPane.addTab("Chat", chat)

View File

@@ -1,183 +0,0 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JPanel
import javax.swing.JTabbedPane
import javax.swing.SwingConstants
import com.muwire.core.Core
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def i
def u
def bandwidth
def trust
def retryField
def updateField
def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox
def inboundLengthField
def inboundQuantityField
def outboundLengthField
def outboundQuantityField
def i2pUDPPortField
def i2pNTCPPortField
def lnfField
def monitorCheckbox
def fontField
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def inBwField
def outBwField
def allowUntrustedCheckbox
def allowTrustListsCheckbox
def trustListIntervalField
def buttonsPanel
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:4))
button(text : "Choose", constraints : gbc(gridx : 1, gridy:4), downloadLocationAction)
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:5, gridwidth:2))
}
i = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
Core core = application.context.get("core")
if (core.router != null) {
label(text : "TCP Port", constraints : gbc(gridx :0, gridy: 5))
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:5))
label(text : "UDP Port", constraints : gbc(gridx :0, gridy: 6))
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:6))
}
}
u = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
}
bandwidth = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1))
inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2))
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2))
}
trust = builder.panel {
gridBagLayout()
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2))
label(text : "hours", constraints : gbc(gridx: 2, gridy:2))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire", p)
tabbedPane.addTab("I2P", i)
tabbedPane.addTab("GUI", u)
Core core = application.context.get("core")
if (core.router != null) {
tabbedPane.addTab("Bandwidth", bandwidth)
}
tabbedPane.addTab("Trust", trust)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())
panel.add(tabbedPane, BorderLayout.CENTER)
panel.add(buttonsPanel, BorderLayout.SOUTH)
d.getContentPane().add(panel)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

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