Compare commits
446 Commits
muwire-0.6
...
file-feeds
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a44603d2f | ||
![]() |
38a027c308 | ||
![]() |
2ba81ccc84 | ||
![]() |
0408349c07 | ||
![]() |
95cb7f3214 | ||
![]() |
69810d7203 | ||
![]() |
f202fa34f3 | ||
![]() |
c082e25c81 | ||
![]() |
2bb07ff7b5 | ||
![]() |
ff952890bc | ||
![]() |
fc393619d8 | ||
![]() |
2882c73876 | ||
![]() |
cbb1de046b | ||
![]() |
a272a45928 | ||
![]() |
3133581363 | ||
![]() |
c3d0dce281 | ||
![]() |
8f710e68c2 | ||
![]() |
15430d6c03 | ||
![]() |
166b71f128 | ||
![]() |
d724986ec6 | ||
![]() |
198c5b5538 | ||
![]() |
96d71ed08f | ||
![]() |
bb7385688c | ||
![]() |
e70bec3a51 | ||
![]() |
ed04c40420 | ||
![]() |
e9f00c2995 | ||
![]() |
fd75d8229b | ||
![]() |
0ff9ca8572 | ||
![]() |
a07f01b641 | ||
![]() |
b9333913c6 | ||
![]() |
fcb5c573f9 | ||
![]() |
1610766e01 | ||
![]() |
e2a9db8056 | ||
![]() |
a0cb214e2b | ||
![]() |
f2bf921d4c | ||
![]() |
aa0fcfb7de | ||
![]() |
48cfce71a8 | ||
![]() |
8798ea38e8 | ||
![]() |
17cd60afe3 | ||
![]() |
c10c1118e8 | ||
![]() |
28425e93dc | ||
![]() |
032338bb48 | ||
![]() |
12e56b1c9a | ||
![]() |
57c75978b6 | ||
![]() |
bfe198e1a6 | ||
![]() |
8e274f940e | ||
![]() |
9f3942c1c7 | ||
![]() |
d60d57ee43 | ||
![]() |
8e3a433afb | ||
![]() |
49cf56fabb | ||
![]() |
2b6565d107 | ||
![]() |
366a2ef841 | ||
![]() |
bcd24e56ac | ||
![]() |
c7d1f0c23c | ||
![]() |
853b9f67fc | ||
![]() |
a505a2449a | ||
![]() |
c11d81c6c3 | ||
![]() |
ee5e90c4ab | ||
![]() |
64d2a87d26 | ||
![]() |
f0304dbe7d | ||
![]() |
bdad8d9309 | ||
![]() |
8c110bbae5 | ||
![]() |
2cc1e384bc | ||
![]() |
9337d1b74d | ||
![]() |
16ed5dd346 | ||
![]() |
7b55fc9ed8 | ||
![]() |
d5c8050572 | ||
![]() |
83546d68d2 | ||
a891c83518 | |||
aa56cc23c0 | |||
a2b37ef567 | |||
4bc04ae631 | |||
56da9a16b0 | |||
2935ee1a1d | |||
855183397b | |||
e27704c1af | |||
5c18b4a141 | |||
dcd233b7ad | |||
7cee8a28ba | |||
7446fc949a | |||
598ab90f63 | |||
043028c296 | |||
cd1757fac3 | |||
9d4b365e63 | |||
![]() |
b12d57e30a | ||
![]() |
f33d1b6db3 | ||
![]() |
9e451460da | ||
![]() |
ffa52c129a | ||
b779fb75a0 | |||
fbe6b53278 | |||
b2bd95788d | |||
83d4a2624b | |||
03e20e21aa | |||
8a08955675 | |||
4ec54ebe54 | |||
758af6f48e | |||
a7bdd47fcd | |||
f7caa77a18 | |||
7641f64536 | |||
02baaace48 | |||
![]() |
d90067ff39 | ||
c910a215f5 | |||
65e073b1b9 | |||
489a7518c3 | |||
3733e48bbd | |||
c3723a1348 | |||
0e0f52bc77 | |||
60b9e990cf | |||
28ad0ae30f | |||
9142de85cd | |||
4eb31c11e3 | |||
e8afe358a5 | |||
![]() |
3db4317fc1 | ||
![]() |
5ad2b28527 | ||
![]() |
3036765f81 | ||
![]() |
8f9b1e5a8b | ||
![]() |
e6d59a2438 | ||
![]() |
32609b4779 | ||
![]() |
74ac4cfecf | ||
![]() |
69173c4156 | ||
![]() |
6283287bee | ||
![]() |
8e3f76f68c | ||
![]() |
574294fdc6 | ||
![]() |
8bd41546cd | ||
![]() |
ba5425c958 | ||
![]() |
22580f002c | ||
![]() |
5c773cec80 | ||
![]() |
7df00e6709 | ||
![]() |
5c05bd2562 | ||
![]() |
9df1d043e4 | ||
![]() |
6ea1a15641 | ||
![]() |
c0575facec | ||
![]() |
09168844e0 | ||
![]() |
e21d482393 | ||
![]() |
f5fc3e40c2 | ||
![]() |
796a0138fa | ||
![]() |
505b4ddb06 | ||
![]() |
a35216ff56 | ||
![]() |
fba92fe9b9 | ||
![]() |
1cc511b0ae | ||
![]() |
fa94c8ebfa | ||
![]() |
88b68a3c5c | ||
![]() |
b3e0d2ee7a | ||
![]() |
ce293cbda8 | ||
![]() |
3abc617e9f | ||
![]() |
67ee634f20 | ||
![]() |
503d54927f | ||
![]() |
5788329e1a | ||
![]() |
f0ffc68122 | ||
![]() |
3d710cebe5 | ||
![]() |
7d67573c92 | ||
![]() |
3acc676448 | ||
![]() |
2bf03b6b84 | ||
![]() |
b8ba6df4d5 | ||
![]() |
9fa7fa07b4 | ||
![]() |
1c7253ea0a | ||
![]() |
d947ad2997 | ||
![]() |
dd0bd6f5f8 | ||
![]() |
f05b6d0b40 | ||
![]() |
906c69a482 | ||
![]() |
5375b7aec0 | ||
![]() |
ea5da2431a | ||
![]() |
14b3a9ac9e | ||
![]() |
40bbef4583 | ||
![]() |
f811653247 | ||
![]() |
f321000071 | ||
![]() |
6eb85283cd | ||
![]() |
2973759cd9 | ||
![]() |
fe945a9941 | ||
![]() |
5f7e949310 | ||
![]() |
11edb2cb3c | ||
![]() |
ff1f801155 | ||
![]() |
0a98083c64 | ||
![]() |
75b2852f6e | ||
![]() |
5774cdee94 | ||
![]() |
2b0f4e52ca | ||
![]() |
1d20dc917b | ||
![]() |
63e3b3710c | ||
![]() |
0878b89082 | ||
![]() |
fecf0ecae8 | ||
![]() |
fec8d4ef9f | ||
![]() |
067ac8582a | ||
![]() |
31cac25a23 | ||
![]() |
6bcc44e01e | ||
![]() |
31652b34d7 | ||
![]() |
41a15fc7d5 | ||
![]() |
da3d7d7a50 | ||
![]() |
3a079d9f21 | ||
![]() |
ba0c85fe07 | ||
![]() |
ecb2283886 | ||
![]() |
cf9a18cee5 | ||
![]() |
982a93a04b | ||
![]() |
58137d11d1 | ||
![]() |
d87bec927d | ||
![]() |
dc8dd96495 | ||
![]() |
add9fb6feb | ||
![]() |
c500e95ab6 | ||
![]() |
477c3285d2 | ||
![]() |
1f5b112bfe | ||
![]() |
b0d09853e4 | ||
![]() |
b96d997037 | ||
![]() |
a631ec1e14 | ||
![]() |
62a06bc891 | ||
![]() |
3534b23194 | ||
![]() |
c561ae9140 | ||
![]() |
5926457eb5 | ||
![]() |
37c93e352b | ||
![]() |
be8fecda39 | ||
![]() |
7ec6257ac0 | ||
![]() |
c4ea58c330 | ||
![]() |
a482fe5c93 | ||
![]() |
2ee84848c4 | ||
![]() |
e29d7f6872 | ||
![]() |
5ded824ef2 | ||
![]() |
c607560cb8 | ||
![]() |
8b341bb125 | ||
![]() |
6bc5a9075b | ||
![]() |
6b1d2bc5ce | ||
![]() |
0cbbaf6a63 | ||
![]() |
3363b99675 | ||
![]() |
4ab4785539 | ||
![]() |
e595fa97e8 | ||
![]() |
65a7088463 | ||
![]() |
2d5bd653c1 | ||
![]() |
a864343c05 | ||
![]() |
696b348469 | ||
![]() |
b08333c5ea | ||
![]() |
0cf368c1af | ||
![]() |
62ab957892 | ||
![]() |
2b9e722165 | ||
![]() |
8cf4b23762 | ||
![]() |
1285c68521 | ||
![]() |
daa9e0bafc | ||
![]() |
8efd9c2c88 | ||
![]() |
918549f164 | ||
![]() |
e30a4666cb | ||
![]() |
26167abc08 | ||
![]() |
93f7c67f37 | ||
![]() |
f9a0a5e08a | ||
![]() |
d8ae275df2 | ||
![]() |
fce879be5d | ||
![]() |
0b58e22714 | ||
![]() |
dd230c4dfc | ||
![]() |
fba0b001c0 | ||
![]() |
6978c7b992 | ||
![]() |
7355e76e1b | ||
![]() |
5147cf21a0 | ||
![]() |
e8dd7d710d | ||
![]() |
fc9114eaa5 | ||
![]() |
20b7104c41 | ||
![]() |
570616951a | ||
![]() |
e075bfac55 | ||
![]() |
b6411a555c | ||
![]() |
d395475727 | ||
![]() |
8ae0a16b8a | ||
![]() |
38fcdfc97a | ||
![]() |
a0fb07cf99 | ||
![]() |
3747f9a5d5 | ||
![]() |
3a738f8f62 | ||
![]() |
ca56363438 | ||
![]() |
e06cb05e2a | ||
![]() |
8ab2dd7900 | ||
![]() |
26116d313a | ||
![]() |
738f177d6c | ||
![]() |
62c4579bbd | ||
![]() |
18d84685ec | ||
![]() |
c05a7a021c | ||
![]() |
a9935eba62 | ||
![]() |
e3d80bf809 | ||
![]() |
a59a1d3f30 | ||
![]() |
37ed75a3e8 | ||
![]() |
cd4b600ba2 | ||
![]() |
fcd6dbcfbd | ||
![]() |
f3ab15bd74 | ||
![]() |
cddaad0f29 | ||
![]() |
ecb597e0a0 | ||
![]() |
ec2a934f73 | ||
![]() |
e1d630fdee | ||
![]() |
5807672503 | ||
![]() |
2fadb314d3 | ||
![]() |
ec5c15ff64 | ||
![]() |
c169a7613f | ||
![]() |
0f762968ae | ||
![]() |
8e6517e7d8 | ||
![]() |
6946bff7f9 | ||
![]() |
37dcedb99b | ||
![]() |
afb92b0e4e | ||
![]() |
7c39dff34f | ||
![]() |
e41c122d2d | ||
![]() |
117c5eaf67 | ||
![]() |
10fab2b47f | ||
![]() |
3f71df3d29 | ||
![]() |
813e211200 | ||
![]() |
1adb130fba | ||
![]() |
f69d4027db | ||
![]() |
e0d006ec69 | ||
![]() |
81d8af57ed | ||
![]() |
42c48a8e37 | ||
![]() |
3b1349b643 | ||
![]() |
0250ea329c | ||
![]() |
b722c64ad8 | ||
![]() |
effa3b567e | ||
![]() |
64f198d599 | ||
![]() |
131b2defbb | ||
![]() |
df5aab67ac | ||
![]() |
fdc030904c | ||
![]() |
2a4fae8de4 | ||
![]() |
662b065116 | ||
![]() |
300938fa44 | ||
![]() |
086e27876d | ||
![]() |
247c62bfb4 | ||
![]() |
a13315c324 | ||
![]() |
65f40ef23a | ||
![]() |
96a611ff78 | ||
![]() |
0f4119b74f | ||
![]() |
6847329093 | ||
![]() |
9d2bcf70c7 | ||
![]() |
aa33709f04 | ||
![]() |
eacaedaf3d | ||
![]() |
f9c428cfcd | ||
![]() |
aa1ede46d2 | ||
![]() |
3c43244631 | ||
![]() |
b468a6f19b | ||
![]() |
cfdc750ac0 | ||
![]() |
6f8b006227 | ||
![]() |
3f4bf986f3 | ||
![]() |
bef1033e12 | ||
![]() |
13061d60a4 | ||
![]() |
5c6917a7e6 | ||
![]() |
2ec15cfbbc | ||
![]() |
1325a8dc65 | ||
![]() |
b5d8fcf25b | ||
![]() |
c22ff0678e | ||
![]() |
07051b813a | ||
![]() |
5c22af6576 | ||
![]() |
c3e1298ea3 | ||
![]() |
949b616fdd | ||
![]() |
2b1d95e2ef | ||
![]() |
3d967da110 | ||
![]() |
66fde32b64 | ||
![]() |
80a89a5ac0 | ||
![]() |
c59e038c2a | ||
![]() |
844bd8fd6e | ||
![]() |
7d9ebb5b0b | ||
![]() |
7fd7444dbf | ||
![]() |
13af6cce22 | ||
![]() |
458dbec5fd | ||
![]() |
2137d6d30b | ||
![]() |
b28de0c119 | ||
![]() |
0fd4695b7c | ||
![]() |
74dddc4da4 | ||
![]() |
8bff987d30 | ||
![]() |
de8684bafc | ||
![]() |
905f559aa9 | ||
![]() |
c7f57c0b15 | ||
![]() |
0f0f46f425 | ||
![]() |
d6a3c8b24c | ||
![]() |
8c661ca1ae | ||
![]() |
f579c8754f | ||
![]() |
5c17536683 | ||
![]() |
8536353c26 | ||
![]() |
84375c0201 | ||
![]() |
9c0c187a18 | ||
![]() |
8ae735e5c0 | ||
![]() |
8224dda3fd | ||
![]() |
c852d7474e | ||
![]() |
71685d2052 | ||
![]() |
e57e513ca1 | ||
![]() |
aa4fb14540 | ||
![]() |
5f74abc944 | ||
![]() |
c4135389a4 | ||
![]() |
a6e0834722 | ||
![]() |
bc628b9c00 | ||
![]() |
9b2669a8b8 | ||
![]() |
a0f70f7677 | ||
![]() |
23b2c912e2 | ||
![]() |
ecfd4180c0 | ||
![]() |
42489ba6b2 | ||
![]() |
61207f893d | ||
![]() |
4e32359718 | ||
![]() |
8d4af48eca | ||
![]() |
693f63534d | ||
![]() |
b057e848d0 | ||
![]() |
0114224d1f | ||
![]() |
beab2be713 | ||
![]() |
edd4a1ff4b | ||
![]() |
85814b7544 | ||
![]() |
d46fbd66f0 | ||
![]() |
06bd9c80e8 | ||
![]() |
54b8628435 | ||
![]() |
b37a548771 | ||
![]() |
a14689acff | ||
![]() |
a73bc956bf | ||
![]() |
d595a768b8 | ||
![]() |
0fd6421fae | ||
![]() |
6e9a36461a | ||
![]() |
d115f54812 | ||
![]() |
f627f661f2 | ||
![]() |
0e7ec3dfb3 | ||
![]() |
0188bd34a9 | ||
![]() |
a2becfa6e2 | ||
![]() |
ea32af9b91 | ||
![]() |
c74c26e4c6 | ||
![]() |
382e21225b | ||
![]() |
81c406cbf6 | ||
![]() |
d9eb46d65c | ||
![]() |
dadfed20f1 | ||
![]() |
6dad29a772 | ||
![]() |
884253fe29 | ||
![]() |
a5eccbdc2b | ||
![]() |
d0318e3e83 | ||
![]() |
d1c308f118 | ||
![]() |
3871170e44 | ||
![]() |
95dd5c4a7c | ||
![]() |
0bff4b55a5 | ||
![]() |
a2022415c2 | ||
![]() |
2b8bd8144f | ||
![]() |
7bf520ac8c | ||
![]() |
ad8983e889 | ||
![]() |
d0b62af32e | ||
![]() |
bc8e259974 | ||
![]() |
ff0a4661fd | ||
![]() |
9151df6816 | ||
![]() |
9c0878408b | ||
![]() |
61baa53076 | ||
![]() |
b2841ee9ab | ||
![]() |
9edea17fb7 | ||
![]() |
ac17618f0c | ||
![]() |
e94ed4eafa | ||
![]() |
8c33a5e62f | ||
![]() |
f9f1017e5b | ||
![]() |
5d2d831b9e | ||
![]() |
562d9a0f4a | ||
![]() |
b981f9199b | ||
![]() |
efef0f3734 | ||
![]() |
cd0b860210 | ||
![]() |
9cb0655cfa | ||
![]() |
3775f28af7 | ||
![]() |
c33b824871 | ||
![]() |
cf396b739e | ||
![]() |
631963f43c | ||
![]() |
06cedb4f41 | ||
![]() |
7a0c60a164 | ||
![]() |
4c038ad932 | ||
![]() |
f6dd38685a | ||
![]() |
2eab0f0567 |
12
.dockerignore
Normal file
12
.dockerignore
Normal 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
2
.gitignore
vendored
@@ -4,3 +4,5 @@
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
9
.tx/config
Normal file
9
.tx/config
Normal 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
64
Dockerfile
Normal 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"
|
32
README.md
32
README.md
@@ -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
43
TODO.md
@@ -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
|
||||
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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", {
|
||||
|
@@ -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}"
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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))
|
||||
|
@@ -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,
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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))
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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))
|
||||
|
110
core/src/main/groovy/com/muwire/core/filefeeds/FeedClient.groovy
Normal file
110
core/src/main/groovy/com/muwire/core/filefeeds/FeedClient.groovy
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemFetchedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemLoadedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedLoadedEvent extends Event {
|
||||
Feed feed
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadFeedItemEvent extends Event {
|
||||
FeedItem item
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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{
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIPersistFilesEvent extends Event {
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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))
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
81
core/src/main/java/com/muwire/core/filefeeds/Feed.java
Normal file
81
core/src/main/java/com/muwire/core/filefeeds/Feed.java
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
79
core/src/main/java/com/muwire/core/filefeeds/FeedItem.java
Normal file
79
core/src/main/java/com/muwire/core/filefeeds/FeedItem.java
Normal 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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
|
8
core/src/main/resources/defaults/i2p.properties
Normal file
8
core/src/main/resources/defaults/i2p.properties
Normal 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
|
@@ -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)
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
||||
|
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable file
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable 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
|
34
docker/rootfs/muwire/.MuWire/MuWire.properties
Normal file
34
docker/rootfs/muwire/.MuWire/MuWire.properties
Normal 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
|
1
docker/rootfs/muwire/.MuWire/i2p.properties
Normal file
1
docker/rootfs/muwire/.MuWire/i2p.properties
Normal file
@@ -0,0 +1 @@
|
||||
i2cp.tcp.host=172.17.0.1
|
7
docker/rootfs/startapp.sh
Normal file
7
docker/rootfs/startapp.sh
Normal 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
|
@@ -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
|
||||
|
@@ -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'
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ class BrowseModel {
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean chatActionEnabled
|
||||
@Observable int totalResults
|
||||
@Observable int resultCount
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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()))
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -25,6 +25,7 @@ class SearchTabModel {
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean chatActionEnabled
|
||||
@Observable boolean subscribeActionEnabled
|
||||
@Observable boolean groupedByFile
|
||||
|
||||
Core core
|
||||
|
@@ -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()),[]))
|
||||
}
|
||||
}
|
@@ -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()
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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)) {
|
||||
|
@@ -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")
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
Reference in New Issue
Block a user